MySQL 数据库隔离级别与幻读现象

MySQL 中的隔离级别与幻读现象

1. 前言

在使用数据库时,事务(Transaction)是保证数据一致性的重要手段。为了让事务并发执行而尽量减少冲突和数据不一致,SQL 标准定义了四种常见的隔离级别:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

其中,MySQL/InnoDB 默认的隔离级别是 REPEATABLE READ。很多人以为在这个级别下已经可以避免大多数并发异常,但往往会对“幻读”这个问题产生疑惑,甚至有“在 InnoDB 下就不会出现幻读”的印象。然而,实际情况是 InnoDB 通过间隙锁(Gap Lock)或 Next-Key Lock 大部分时候能阻止幻读,但并意味着“绝对不会”发生幻读。

本文将从以下几个方面展开:

  1. 事务隔离级别概览
  2. 幻读(Phantom Read)的定义
  3. InnoDB 中的一致性读与当前读
  4. 间隙锁(Gap Lock)及其作用
  5. 为什么 MySQL InnoDB 的 REPEATABLE READ 下仍可能出现幻读
  6. 示例与总结

2. 数据库隔离级别概览

按照 SQL 标准,从低到高通常分为以下四种隔离级别:

  1. READ UNCOMMITTED

    • 最低级别,允许读取尚未提交的数据(脏读)。也有可能出现不可重复读、幻读等各种异常。
  2. READ COMMITTED

    • 只能读取已经提交的数据,避免了脏读。但仍可能出现“不可重复读”和“幻读”。
  3. REPEATABLE READ

    • 保证在同一个事务中,多次读取同一范围的数据,读到的结果一致。在多数数据库里,它能避免不可重复读,但通常仍无法避免“幻读”。
    • MySQL InnoDB 通过MVCC + 间隙锁的组合,在大部分范围查询的场景下能避免幻读。但是由于某些特殊场景(例如唯一索引等值查询),依旧可能出现幻读。
  4. SERIALIZABLE

    • 最高级别,相当于对所有查询都加锁,事务几乎是串行执行,吞吐量最低,但可以彻底避免脏读、不可重复读、幻读等并发异常问题。

3. 幻读(Phantom Read)是什么?

  • 幻读(Phantom Read):当一个事务(事务 A)在对某一范围内的记录进行查询(特别是加锁查询)时,如果另一个事务(事务 B)在该范围内插入了新的数据行,导致事务 A 在同一个事务中两次读取时出现了前后结果不一致、多出“幻影”数据的情况,就称为“幻读”。

举个简单的例子:

  • 事务 A 第一次执行:SELECT * FROM table WHERE col BETWEEN 10 AND 20 FOR UPDATE;
    可能只查到若干行。
  • 事务 B 这时往 table 里插入了一条新行,其 col 值在 10 到 20 之间。
  • 事务 A 再次执行同一个查询,发现新的那行出现了,这比第一次多了一行,这就是“幻读”。

4. InnoDB 中的一致性读与当前读

MySQL InnoDB 常提到的 MVCC(Multi-Version Concurrency Control) 可以保证“可重复读”,但前提是使用一致性读(Snapshot Read),即普通不加锁的 SELECT。在这种场景下,同一个事务中的多次查询会基于同一“Read View”,看不到其他事务在此之后插入或更新的数据,所以看起来就没有幻读。

然而,一旦我们使用以下这些操作,就会触发 当前读(Current Read)

  • SELECT ... FOR UPDATE
  • SELECT ... LOCK IN SHARE MODE
  • 以及各种实际的 UPDATE, DELETE, INSERT 语句

当前读需要获得数据的最新版本并加锁,目的是防止并发修改冲突。此时如果只锁住了已存在的记录,却没有锁住记录之间的“间隙”,别的事务就能往中间插入新行。当同一个事务再次查询同一范围时,就会看到之前不存在的新行,从而出现幻读。


5. 间隙锁(Gap Lock)与 Next-Key Lock

5.1 什么是间隙锁?

间隙锁(Gap Lock):对索引记录之间的“间隙”进行加锁。这样可以避免其他事务往这个区间插入新数据。

举个非常简单的示例:

CREATE TABLE t (
  id INT PRIMARY KEY,
  value VARCHAR(100)
) ENGINE = InnoDB;

INSERT INTO t (id, value) VALUES
(10, 'A'),
(20, 'B'),
(30, 'C');

如果事务 A 做了:

START TRANSACTION;
SELECT * FROM t WHERE id > 10 AND id < 30 FOR UPDATE;

InnoDB 不仅给已存在的 id=20 这条记录加行锁,也会在 (10,30) 之间上间隙锁,阻止任何事务在 id=11,12,...29 的范围插入新数据。否则,一旦被插入新的行,就会导致“幻读”。

5.2 Next-Key Lock

Next-Key Lock 可以简单理解为:行锁(Record Lock) + 间隙锁(Gap Lock) 的组合。

  • 当 InnoDB 在某个索引范围内做扫描时,会把每条命中的记录和它后面的那段间隙一起锁住,使得在这段区间里无法插入新的行。

5.3 为什么有时仍可能出现幻读

在大多数情况下,InnoDB 会自动使用 Next-Key Lock 避免幻读。但在一些特殊场景下(如唯一索引 + 等值查询且没有匹配到任何行),InnoDB 往往只给已存在的行加行锁,而不会给那块“空洞”上间隙锁。于是,如果其他事务往这个等值位置插入一条新记录,就会被事务再次读取时“看见”,出现幻读。


6. 为什么 MySQL REPEATABLE READ 下仍可能出现幻读?

根据 SQL 标准,REPEATABLE READ 级别本就不一定能完全避免幻读。但 InnoDB 做了许多努力,通过间隙锁Next-Key Lock在绝大部分情况下避免了幻读,这就让很多人误以为 MySQL 的 REPEATABLE READ 不会出现幻读。

然而,仍有典型场景会触发幻读:

场景:唯一索引 + 等值查询并无匹配记录

  • 事务 A:SELECT * FROM t WHERE id = 10 FOR UPDATE;
    如果表里并没有 id=10 这条记录,则 InnoDB 不会对 id=10 的间隙加锁(因为没有任何实际行被命中)。
  • 事务 B:INSERT INTO t(id, value) VALUES(10, 'phantom'); 成功插入。
  • 事务 A 再次查询 SELECT * FROM t WHERE id = 10 FOR UPDATE; 这次就查到 id=10 的行,出现幻读。

也就是说,InnoDB 的 REPEATABLE READ 通过 Next-Key Lock 大多数时候避免了幻读,但当查询条件是“唯一索引 + 等值”且没有匹配数据时,InnoDB 并不会给“这个不存在的值对应的空隙”加锁,因此其他事务可以插入新行,导致幻读。


7. 示例:唯一索引 + 等值查询下的幻读

表结构

CREATE TABLE t (
  id INT PRIMARY KEY,
  value VARCHAR(100)
) ENGINE = InnoDB;

事务 A(Session A)

START TRANSACTION;
SELECT * FROM t WHERE id = 10 FOR UPDATE;
-- 假设表里目前没有 id = 10 的记录
-- 此时 InnoDB 并不会锁住 "id=10" 这块间隙

事务 B(Session B)

START TRANSACTION;
INSERT INTO t(id, value) VALUES(10, 'phantom');
COMMIT;
-- 可以成功插入,因为 A 只对“存在的记录”加了行锁,
-- 对这个并不存在的“id=10”没加间隙锁。

事务 A 继续

SELECT * FROM t WHERE id = 10 FOR UPDATE;
-- 这次就查到了 (id=10, value='phantom') 这条记录
-- 前后两次查询不一致 -> 幻读

8. 总结

  1. MVCC 快照读:在不加锁的 SELECT 中,InnoDB 基于多版本控制(生成 Read View)的方式,通常可以保证在同一事务下多次读取结果一致,不会出现“幻读”。

  2. 当前读(加锁读):例如 SELECT ... FOR UPDATEUPDATEDELETE 等,需要读取最新版本数据并加锁,如果不使用间隙锁(Gap Lock)Next-Key Lock,其他事务就有机会往这个范围插入新记录,下次读取时就会出现幻读。

  3. 间隙锁/Next-Key Lock:InnoDB 默认会在 REPEATABLE READ 下使用这套锁机制(主要针对范围查询),从而“部分”解决幻读问题。但在一些特定场景(比如“唯一索引 + 等值查询无记录”),InnoDB 不会加间隙锁,仍然可能出现幻读。

  4. SERIALIZABLE:如果想彻底杜绝幻读,必须使用更高的隔离级别或通过显式的方式(如手动加锁表或范围)确保不会出现新的插入,但这样做通常会降低并发性能。


9. 参考资料

总的来说,在 MySQL 中,REPEATABLE READ + InnoDB + Next-Key Lock 绝大多数情况都能避免幻读,但要注意它并不是“绝对”避免。在一些特殊查询下,还是有机会出现幻读。理解其根本原因(查询方式锁机制 的不同)是我们真正驾驭数据库并发的关键。


欢迎在评论区交流对 MySQL 隔离级别、MVCC、幻读的看法或疑问。

posted on 2025-03-08 02:30  滚动的蛋  阅读(150)  评论(0)    收藏  举报

导航