MySQL MVCC实现可重复读:快照读机制详解
1. MVCC基本概念
多版本并发控制(MVCC)是MySQL InnoDB存储引擎实现事务隔离级别的核心机制。它的基本思想是:同一数据在不同时刻可能有多个版本,每个事务只能看到特定版本的数据。
2. 行记录的隐藏字段
InnoDB中,每行记录都包含以下隐藏字段:
DB_TRX_ID:修改该行的最后一个事务ID
DB_ROLL_PTR:回滚指针,指向该行的上一个版本
DB_ROW_ID:隐藏主键(仅在表没有定义主键时使用)
行记录结构:
+-------------+-------------+-------------+-------------+
| 用户字段 | 用户字段 | DB_TRX_ID | DB_ROLL_PTR |
+-------------+-------------+-------------+-------------+
3. Undo Log与版本链
当事务修改数据时,原数据会被保存到Undo Log中,形成版本链:
版本链结构:
最新版本 (DB_TRX_ID=30) --> 旧版本 (DB_TRX_ID=20) --> 最初版本 (DB_TRX_ID=10)
4. ReadView结构
ReadView是事务在某个时间点上对数据库的一致性快照,包含:
m_ids:活跃(未提交)事务ID列表
min_trx_id:最小活跃事务ID
max_trx_id:下一个将分配的事务ID
creator_trx_id:创建该ReadView的事务ID
ReadView结构:
+------------------+------------------+------------------+------------------+
| m_ids | min_trx_id | max_trx_id | creator_trx_id |
| [事务ID列表] | 最小活跃事务ID | 下一个事务ID | 创建者事务ID |
+------------------+------------------+------------------+------------------+
5. 可见性判断规则
当事务读取一行记录时,通过以下规则判断该版本是否可见:
如果 DB_TRX_ID == creator_trx_id:当前事务自己修改的,可见
如果 DB_TRX_ID < min_trx_id:已提交的老事务修改的,可见
如果 DB_TRX_ID >= max_trx_id:在ReadView创建后才开始的事务修改的,不可见
如果 min_trx_id <= DB_TRX_ID < max_trx_id:
如果 DB_TRX_ID 在 m_ids 中:活跃事务修改的,不可见
如果 DB_TRX_ID 不在 m_ids 中:已提交事务修改的,可见
如果不可见,沿版本链找下一个版本,重复以上判断
可见性判断流程:
+-------------------------+
| 读取记录的DB_TRX_ID |
+-------------------------+
|
v
+-------------------------------+
| DB_TRX_ID == creator_trx_id? |----是---> 可见
+-------------------------------+
|
否
v
+-------------------------------+
| DB_TRX_ID < min_trx_id? |----是---> 可见
+-------------------------------+
|
否
v
+-------------------------------+
| DB_TRX_ID >= max_trx_id? |----是---> 不可见
+-------------------------------+
|
否
v
+-------------------------------+
| DB_TRX_ID 在 m_ids 中? |----是---> 不可见
+-------------------------------+
|
否
v
可见
6. 不同隔离级别的ReadView创建时机
MVCC在不同隔离级别下的关键区别:
READ COMMITTED(读已提交):每次读取都创建新ReadView
REPEATABLE READ(可重复读):事务第一次读取时创建ReadView,后续复用
READ COMMITTED vs REPEATABLE READ:
READ COMMITTED:
查询1 --> 创建ReadView1 --> 返回结果
查询2 --> 创建ReadView2 --> 返回结果
查询3 --> 创建ReadView3 --> 返回结果
REPEATABLE READ:
查询1 --> 创建ReadView --> 返回结果
查询2 --> 复用ReadView --> 返回结果
查询3 --> 复用ReadView --> 返回结果
7. 快照读与当前读
快照读:普通SELECT,读取历史版本,通过MVCC实现
当前读:SELECT FOR UPDATE/SHARE、INSERT、UPDATE、DELETE,读取最新版本,需加锁
8. 可重复读实现原理
在REPEATABLE READ隔离级别下,事务在第一次读取时创建ReadView,并在整个事务期间复用该ReadView。这确保了:
事务开始前已提交的数据对该事务可见
事务自己修改的数据对自己可见
事务开始后其他事务提交的修改对该事务不可见
这就实现了可重复读:无论其他事务如何修改并提交数据,在同一事务内多次读取得到的结果都是一致的。
9. 简单示例
假设有表:users(id, name),初始数据(1, "Alice")
时间轴:
T1: 事务A和事务B开始
T2: 事务C开始,将name改为"Bob"并提交
T3: 事务A第一次查询(创建ReadView)
T4: 事务D开始,将name改为"Charlie"并提交
T5: 事务A第二次查询(复用ReadView)
版本链:
最新版本 (DB_TRX_ID=D, name="Charlie") -->
中间版本 (DB_TRX_ID=C, name="Bob") -->
初始版本 (DB_TRX_ID=初始, name="Alice")
事务A在T3时刻的ReadView:
m_ids=[A,B]
min_trx_id=min(A,B)
max_trx_id=下一个ID(>C)
creator_trx_id=A
事务A的第一次查询:
检查最新版本:DB_TRX_ID=C
C < max_trx_id 且 C 不在 m_ids 中(已提交)
返回name="Bob"
事务A的第二次查询:
检查最新版本:DB_TRX_ID=D
D >= max_trx_id(ReadView创建后的事务)
不可见,检查下一版本:DB_TRX_ID=C
C < max_trx_id 且 C 不在 m_ids 中(已提交)
返回name="Bob"
尽管期间有事务D修改了数据,但事务A的两次查询结果一致,实现了可重复读。
10. 总结
MySQL InnoDB通过MVCC实现可重复读的关键点:
利用隐藏字段记录数据版本信息
通过Undo Log构建版本链
在事务首次读取时创建ReadView并复用
根据可见性规则确保事务只能看到特定版本的数据
这种机制在保证数据一致性的同时,最大限度地提高了并发性能,是关系型数据库实现事务隔离的重要技术。