一、MVCC 的核心思想
目标 :
允许读写操作并发执行,读操作不阻塞写操作,写操作不阻塞读操作,从而避免锁竞争。
基本原理:
- 每个事务修改数据时生成数据的新版本(版本链)。
- 读操作根据事务启动时刻的"快照"读取对应版本的数据,而非直接读取最新数据。
二、MVCC 的底层实现
1. 数据行的隐藏字段
InnoDB 每行数据包含三个隐藏字段:
字段名 | 描述 |
---|---|
DB_TRX_ID |
最近修改该行的事务ID(插入或更新时更新) |
DB_ROLL_PTR |
回滚指针,指向 Undo Log 中旧版本数据的指针(构成版本链) |
DB_ROW_ID |
隐式自增行ID(无主键时自动生成) |
2. Undo Log 与版本链
-
Undo Log:记录数据修改前的旧值,用于回滚和构建历史版本。
-
版本链 :通过
DB_ROLL_PTR
将同一行数据的多个版本连接成链表,结构如下:plaintext最新数据行 → Undo Log V3 → Undo Log V2 → Undo Log V1
每个 Undo Log 包含:旧数据值 + 生成该版本的事务ID(
DB_TRX_ID
)。
3. ReadView(读视图)
- 作用:决定事务能看到哪些版本的数据。
- 关键属性 :
creator_trx_id
:创建该 ReadView 的事务ID。m_ids
:生成 ReadView 时活跃的事务ID集合。min_trx_id
:m_ids
中的最小事务ID。max_trx_id
:生成 ReadView 时系统将分配的下一个事务ID。
三、MVCC 的可见性规则
判断数据版本对当前事务是否可见的算法:
- 数据版本的
DB_TRX_ID < min_trx_id
→ 可见(版本在 ReadView 生成前已提交)。 - 数据版本的
DB_TRX_ID > max_trx_id
→ 不可见(版本在 ReadView 生成后创建)。 - 数据版本的
DB_TRX_ID ∈ [min_trx_id, max_trx_id)
:- 若
DB_TRX_ID ∉ m_ids
→ 可见(事务已提交)。 - 若
DB_TRX_ID ∈ m_ids
→ 不可见(事务未提交)。
- 若
- 若数据版本由当前事务自身修改 → 可见。
遍历版本链:从最新版本开始,依次判断直到找到第一个可见的版本。
四、MVCC 在事务隔离级别中的行为
1. READ COMMITTED(读已提交)
- ReadView 生成时机:每次执行 SELECT 时生成新 ReadView。
- 效果 :
能读取到其他事务已提交的最新数据,存在不可重复读和幻读问题。
2. REPEATABLE READ(可重复读)
- ReadView 生成时机:第一次执行 SELECT 时生成,后续复用同一 ReadView。
- 效果 :
整个事务中看到的数据版本一致,解决了不可重复读,但幻读需配合间隙锁。
五、MVCC 的读写操作流程
1. SELECT 操作(快照读)
plaintext
1. 获取事务的 ReadView
2. 遍历数据行的版本链
3. 返回第一个符合可见性规则的版本数据
2. UPDATE/DELETE 操作(当前读)
plaintext
1. 加行锁(X锁)
2. 读取最新数据版本(需处理其他事务的锁冲突)
3. 生成新版本数据并插入版本链
4. 更新 DB_TRX_ID 和 DB_ROLL_PTR
3. INSERT 操作
plaintext
1. 直接插入新行,DB_TRX_ID = 当前事务ID
2. DB_ROLL_PTR 指向空(无历史版本)
六、MVCC 的优缺点
优点:
- 高并发:读写操作不互相阻塞。
- 避免锁竞争:减少死锁概率。
- 快速回滚:利用 Undo Log 实现事务原子性。
缺点:
- 存储开销:需维护多版本数据和 Undo Log。
- 历史版本清理:长事务可能导致 Undo Log 无法及时清理(Purge 线程压力)。
- 写冲突检测延迟:需通过锁机制补充(如间隙锁)。
七、实战案例:事务并发场景
场景描述:
- 事务A(TrxID=100):
SELECT * FROM users WHERE id=1;
- 事务B(TrxID=101):
UPDATE users SET name='Bob' WHERE id=1; COMMIT;
- 事务A再次执行相同 SELECT。
执行过程(隔离级别=REPEATABLE READ):
-
事务A首次 SELECT:
- 生成 ReadView:
creator_trx_id=100
,m_ids=[100,101]
,min=100
,max=102
。 - 若数据行
DB_TRX_ID=99
(已提交)→ 可见。
- 生成 ReadView:
-
事务B提交 UPDATE:
- 数据行更新为
DB_TRX_ID=101
,生成新版本。
- 数据行更新为
-
事务A再次 SELECT:
- 复用原 ReadView。
- 最新版本
DB_TRX_ID=101
(属于m_ids
且未提交 → 不可见)。 - 继续查找旧版本
DB_TRX_ID=99
→ 可见。
结果:事务A两次读取结果一致(可重复读)。
八、总结
MVCC 是 MySQL 高并发能力的基石,通过 版本链 + ReadView + Undo Log 的组合实现非锁定读。理解其机制可帮助我们:
- 合理设计事务边界,避免长事务导致版本堆积。
- 优化查询,利用覆盖索引减少回表。