MVCC机制简明指南
MVCC(Multi-Version Concurrency Control,多版本并发控制)是数据库实现高并发的核心技术,核心思想是通过保存数据的多个版本,让读写操作互不阻塞。以下是其核心原理和实现逻辑:
一、MVCC解决什么问题?
1. 传统锁机制的痛点
- 读-写冲突:读数据时加锁,会阻塞写操作(反之亦然)。
- 并发度低:悲观锁(如
SELECT FOR UPDATE
)导致大量线程等待。
2. MVCC的解决方案
- 读操作:读取数据的历史快照(无需加锁)。
- 写操作:创建新版本数据,不影响正在读取旧版本的事务。
二、MVCC的实现原理(以InnoDB为例)
1. 隐藏字段
每行记录包含两个隐藏字段:
-
DB_TRX_ID
:最后一次修改该行的事务ID。 -
DB_ROLL_PTR
:指向Undo Log中旧版本数据的指针(构成版本链)。
id | name | age | DB_TRX_ID |
DB_ROLL_PTR |
---|---|---|---|---|
1 | Alice | 25 | 101 | 0x123456 |
2. Undo Log(回滚日志)
- 存储数据被修改前的值,用于:
- 事务回滚:恢复到修改前的状态。
- 构建版本链:支持MVCC读取历史版本。
3. ReadView(读视图)
事务每在读取数据时生成一个快照,决定能看到哪些版本的数据:
-
m_ids
:当前活跃(未提交)的事务ID列表,是一个集合。 -
min_trx_id
:所有正在并发的活跃事务中最小ID。 -
max_trx_id
:系统预分配的下一个事务ID。 -
creator_trx_id
:创建该ReadView的事务ID。
三、MVCC的工作流程
1. 读操作(SELECT)
-
m_ids
:当前活跃(未提交)的事务ID列表,是一个集合。 -
min_trx_id
:所有正在并发的活跃事务(m_ids)
中最小ID。 -
max_trx_id
:系统预分配的下一个事务ID。 -
creator_trx_id
:创建该ReadView的事务ID(查询事务)。 -
DB_TRX_ID
:最后一次修改该行的事务ID。最新修改的事务ID。
判断redo log版本链中,到底那一条数据可以被当前事务看见:
- 检查行数据的
DB_TRX_ID
:- 如果
DB_TRX_ID < min_trx_id
:说明该版本已提交,可见。 - 如果
DB_TRX_ID > max_trx_id
:说明该版本是未来事务修改的,不可见。 - 如果
min_trx_id ≤ DB_TRX_ID ≤ max_trx_id
:- 若
DB_TRX_ID
在m_ids
中:说明事务未提交,不可见。 - 否则:可见。
- 若
- 如果
- 若不可见,则通过
DB_ROLL_PTR
找到Undo Log中的旧版本,重复判断。
2. 写操作(UPDATE/INSERT/DELETE)
- UPDATE:拷贝当前行到Undo Log,修改当前行并更新
DB_TRX_ID
。 - DELETE:标记删除(逻辑删除),通过
DB_ROLL_PTR
保留旧版本。 - INSERT:直接插入新行,分配新
DB_TRX_ID
。
四、MVCC如何保证隔离级别?
隔离级别 | MVCC的实现方式 |
---|---|
读未提交(RU) | 直接读最新数据,忽略版本链 |
读已提交(RC) | 每次SELECT生成新ReadView,能看到其他事务已提交的修改 |
可重复读(RR) | 事务内第一次SELECT生成ReadView,后续复用该视图(MySQL默认,避免不可重复读和幻读) |
串行化(S) | 退化为悲观锁(如加表锁) |
五、MVCC的优缺点
优点
- 读不阻塞写,写不阻塞读:大幅提升并发性能。
- 避免脏读和不可重复读:通过版本链和ReadView机制。
缺点
- 存储开销:需额外保存历史版本(Undo Log占用空间)。
- 清理成本:需要定期清理不再需要的旧版本(Purge线程)。
六、示例说明
假设事务A(ID=100)和事务B(ID=101)并发操作:
- 初始数据:行X的
DB_TRX_ID=90
(已提交)。 - 事务B修改行X:
- 将旧值存入Undo Log,更新行X的
DB_TRX_ID=101
。
- 将旧值存入Undo Log,更新行X的
- 事务A读取行X:
- 生成ReadView:
m_ids=[101]
(事务B未提交),min_trx_id=101
。 - 发现行X的
DB_TRX_ID=101
在m_ids
中,不可见 → 通过DB_ROLL_PTR
读取Undo Log中的旧版本(DB_TRX_ID=90
),返回该值。
- 生成ReadView:
总结
MVCC通过版本链+读视图的机制,在保证事务隔离性的同时,最大化并发性能。它是MySQL高并发能力的基石,理解其原理对优化SQL和排查并发问题至关重要。
通俗易懂版解释:MVCC的可见性规则
你可以把MVCC的可见性判断想象成一个"时间线游戏",通过比较事务ID的大小关系,决定当前事务能看到哪个版本的数据。下面用最直白的语言和例子解释:
1. 关键角色说明
-
DB_TRX_ID
:每行数据上贴的"最后修改者身份证号"(事务ID)。 -
min_trx_id
:当前系统中"最老未提交事务的身份证号"。 -
max_trx_id
:系统即将分配的下一个事务ID(未来事务的起点号)。 -
m_ids
:当前所有"未提交事务的身份证号列表"。
2. 判断规则拆解
情况1:DB_TRX_ID < min_trx_id
(已提交的旧数据)
- 比喻:你(当前事务)在查资料时,发现这份资料的修改者(
DB_TRX_ID=50
)比教室里最老的学生(min_trx_id=100
)还早,说明他早就交卷离开(已提交)。 - 结论:这份资料是可见的。
情况2:DB_TRX_ID > max_trx_id
(未来的数据)
- 比喻:资料上写着修改者ID是
200
,但系统现在最大只分配到150
,这个ID属于"未来人"。 - 结论:这份资料是不可见的(可能是系统异常)。
情况3:min_trx_id ≤ DB_TRX_ID ≤ max_trx_id
(可能是未提交的数据)
子情况3.1:
DB_TRX_ID
在m_ids
列表中- 比喻:资料修改者(
DB_TRX_ID=120
)还在教室里考试(未提交事务列表中有他)。 - 结论:不可见(不能看他未提交的答案)。
- 比喻:资料修改者(
子情况3.2:
DB_TRX_ID
不在m_ids
列表中- 比喻:资料修改者ID是
130
,但教室里没这个人(说明他已交卷离开)。 - 结论:可见。
- 比喻:资料修改者ID是
如果不可见怎么办?
- 动作:顺着
DB_ROLL_PTR
指针(类似"上一版本链接")找到旧版本数据,重新判断。 - 比喻:当前资料不可见,就去翻看它的历史修订版,直到找到符合条件的版本。
3. 实际例子
假设当前系统状态:
- 活跃事务列表
m_ids = [100, 110]
(事务100和110未提交) min_trx_id = 100
,max_trx_id = 120
某行数据的版本链:
版本3: DB_TRX_ID=110 (通过ROLL_PTR指向版本2)
版本2: DB_TRX_ID=100 (通过ROLL_PTR指向版本1)
版本1: DB_TRX_ID=80
事务A(ID=105)读取该行时的判断过程:
- 先看最新版本(版本3):
DB_TRX_ID=110
,min_trx_id=100
→ 属于情况3110
在m_ids=[100,110]
中 → 不可见
- 通过
ROLL_PTR
找到版本2:DB_TRX_ID=100
,min_trx_id=100
→ 属于情况3100
在m_ids
中 → 不可见
- 继续找版本1:
DB_TRX_ID=80
,80 < min_trx_id=100
→ 属于情况1- 可见! 最终返回版本1的数据。
4. 为什么这样设计?
- 读已提交(RC):每次查询都重新生成
ReadView
,能看到其他事务最新提交的数据。 - 可重复读(RR):事务内第一次查询生成
ReadView
后固定不变,保证多次读取结果一致。
总结
MVCC的可见性规则本质是:
通过比较事务ID,判断数据版本是否由已提交的事务创建。
- 比"最老未提交事务"更早的 → 安全可见
- 属于"未提交事务"的 → 不可见
- 属于"已提交但不算太老"的 → 也可见
就像考试时,你只能参考已交卷同学的答案,不能看还在答题的同学的试卷!