MySQL MVCC 多版本并发控制实战:场景解析与幻读规避
在MySQL的RR(Repeatable Read)隔离级别下,MVCC虽能解决大部分读写冲突,但“幻读”现象仍存在特定触发条件。本文通过模拟四种并发事务场景,对比快照读与当前读的行为差异,阐明Read View创建时机、索引对锁机制的影响,以及如何利用当前读锁定行范围来彻底规避幻读。
环境与准备
实验基于 MySQL 8.0.27 版本,事务隔离级别默认设置为 REPEATABLE-READ。测试表 test 包含主键 id 及联合索引 idx_name_phone,初始数据如下:
mysql> show create table test;
- 字段:id (bigint), name (char), num (int), phone (char)
- 索引:
PRIMARY KEY (id),KEY idx_name_phone (name,phone)
预置数据包括 id=7 (wdf) 和 id=15 (df) 等记录,用于后续事务干扰观察。
场景一:FOR UPDATE 下的当前读行为
此场景旨在验证持有排他锁的事务是否会刷新视图,从而读到其他事务已提交的最新数据。
第1步:事务A 获取排他锁
执行带 for update 的查询,MySQL 将发起当前读并申请行级排他锁。
mysql> begin; SELECT * FROM test WHERE id IN (7,15) FOR UPDATE;
此时检查性能库锁状态:
SELECT * FROM performance_schema.data_locks; -- 结果可见 ENGINE 'INNODB' 持有 RECORD X (排他) 锁于 id=7 和 id=15
第2步:事务B 提交修改与新增
另一事务立即执行更新与插入并提交:
UPDATE test SET name = 'sds' WHERE id = 10; INSERT INTO test(id, name) VALUES(8, 'hello');
第3步:事务A 再次查询验证
由于 FOR UPDATE 触发了当前读(Current Read),事务A在此刻重新构建了可见性判断。
SELECT * FROM test WHERE id IN (7,8,10,15); -- 输出显示包含了 id=8 (新建) 和 id=10 (已更新 sds),证明了当前读能看到最新提交值。
场景二:普通快照读的不可见性与后续影响
若不加锁直接查询,根据 MVCC 规则,同一事务内的多次简单 SELECT 应返回相同结果。
第1步:事务A 发起普通快照读
BEGIN; SELECT * FROM test WHERE id IN (7,15); -- 仅查看现有行
第2步:事务B 提交变更
UPDATE test SET name = 'dd' WHERE id = 10; INSERT INTO test(id, name) VALUES(8, 'hello');
第3步:事务A 再次查询验证
第二次查询依然受限于初次生成的 Read View,无法感知到事务B新增的 id=8,也无法看到 id=10 的变化(如果原存在)。
SELECT * FROM test WHERE id IN (7,8,10,15); -- 输出仅含 id=7, 10(dd), 15。注意:因为ID 8是事务B新插入且不在事务A的初始视图中,故不显示。
关键推论:普通 SELECT 开启的是快照读,使用事务启动时或第一条语句时的 Read View。因此看不到后来的 INSERT/UPDATE。然而,如果在事务内随后执行了修改操作(即使是非目标行),会改变事务自身的版本链上下文,导致内部状态变化。
场景三:混合操作后的视图一致性
本场景探讨在执行完修改操作后,视图是否会被刷新。
第1步:事务A 先进行普通查询
BEGIN; SELECT * FROM test WHERE id IN (7,15);
第2步:事务B 提交变更
UPDATE test SET name = 'd' WHERE id = 10; INSERT INTO test(id, name) VALUES(8, 'hello');
第3步:事务A 尝试当前读确认
-- 强制走当前读路径 SELECT * FROM test WHERE id IN (7,15) FOR UPDATE; -- 接着查完整范围 SELECT * FROM test WHERE id IN (7,8,10,15); -- 发现依然未读到 id=8
这表明简单的加锁操作如果不配合具体的修改动作或未跨越事务边界,其 Read View 仍可能指向较早的时间点。但对于真正的“写”操作而言,逻辑会有所不同。
场景四:同一事务内的版本链修改与幻读风险
这是最容易引发理解偏差的场景:为什么同一个事务中,第一次查不到某条数据,稍作修改后又能查到?
第1步:事务A 快照查询
BEGIN; SELECT * FROM test WHERE id IN (7,15); -- 此时未见到 id=8
第2步:事务B 独立插入
INSERT INTO test(id, name) VALUES(8, 'hello');
第3步:事务A 再次快照查询
SELECT * FROM test WHERE id IN (7,8,15); -- 结果仍然不包含 id=8 (符合 RR 预期)
第4步:事务A 对本事务内“不存在”的行执行更新
虽然 SELECT 没查到,但如果盲目 UPDATE (例如通过模糊条件匹配到了该行):
UPDATE test SET name = 'cv' WHERE id = 8;
注意:在实际生产中,若不指定确切的主键或唯一索引值,直接 UPDATE 可能会命中意外数据,此处假设精准命中。一旦涉及写操作,InnoDB 会使用当前读定位到该物理记录并进行修改。
第5步:事务A 最终查询
SELECT * FROM test WHERE id IN (7,8,15); -- 结果显示 id=8 (name=cv),能够看到了!
原理解析:当事务 A 执行 UPDATE 时,它实际上执行了一次当前读以锁定并修改记录。这次操作使得该记录的版本被归入当前事务的活动日志中。随后的 SELECT 在读取该行时,发现它是当前事务自己刚改过的,依据 MVCC “自身事务修改立即可见” 的规则,便将其展示出来。这就是典型的伪幻读或广义上的幻读成因之一。
结论与避坑指南
- Read View 生成节点:在 RR 隔离级别下,Read View 通常在事务的第一次普通
SELECT时建立。此后所有无锁的SELECT均复用此视图,表现为“快照读”。这意味着只要不改数据,就看不到外部新插入的行。 - 当前读打破幻觉:
SELECT ... FOR UPDATE或LOCK IN SHARE MODE以及所有的DML(INSERT/UPDATE/DELETE) 均属于“当前读”,它们无视 Read View,直接访问最新的已提交(或自己未提交)数据。 - 如何彻底消除幻读:MVCC 只能保证普通读的一致性,不能防住 DML 带来的幻读风险。若要实现严格的线性一致性,必须使用
FOR UPDATE配合合适的索引(推荐Next-Key Lock)。通过锁定行及其间隙,防止其他事务插入新记录,从而从物理层面杜绝该范围内的幻读现象。
以上就是 MySQL MVCC 各场景理解的详细内容,更多关于 MySQL mvcc 场景的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









