解决 InnoDB 幻读[MySQL]:MVCC/next-key lock 原理 + 特殊场景避坑

4 阅读2分钟

解决幻读

(理解掌握 快照读 和 当前读 ,InnoDB避免幻读的方式。。。)

目录

  • 一、幻读
  • 二、 InnoDB 是怎么 “尽量避免” 幻读的?
  • 三、 为什么 “没完全解决”幻读?
  • 四、怎么避免这些特殊幻读?

一、幻读

同一个事务中,由于期间其他并发事务修改这个结果集,多次相同的查询在不同时间得到不同的结果集,就是幻读。

二、 InnoDB 是怎么 “尽量避免” 幻读的?

方案 A:快照读(普通 SELECT)→ MVCC

  • 事务开始后,第一次执行 SELECT 时,会生成一个 Read View
  • 之后所有的普通 SELECT,都用这个 Read View 去 undo log 里找 “事务开始时” 的数据版本。
  • 所以,即使别的事务插入了新数据,你也查不到,避免了幻读。

方案 B:当前读(SELECT ... FOR UPDATE / UPDATE / DELETE)→ next-key lock

  • sql执行前查询最新版本的数据,所以 InnoDB 用了 next-key lock(记录锁 + 间隙锁)。
  • 它锁住查询范围和区间,防止别的事务在这个范围里插入新行。

举例:事务 A 执行,就会在 id > 2 的范围上加 next-key lock。

 begin;
 select * 
 from t_stu 
 where id > 2 for update
 ...(同时事务 B 想插入 id=5 的记录,就会被阻塞)
​
 select * 
 from t_stu 
 where id > 2 for update
 commit

事务 B 想插入 id=5 的记录,就会被阻塞,直到事务 A 提交,从而避免幻读。

三、 为什么 “没完全解决”幻读?

场景一:(事务A快照读 + 事务B更新操作)导致幻读

  • 第一次查询用的是事务A开始时的 Read View,看不到新数据。
  • 事务B更新操作是 “当前读”,能看到并修改了事务 B 插入的新数据,这条新数据的 trx_id 变成了事务 A 的。
  • 再次查询时,Read View 就 “看到” 了这条记录,导致前后结果不一致,发生了幻读。

场景二:先快照读,再当前读 导致幻读

  1. T1:事务 A 先快照读 select * from t_test where id > 100 → 3 条记录
  2. T2:事务 B 插入 id=200 的记录并提交
  3. T3:事务 A 再当前读 select * from t_test where id > 100 for update → 4 条记录

这里的问题是:

  • 第一次快照读,用的是旧的 Read View,没看到新数据。
  • 第二次当前读,用的是最新数据,看到了事务 B 插入的行,前后结果集不一样,发生了幻读。

四、怎么避免这些特殊幻读?

  • 尽量在事务开启后,马上执行当前读语句(如 SELECT ... FOR UPDATE)。
  • 这样可以提前加上 next-key lock,锁住范围,防止其他事务插入,从根源上避免幻读。