脏读、不可重复读、幻读分别由事务B未提交更新、已提交更新同一行、已提交插入新行触发;MySQL默认RR级通过MVCC+临键锁防前两者,但幻读仅对快照读有效,当前读仍可能发生。

脏读、不可重复读、幻读到底怎么触发?
这三个问题不是理论概念,而是真实 SQL 执行顺序下必然出现的现象。关键在于:**事务 A 读取时,事务 B 正在做什么、是否已提交、操作的是同一行还是新插入的行**。
-
脏读:事务 A 执行
SELECT时,事务 B 刚UPDATE了一行但还没COMMIT;A 读到了这行“未定稿”,B 后续ROLLBACK→ A 的读取结果就失效了。 -
不可重复读:事务 A 第一次
SELECT id=100得到name='Alice';事务 B 提交了UPDATE users SET name='Bob' WHERE id=100;A 再次SELECT id=100,得到name='Bob'—— 同一行值变了。 -
幻读:事务 A 执行
SELECT * FROM orders WHERE status='pending'返回 3 条;事务 B 插入一条新status='pending'记录并COMMIT;A 再次执行相同查询,返回 4 条 —— 行数变多了,像“幻影”。注意:这不是更新同一行,而是满足条件的新行被插入。
MySQL 默认的 REPEATABLE READ 真的能防住所有问题吗?
不能。MySQL InnoDB 的默认隔离级别是 REPEATABLE READ,它靠 MVCC + Next-Key Lock(临键锁)实现,能防脏读和不可重复读,但对幻读只做“部分防护”——仅针对普通 SELECT(快照读)有效;一旦用了当前读(如 SELECT ... FOR UPDATE、UPDATE、DELETE),幻读仍可能发生,尤其在范围条件上。
- 例如:事务 A 执行
SELECT * FROM t WHERE c > 10 FOR UPDATE锁住满足条件的记录和间隙;事务 B 尝试插入c=15会被阻塞 → 这是幻读被锁挡住。 - 但如果事务 B 插入的是
c=5(不在 A 的查询范围内),或 A 没加锁直接SELECT(快照读),那 B 的插入就能成功,A 再查就会“看到幻影”。 - 真正彻底解决幻读,只有
SERIALIZABLE隔离级别,但它会让所有并发SELECT变成串行,线上基本不用。
写-写冲突:为什么两个 UPDATE 会互相卡住?
当两个事务同时想改同一行,InnoDB 必须用排他锁(X 锁)互斥。谁先拿到锁谁先改,后到的只能等 —— 这就是锁等待。如果等待超时(默认 innodb_lock_wait_timeout = 50 秒),会报错:Lock wait timeout exceeded; try restarting transaction。
- 典型场景:秒杀扣库存,多个请求同时执行
UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock > 0。 - 陷阱:即使 SQL 带了
AND stock > 0条件,InnoDB 仍会对匹配的索引记录(甚至间隙)加 X 锁,后续请求必须排队。 - 优化方向不是“去掉锁”,而是缩短锁持有时间:确保该语句走索引、避免大事务、减少其他无关 SQL 在同一事务中。
读-写并发下,MVCC 是怎么悄悄帮你躲开锁的?
MVCC 不是魔法,它是通过给每行数据维护多个版本(由 DB_TRX_ID 标记),配合事务启动时生成的 ReadView,让普通 SELECT 读取“快照”,而不是最新行 —— 所以读不加锁,也不阻塞写。
- 关键点:
REPEATABLE READ下,事务第一次SELECT生成ReadView,之后所有快照读都复用它;READ COMMITTED则每次SELECT都新建ReadView,所以能看到其他事务已提交的修改(即允许不可重复读)。 - 注意:
SELECT ... LOCK IN SHARE MODE或FOR UPDATE是当前读,绕过 MVCC,直接加 S/X 锁,会阻塞其他写操作。 - 一个易忽略的事实:MVCC 只解决读-写冲突,对写-写冲突完全不管 —— 两个
UPDATE依然要抢锁,哪怕它们读的是不同快照。
最常被低估的一点:并发问题从来不是孤立存在的。比如你调高了隔离级别防幻读,却没意识到它会让更多语句升级为当前读,从而增加锁竞争;又比如你依赖 MVCC 实现无锁读,却在事务里混进了 SELECT ... FOR UPDATE,瞬间打破快照一致性。真正的并发控制,是隔离级别、锁策略、SQL 写法、应用重试逻辑四者咬合的结果,缺一不可。










