可重复级别下什么情况处理不了幻读

MySQL InnoDB 在 REPEATABLE READ (RR) 隔离级别下,通过 MVCC间隙锁(Next-Key Lock) 在很大程度上避免了幻读,但并非在所有场景下都能 100% 解决。

幻读的核心问题:一个事务在重新执行同一个查询时,看到了第一次查询时没有的新的行(这些行是由其他已提交事务插入的)。

在 RR 级别下,单纯的快照读 (SELECT) 是不会发生幻读的,因为它始终读取事务开始时的快照。幻读通常发生在当前读快照读混合使用的场景中。


无法解决幻读的典型场景举例

让我们来看一个最经典的例子。假设我们有一张表 users

id (主键) name age
1 Alice 20
5 Bob 25
10 Carol 30

事务执行序列如下:

时间点 事务A (Trx ID=50) 事务B (Trx ID=60)
T1 START TRANSACTION;
T2 SELECT * FROM users WHERE age > 20; (快照读)
结果:
id=5, Bob, 25
id=10, Carol, 30
T3 START TRANSACTION;
T4 INSERT INTO users (id, name, age) VALUES (7, 'Dave', 28);
COMMIT; (提交!)
T5 UPDATE users SET name = 'Hi' WHERE age > 20;
InnoDB 报告: “2 rows affected”
T6 SELECT * FROM users WHERE age > 20; (快照读)
结果:
id=5, Hi, 25
id=10, Hi, 30
id=7, Hi, 28 👈 幻读出现了!
T7 COMMIT;

详细过程分析

  1. T2 (事务A - 快照读)

    • 事务A 执行第一次查询 WHERE age > 20
    • InnoDB 为其生成一个 Read View。此时,它只能看到在它开始之前已提交的数据,即 id=5 和 id=10 这两行。
    • 结果符合预期,没有幻读
  2. T4 (事务B - 插入并提交)

    • 事务B 插入了一条新记录 (7, 'Dave', 28),它满足 age > 20 的条件,并且成功提交。这条新数据成为数据库中的最新状态。
  3. T5 (事务A - 当前读)

    • 这是最关键的一步。事务A 执行了一个 UPDATE 语句。
    • UPDATEDELETEINSERT 都属于当前读。它们不是快照读
    • 当执行 UPDATE ... WHERE age > 20 时,InnoDB 必须找到所有当前最新版本中满足 age > 20 的记录以便更新。它会读取最新的已提交数据。
    • 因此,它看到了事务B 刚刚提交的 id=7 这条新记录。
    • InnoDB 会为所有它找到的记录(id=5, 10, 7)加上锁(间隙锁会锁住范围,防止其他事务再插入,但已插入的且已提交的它管不了),然后进行更新。
    • 所以,UPDATE 操作成功更新了3行数据(包括事务B插入的那一行)。UPDATE 操作本身看不到幻读,因为它总是处理最新数据。
  4. T6 (事务A - 快照读)

    • 事务A 再次执行相同的 SELECT 查询。
    • 在 RR 级别下,它依然复用 T2 时刻生成的旧 Read View
    • 但是id=7 这行记录已经被当前事务A自己UPDATE 语句修改了!记住 MVCC 的可见性规则第一条如果数据版本的 DB_TRX_ID 等于当前事务的 ID,则当前事务总是可见的
    • 虽然新插入的 id=7 最初是由事务B (Trx-ID=60) 创建的,但它现在已经被事务A (Trx-ID=50) 修改了。它的 DB_TRX_ID 变成了 50。
    • 因此,对于事务A 来说,这行数据是“我自己修改的”,所以对我可见。
    • 最终,查询结果变成了三条记录。事务A 看到了一个“幻影行”。

为什么说这是幻读?

  1. 第一次读(T2):事务A 看到了 2 行数据。
  2. 中间操作:它试图修改所有满足条件的行。
  3. 第二次读(T6):它发现自己修改了 3 行,并且查询结果也变成了 3 行。

这个“多出来的一行”就是幻读。事务A 的逻辑前提被打破了:它本以为自己在操作 2 行数据,但实际上操作了 3 行。

总结:RR 级别下幻读发生的条件

  1. 并发事务:有两个并发的事务在操作。
  2. 其他事务插入并提交:另一个事务插入了新的行并成功提交
  3. 当前事务进行当前读:当前事务使用了当前读(如 UPDATE, DELETE, SELECT ... FOR UPDATE)来操作一个范围。当前读会看到其他事务已提交的新数据。
  4. 当前事务再次快照读:当前事务随后再次进行快照读时,会因为自己修改了这些新数据(使得其版本号变为自己的事务ID)而看到它们,从而导致幻读。

如何绝对避免幻读?
如果整个事务中的所有操作都使用当前读(例如全部使用 SELECT ... FOR UPDATE),那么由于间隙锁(Gap Lock)和临键锁(Next-Key Lock)的存在,其他事务无法在查询范围内插入新数据,从而可以彻底避免幻读。但这会严重牺牲并发性能。

因此,MySQL InnoDB 的 RR 级别提供的防幻读保障是:纯快照读不会幻读,但当前读可能会引入幻读。如果要实现最高级别的隔离性(序列化),需要应用程序谨慎地使用当前读来手动加锁。

posted @ 2025-08-30 16:36  adragon  阅读(22)  评论(0)    收藏  举报