PDF大白话说Java面试题 — 03-Mysql篇第15题MySQL 的事务中幻读是怎么解决的回答核心考点大厂面试要求深入理解MVCC与Next-Key Lock的区别、快照读 vs 当前读的不同处理方式、Next-Key Lock的加锁范围以及RR级别下幻读是否100%解决。面试官常追问“RR级别下能完全避免幻读吗”、“Serializable隔离级别怎么解决幻读”1. 幻读的定义幻读是指在同一事务中相同的查询条件在不同时间点执行返回的结果集行数不一致。典型场景-- 事务 ASELECT*FROMusersWHEREage20;-- 返回 10 条-- 事务 B 插入了一条 age20 的新记录INSERTINTOusers(age)VALUES(20);-- 事务 A 再次查询SELECT*FROMusersWHEREage20;-- 返回 11 条幻读幻读 vs 不可重复读问题定义示例不可重复读同一行数据两次读取的内容不一致第一次读到name张三第二次变成name李四幻读两次查询返回的行数不一致第一次10行第二次11行新增注意MySQL的InnoDB在RR级别下MVCC已经解决了快照读的幻读但当前读仍需Next-Key Lock解决。2. 解决方案概览读类型解决方案原理快照读SELECTMVCC读取Undo日志中的历史版本不受新插入数据影响当前读SELECT FOR UPDATE/UPDATE/DELETENext-Key Lock锁定记录间隙防止其他事务插入为什么需要两种方案MVCC无锁并发性能好但只能用于快照读Next-Key Lock阻塞其他事务保证当前读数据一致性有锁代价3. MVCC如何解决快照读的幻读3.1 MVCC核心组件组件作用存储位置Undo Log版本链记录每次修改的历史版本通过roll_pointer串联Undo表空间ReadView判断事务可见性的快照事务执行快照读时生成trx_id最近修改该行的事务ID每行记录头信息roll_pointer指向上一个版本的指针每行记录头信息3.2 ReadView判断规则// ReadView核心字段m_ids// 当前未提交的事务ID列表活跃事务min_trx_id// m_ids中的最小值max_trx_id// 系统预分配的下一个事务ID即当前最大事务ID1creator_trx_id// 创建该ReadView的事务ID// 可见性判断规则伪代码boolisVisible(trx_id){if(trx_idcreator_trx_id)returntrue;// 当前事务修改if(trx_idmin_trx_id)returntrue;// 已提交if(trx_idmax_trx_id)returnfalse;// 未来事务if(trx_id isnotin m_ids)returntrue;// 已提交returnfalse;// 未提交}3.3 MVCC解决快照读幻读示例-- 事务 A开启时间早STARTTRANSACTION;SELECT*FROMusersWHEREage20;-- 生成ReadView_A返回10行-- 事务 B在事务A之后提交前STARTTRANSACTION;INSERTINTOusers(age)VALUES(20);COMMIT;-- 事务 A 再次快照读SELECT*FROMusersWHEREage20;-- 仍用ReadView_A新插入的行不可见仍返回10行关键事务A的第二次快照读仍使用第一次生成的ReadView因此看不到事务B插入的新行。4. Next-Key Lock解决当前读的幻读4.1 什么是Next-Key LockNext-Key Lock Record Lock记录锁Gap Lock间隙锁锁类型锁范围作用Record Lock锁定具体索引记录防止其他事务修改/删除该记录Gap Lock锁定索引记录之间的间隙防止其他事务在该间隙插入新记录Next-Key LockRecord Lock Gap Lock防止幻读新记录插入4.2 Gap Lock的锁定范围-- 假设users表有id索引1, 3, 5, 7, 9-- 查询条件WHERE id 5 且使用范围锁定Next-KeyLock锁定范围(3,5]和(5,7]-- 即锁定了3-7之间的所有间隙间隙锁定规则锁定的间隙是开区间不包含边界值除非是Record Lock间隙锁之间不互斥多个事务可以锁同一个间隙间隙锁只阻止插入不阻止读4.3 Next-Key Lock解决当前读幻读示例-- 事务 A当前读STARTTRANSACTION;SELECT*FROMusersWHEREage20FORUPDATE;-- 获取 Next-Key Lock锁定 age20 的记录及对应间隙-- 事务 B试图插入STARTTRANSACTION;INSERTINTOusers(age)VALUES(20);-- 阻塞因为事务 A 锁定了该间隙-- 事务 A 再次查询SELECT*FROMusersWHEREage20FORUPDATE;-- 返回行数不变无幻读5. Next-Key Lock在不同查询条件下的加锁范围查询条件锁类型加锁范围示例索引值1,3,5,7,9唯一索引等值查询命中Record Lock仅锁定该记录WHERE id5→ 锁5唯一索引等值查询未命中Gap Lock锁定命中间隙WHERE id6→ 锁(5,7)普通索引等值查询命中Next-Key Lock锁记录前间隙WHERE age5→ 锁(3,5]、(5,7)普通索引等值查询未命中Gap Lock锁定命中间隙WHERE age6→ 锁(5,7)范围查询Next-Key Lock锁范围内的所有间隙记录WHERE id 5→ 锁(5,∞)关键规则InnoDB加锁原则唯一索引等值命中→ Record Lock退化为行锁唯一索引等值未命中→ Gap Lock锁定间隙普通索引→ Next-Key Lock无法退化因为可能有重复值范围查询→ Next-Key Lock锁到范围结束无索引条件→ 全表锁所有间隙记录高并发风险示例分析-- 表结构CREATETABLEt(idINTPRIMARYKEY,ageINT,INDEXidx_age(age));-- 已有数据age: 1, 3, 5, 7, 9-- 查询1唯一索引命中SELECT*FROMtWHEREid5FORUPDATE;-- 锁id5的Record Lock不锁间隙-- 查询2普通索引命中SELECT*FROMtWHEREage5FORUPDATE;-- 锁Next-Key Lock覆盖(3,5]和(5,7)-- 不能插入age4,5,6-- 但可以插入age2,8-- 查询3范围查询SELECT*FROMtWHEREage5FORUPDATE;-- 锁所有age5的Next-Key Lock直到正无穷-- 不能插入age6,7,8,9,10...6. RR级别下幻读能100%解决吗结论InnoDB RR级别能100%防止幻读官方定义。官方保障快照读MVCC保证同一事务中的SELECT返回一致快照当前读Next-Key Lock阻止其他事务插入/删除符合条件的记录边界情况面试陷阱-- 事务 ASELECT*FROMusersWHEREage20;-- 快照读返回10行-- 事务 B 插入一条age20并提交-- 事务 A 执行当前读SELECT*FROMusersWHEREage20FORUPDATE;-- 返回11行幻读结论这不是幻读因为事务A通过不同读类型快照读→当前读获取了不同的数据视图。幻读的定义要求相同的查询类型。如果事务A第一次就用当前读第二次也用当前读不会有幻读。7. 不同隔离级别下幻读的处理隔离级别是否有幻读解决方案备注READ UNCOMMITTED✅ 有无脏读幻读READ COMMITTED✅ 有无有不可重复读幻读REPEATABLE READ❌ 无MVCC快照读 Next-Key Lock当前读MySQL默认级别SERIALIZABLE❌ 无所有SELECT隐式加锁LOCK IN SHARE MODE性能极差实际不用RC vs RR区别RC级别没有Gap Lock所以当前读无法防止幻读RC级别快照读每次都生成新ReadView所以快照读也无法防止幻读8. 实战案例分析案例1秒杀系统防止超卖-- 商品表CREATETABLEproduct(idINTPRIMARYKEY,stockINT,INDEXidx_stock(stock));-- 减库存避免幻读防止减到负数STARTTRANSACTION;-- 当前读锁定stock范围SELECTstockFROMproductWHEREstock1FORUPDATE;-- 检查后执行减库存UPDATEproductSETstockstock-1WHEREid1;COMMIT;案例2订单号生成防止重复-- 使用间隙锁防止幻读STARTTRANSACTION;-- 锁定order_no在(100, 200]范围的间隙SELECT*FROMordersWHEREorder_noBETWEEN100AND200FORUPDATE;-- 在此范围内生成新订单号不会被其他事务插入重复INSERTINTOorders(order_no)VALUES(150);COMMIT;9. 总结对比表对比维度MVCCNext-Key Lock解决的问题快照读的幻读不可重复读当前读的幻读核心机制Undo版本链 ReadViewRecord Lock Gap Lock是否加锁无锁非阻塞有锁阻塞其他事务隔离级别依赖RR级别及以上RR级别及以上性能开销低中间隙锁可能扩大锁范围适用场景普通查询SELECT FOR UPDATE、UPDATE、DELETE面试官想要的满分总结MySQL通过两种机制解决幻读取决于读类型快照读普通SELECT通过MVCC解决。事务第一次快照读生成ReadView后续快照读复用该ReadView通过Undo版本链判断可见性新提交事务插入的数据不可见保证了无幻读。当前读SELECT FOR UPDATE/UPDATE/DELETE通过Next-Key Lock解决。Next-Key Lock Record Lock Gap Lock不仅锁定现有记录还锁定记录之间的间隙阻止其他事务在间隙中插入新记录从而防止幻读。关键区别MVCC是乐观锁方案无锁并发性能好Next-Key Lock是悲观锁方案加锁阻塞保证当前读数据一致注意点RR级别下MVCC和Next-Key Lock共同保证100%无幻读唯一索引等值查询时Next-Key Lock退化为Record Lock无索引时Next-Key Lock会锁全表所有间隙记录一句话快照读的幻读靠MVCC当前读的幻读靠Next-Key LockRecord LockGap Lock两者在RR级别下联手彻底消灭幻读。觉得对您有帮助麻烦点点关注啦您的关注是我创作的最大动力~