Mysql中MVCC机制详解

事务的并发控制

并发执行的事务可能会读、写相同的记录,如果不对事务进行并发控制,可能会出现事务之间相互干扰的现象,导致数据不一致或者其
他异常。通过锁机制多版本并发控制(MVCC)以及事务的隔离级别(IsolationLevel)来控制多个事务的并发操作,以保证数据库事务的ACID属性。

事务并发可能导致的问题

  • 脏读: 事务1读取了事务2未提交的数据。
  • 不可重复读: 事务在执行的过程中,刚刚读取过的数据被别的事务修改了,事务再次读取同一批数据时发现数据时不一样的
  • 幻读: 在同一事务的操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据(期间被别的事务插入或者删除了数据)。

锁机制

分为表锁和行锁,表级锁用于锁定整个表,表锁有不同的等级,不同等级间的表锁可以相容,也可以互斥;行级锁则用于锁定单个行,更新数据时行锁是互斥的。DML(SELECT 除外)操作不仅会在行上加排他的行锁,还会在表上加相容的表锁,而 DDL操作通常会在表上加互斥的表锁。通过表锁与行锁,从而实现对DML、DDL的并发控制,确保数据的一致性和完整性。

可能会互相等待,出现死锁

多版本并发控制(MVCC)

MVCC 机制是一种用于提高数据库并发性能的技术,允许多个用户同时访问数据库而不会导致数据冲突和不一致性。

当 T2 事务只是读取时:

MVCC和锁的结合

通过锁与多版本并发控制(MVCC)机制⼀起来控制并发的读、写操作,实现了读不加锁,避免读写冲突,并能保证数据的完整性和一致性。

当 事务2 也包含写操作时就要加锁了:

隔离级别

隔离级别用于描述事务并发执行时互相干扰的程度。不同的隔离级别下事务间的相互影响程度不同,隔离级别越高,事务间的相互影响越小,允许出现的异常情况越少。
ANSI/ISO SQL 标准基于事务执行过程中必须避免的异常情况,定义了四种隔离级别。隔离强度由低到高排序:

隔离级别 英文名 能否出现脏读 能否出现不可重复读 能否出现幻读
读未提交 Read Uncommitted ✅ 可能 ✅ 可能 ✅ 可能
读已提交 Read Committed ❌ 不会 ✅ 可能 ✅ 可能
可重复读 Repeatable Read ❌ 不会 ❌ 不会 ✅ 可能(标准定义)
可串行化 Serializable ❌ 不会 ❌ 不会 ❌ 不会

MVCC 即多版本并发控制,通过保存数据的多个版本,使得 读操作(SELECT)不需要阻塞写操作(UPDATE/DELETE),从而实现高并发下的“读写不冲突”。

MVCC 适用于哪些隔离级别:

隔离级别 是否使用 MVCC 说明
READ UNCOMMITTED 可以读到未提交数据(脏读),不需要 MVCC
READ COMMITTED 每次读都读到最新提交版本
REPEATABLE READ(默认) 同一个事务中多次读到的数据一致
SERIALIZABLE 强制加锁,序列化执行,不用 MVCC

MVCC 的核心机制:隐藏列 + Undo Log + Read View

在 InnoDB 的每一行记录中,都隐含有两个重要的隐藏字段:

字段名 含义
trx_id 最后一次修改该行的事务 ID
roll_pointer 指向 undo log 的指针,用于回溯到旧版本

此外,还有两个核心组件:

1. Undo Log(回滚日志)

  • 每当事务修改数据时,旧版本会被保存在 Undo Log 中。

  • 这样就可以“回滚”或“读取历史版本”。

  • Undo Log 存在系统表空间或独立表空间中。

2. Read View(读视图)

  • 当 InnoDB 做“一致性读(consistent read)/快照读”时,会为该语句(或事务)创建一个 Read View(读视图 / snapshot)。

  • 它决定了当前事务能“看到”哪些版本的数据。

Read View 中包含以下关键字段:

  • m_ids:创建 Read View 时 正在运行(active) 的事务 ID 列表(即那些当时还没提交或回滚的事务)。
  • min_trx_id:最小活跃事务 ID
  • max_trx_id:下一个要分配的事务 ID(比所有已存在事务大 1)

可见性判断机制

条件 结果
记录的 trx_id < min_trx_id ✅ 可见(已提交的旧事务)
记录的 trx_idm_ids ❌ 不可见(仍在活跃事务中)
记录的 trx_idmax_trx_id ❌ 不可见(未来事务)

可是为什么按这三个条件/顺序来设计?

比较直观的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 参数: row_trx_id = 记录上标记的 trx_id(生成该版本的事务)
# read_view = { min_trx_id, max_trx_id, m_ids, creator_trx_id }
# cur_trx_id = 当前事务的 trx_id

if row_trx_id == cur_trx_id:
return VISIBLE # 当前事务应能看到自己做的修改
if row_trx_id < read_view.min_trx_id:
return VISIBLE # 老版本:一定由早先已提交的事务生成
if row_trx_id >= read_view.max_trx_id:
return NOT_VISIBLE # max_trx_id 是创建 Read View 时系统准备分配的下一个 ID,凡是 ≥ max_trx_id 的事务都是 在 Read View 之后才开始 的。

# 否则 row_trx_id 在 剩下的区间[min_trx_id, max_trx_id)里
# 说明该生成版本对应的事务在创建ReadView时是“可能活跃的”
if row_trx_id in read_view.m_ids:
return NOT_VISIBLE # 那个事务在创建ReadView时还没提交 —— 对快照不可见
else:
return VISIBLE # 不在活跃列表里,说明它已提交(但 id 在范围内)

示例展示

假设系统中事务 ID 单调增长(分配顺序为 100、101、102……):

事务 T1:trx_id = 100,已提交(早先的改动)。
事务 T2:trx_id = 200,正在运行(未提交)。
事务 T3:trx_id = 300,正在运行(未提交)。

此刻某事务 R(要读)创建了 Read View,复制了活跃列表 m_ids = [200, 300],得到 min_trx_id = 200, max_trx_id = 301(假设下一个分配 ID 是 301)。

现在遇到一条记录,其 row_trx_id 为:
90 → 90 < min_trx_id(200) → 可见(老版本,由更早提交的 Tx 生成)。
320 → 320 >= max_trx_id(301) → 不可见(由之后启动的事务生成)。
200 → 在 [min,max),并且 200 ∈ m_ids → 不可见(当时 T2 还活着)。
205 → 在 [min,max),但 205 ∉ m_ids → 可见(说明 205 的事务在创建 Read View 时已不在活跃列表 —— 已提交)。

跟 Undo Log 的配合(实际步骤)

行上只保存当前版本的 trx_id 和 roll_pointer(指向 undo log 的旧版本)。

当可见性检查判为“当前版本不可见”(例如属于别的活跃事务或未来事务),InnoDB 会沿着 roll_pointer 到 undo log 找前一个版本,对那个版本再走同样的可见性判定,直到找到某个对当前 Read View 可见的版本,或没有更早版本(此时记录视为不存在)。

因此 Read View 的判定和 undo log 的版本链配合起来,完成“回溯到对当前快照可见的历史值”。

  • Copyrights © 2023-2026 Hexo

请我喝杯咖啡吧~

支付宝
微信