MySql MVCC 多版本控制
1. 背景
MVCC(Multi-Version Concurrency Control,多版本并发控制)主要用于 解决高并发下的读写冲突,让:
-
读操作不用阻塞写操作(读一致性)
-
写操作不用阻塞读操作(写快照)
在 MySQL 里,
InnoDB 依赖 隐藏字段 + Undo Log + ReadView 来实现:
-
每行记录有两个隐藏字段:
-
trx_id:最近一次修改该行的事务 ID -
roll_pointer:指向 Undo Log 的指针(能找到历史版本)
-
-
事务启动时 会生成一个 ReadView,记录当前活跃事务的 ID 集合。
-
查询数据时:
-
如果某行数据的
trx_id在活跃事务列表中,就沿着roll_pointer找历史版本。 -
如果不在活跃事务中,就说明对当前事务可见。
-
这样就能做到:读不阻塞写,写不阻塞读。
优点:
-
有 MVCC(InnoDB 默认):
-
事务内部看到的都是事务开始时的快照。
-
并发读写不阻塞。
-
解决了“不可重复读”和“读写冲突”问题。
-
-
没有 MVCC:
-
多次读取可能得到不同结果(不可重复读)。
-
并发写会阻塞读,高并发性能差。
-
事务逻辑可能出错。
-
2. 具体实现机制
🔹 表中的隐藏列
InnoDB 每行记录都会自动增加两个隐藏列(假设事务 ID 为 trx_id):
-
DB_TRX_ID:最后一次修改该行的事务 ID
-
DB_ROLL_PTR:指向 Undo Log 的指针(可以找到历史版本)
(还有个 DB_ROW_ID,用于没有主键时的聚簇索引,这里略)
🔹 Undo Log
Undo Log 保存了 数据被修改前的旧值。
-
插入 → 需要回滚时删除
-
更新 → 保存旧值(指向前一个版本)
-
删除 → 保存整行数据(回滚时能插回去)
这样,InnoDB 就能通过 Undo Log 找到某一行的历史版本。
🔹 ReadView(快照读)
一致性视图(ReadView) 是 MVCC 的核心,它决定了一个事务在“可重复读”或“读已提交”时,能看到哪些版本。
ReadView 包含四个关键信息:
-
m_ids:当前活跃事务的 ID 集合
-
min_trx_id:m_ids 中的最小事务 ID
-
max_trx_id:下一个将被分配的事务 ID
-
creator_trx_id:创建该 ReadView 的事务 ID
判断某一行版本是否可见的规则:
-
如果
trx_id < min_trx_id→ 已提交,可见 -
如果
trx_id >= max_trx_id→ 未开始,不可见 -
如果
trx_id在m_ids内 → 还没提交,不可见 -
其他情况 → 可见
3. MVCC 的具体实现过程
🔸 1)查询(快照读)
-
InnoDB 会生成一个 ReadView(快照视图)
-
通过 隐藏列 + Undo Log 找到符合快照视图的版本
-
返回的是历史版本,而不是最新的物理数据
👉 快照读不会加锁,所以效率很高。
🔸 2)更新(当前读)
-
先加锁(保证写一致性)
-
将旧值写入 Undo Log(形成历史版本)
-
更新记录中的 trx_id 为当前事务 ID
-
指向新的 Undo Log
👉 这就产生了多版本链条。
🔸 3)删除
-
并不是直接删除,而是打上删除标记 + 生成 Undo Log
-
其他事务在快照读时,可能还能看到老版本
4. 不同隔离级别下的表现
-
读已提交(RC):每次查询都会生成新的 ReadView → 每次都能看到最新提交的数据
-
可重复读(RR,默认):事务中第一次查询时生成 ReadView,整个事务期间都用这个视图 → 保证可重复读
-
串行化:MVCC 不起作用,所有读都加锁
5. 小结
InnoDB MVCC 实现方案 = 隐藏列(DB_TRX_ID + DB_ROLL_PTR) + Undo Log + ReadView
-
隐藏列:保存版本信息
-
Undo Log:保存旧值,形成版本链
-
ReadView:决定某个事务能看到哪个版本
这样就实现了:
-
读写并行 → 读走快照,写走最新
-
高并发下的一致性和性能兼顾
一、MVCC 生效条件
-
存储引擎必须是 InnoDB
MyISAM 没有 MVCC。
-
确认 Engine = InnoDB。
-
隔离级别必须是 REPEATABLE READ 或 READ COMMITTED
-
SERIALIZABLE 会强制加锁,不走 MVCC。
-
READ UNCOMMITTED 会直接读最新版本,也不依赖 MVCC。
如果要修改:
-
-
查询必须是快照读(Snapshot Read)
-
普通
SELECT会使用 MVCC。 -
但以下情况会强制加锁,不走 MVCC:
-
SELECT ... FOR UPDATE -
SELECT ... LOCK IN SHARE MODE -
UPDATE/DELETE
-
-
三、验证 MVCC 是否在用
你可以做一个小实验 👇
✅ 总结:
-
InnoDB 默认就启用 MVCC,你不需要额外配置。
-
只要 Engine=InnoDB + 隔离级别是 RR/RC + 普通 SELECT,就是在用 MVCC。
-
特殊的加锁读、更新、删除不走 MVCC。
1️⃣ 举例
-
会话 A 开启事务,读取 id=1 的值 →
100。 -
会话 B 开启事务,更新 id=1 的值 →
200并提交。 -
会话 A 在事务未提交前再次查询 → 依然是
100(这是 MVCC 的效果)。 -
会话 A 提交事务后再查询 → 才看到
200。
2️⃣ 如果没有 MVCC
如果数据库不支持 MVCC,比如使用传统的 行锁 + 阻塞读 模型(类似 MyISAM 或某些非事务存储引擎):
-
会话 A 开启事务,读取 id=1 的值 →
100。 -
会话 B 尝试更新 id=1:
-
如果会话 A 持有行锁,会话 B 会阻塞,直到会话 A 提交。
-
如果没有行锁,会话 B 会直接修改 → 会话 A 再次查询时可能看到
200,这就是不可重复读。
-
换句话说:
| 事务隔离 | 会话 A 再次读的结果 |
|---|---|
| 无 MVCC / 低隔离 | 可能是 200(被会话 B 更新) |
| MVCC / REPEATABLE READ | 100(事务快照,不受其他事务影响) |
3️⃣ 实际并发场景问题
如果没有 MVCC:
-
不可重复读:
-
事务中多次读取同一行,得到不同值。
-
比如用户余额计算时,第一次查询余额 100,第二次查询已经被别人更新为 200 → 会导致计算错误。
-
-
读阻塞写或写阻塞读:
-
传统行锁机制,读操作可能阻塞写,写操作可能阻塞读。
-
高并发下,性能会严重下降。
-
-
事务逻辑出错:
-
依赖同一事务内数据一致性的业务(库存扣减、订单处理、财务计算)会受到影响。
-
✅ 总结
-
有 MVCC(InnoDB 默认):
-
事务内部看到的都是事务开始时的快照。
-
并发读写不阻塞。
-
解决了“不可重复读”和“读写冲突”问题。
-
-
没有 MVCC:
-
多次读取可能得到不同结果(不可重复读)。
-
并发写会阻塞读,高并发性能差。
-
事务逻辑可能出错。
-
浙公网安备 33010602011771号