MVCC (Multi-Version Concurrency Control) 是 MySQL 实现高并发访问的核心机制,它通过在数据行上维护多个版本,使读写操作可以并发执行而不互相阻塞。
一、MVCC 的作用与价值
主要作用
- 读写不阻塞:读操作不会阻塞写操作,写操作也不会阻塞读操作
- 提高并发:不同事务可以看到数据的不同版本
- 避免脏读:确保事务只能看到已提交的数据
- 实现隔离级别:支持 READ COMMITTED 和 REPEATABLE READ 隔离级别
实际价值
- 读多写少场景下性能大幅提升
- 避免大量锁等待
- 实现非锁定一致性读
二、MVCC 核心原理
InnoDB 通过三个隐藏字段和 Undo Log 实现 MVCC:
-
隐藏字段:
DB_TRX_ID
:6字节,记录最近修改该行的事务IDDB_ROLL_PTR
:7字节,指向该行回滚段的指针(Undo Log)DB_ROW_ID
:6字节,隐藏的行ID(如果没有主键)
-
Undo Log:
- 存储数据被修改前的值
- 形成版本链,用于事务回滚和其他事务读取
-
ReadView:
- 事务执行快照读时产生的读视图
- 包含:
m_ids
:当前活跃事务ID列表min_trx_id
:最小活跃事务IDmax_trx_id
:预分配的下一个事务IDcreator_trx_id
:创建该ReadView的事务ID
三、MVCC 工作流程案例
案例背景
有一张账户表:
CREATE TABLE accounts (
id INT PRIMARY KEY,
name VARCHAR(100),
balance DECIMAL(10,2)
);
INSERT INTO accounts VALUES (1, 'Alice', 1000.00);
事务执行时序
时间 | 事务1 (TRX_ID=101) | 事务2 (TRX_ID=102) | 事务3 (TRX_ID=103) |
---|---|---|---|
T1 | BEGIN; | ||
T2 | UPDATE accounts SET balance=900 WHERE id=1; | ||
T3 | BEGIN; | ||
T4 | SELECT * FROM accounts WHERE id=1; (看到balance=1000) | ||
T5 | COMMIT; | ||
T6 | SELECT * FROM accounts WHERE id=1; (仍看到balance=1000) | ||
T7 | COMMIT; | ||
T8 | BEGIN; | ||
T9 | SELECT * FROM accounts WHERE id=1; (看到balance=900) |
版本链形成过程
- 初始数据:
id=1, name='Alice', balance=1000.00, DB_TRX_ID=0, DB_ROLL_PTR=null
- 事务1 (TRX_ID=101) 更新:
- 在Undo Log中记录修改前的值:
Undo Log Record: balance=1000.00, DB_TRX_ID=0
-
更新数据行:
id=1, name='Alice', balance=900.00, DB_TRX_ID=101, DB_ROLL_PTR -> Undo Log
- 在Undo Log中记录修改前的值:
-
事务2 (TRX_ID=102) 第一次查询:
- 创建ReadView:
- m_ids = [101, 102]
- min_trx_id = 101
- max_trx_id = 103
- creator_trx_id = 102
- 检查当前行:
- 行DB_TRX_ID=101 ∈ m_ids 且 ≠ creator_trx_id
- 通过DB_ROLL_PTR找到Undo Log中的版本
- 返回balance=1000.00
- 创建ReadView:
-
事务1提交后,事务2第二次查询:
- 仍使用之前的ReadView(REPEATABLE READ特性)
- 即使事务1已提交,仍返回Undo Log中的旧版本
-
事务3 (TRX_ID=103) 查询:
- 创建新的ReadView:
- m_ids = (事务1已提交,事务2已提交)
- min_trx_id = 103
- max_trx_id = 104
- creator_trx_id = 103
- 检查当前行:
- 行DB_TRX_ID=101 < min_trx_id(已提交)
- 直接返回当前行balance=900.00
- 创建新的ReadView:
四、不同隔离级别下的MVCC
1. READ COMMITTED (提交读)
- 每次SELECT都生成新的ReadView
- 能看到其他事务已提交的修改
- 案例中事务2在T6会看到balance=900
2. REPEATABLE READ (可重复读)
- 第一次SELECT时生成ReadView,后续复用
- 整个事务看到相同的数据快照
- 案例中事务2在T6仍看到balance=1000
五、MVCC与锁的协同
MVCC解决读-写冲突,锁解决写-写冲突:
-- 事务1
BEGIN;
SELECT * FROM accounts WHERE id=1 FOR UPDATE; -- 加排他锁
UPDATE accounts SET balance=900 WHERE id=1;
COMMIT;
-- 事务2
BEGIN;
SELECT * FROM accounts WHERE id=1; -- MVCC读,不受锁影响
SELECT * FROM accounts WHERE id=1 FOR UPDATE; -- 会被阻塞,直到事务1提交
六、MVCC的局限性
- 额外存储:需要维护版本链,占用更多空间
- 清理机制:需要定期purge过期版本
- 写冲突:无法解决两个事务同时更新同一行的冲突
- 长事务问题:长事务会导致版本链过长
七、MVCC优化建议
- 控制事务大小:避免长事务
- 合理设计索引:减少回表查找
- 监控版本链:
SHOW ENGINE INNODB STATUS\G -- 查看 HISTORY LIST LENGTH
- 定期维护:在低峰期执行OPTIMIZE TABLE