MySQL 数据库隔离级别与幻读现象
MySQL 中的隔离级别与幻读现象
1. 前言
在使用数据库时,事务(Transaction)是保证数据一致性的重要手段。为了让事务并发执行而尽量减少冲突和数据不一致,SQL 标准定义了四种常见的隔离级别:
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
其中,MySQL/InnoDB 默认的隔离级别是 REPEATABLE READ。很多人以为在这个级别下已经可以避免大多数并发异常,但往往会对“幻读”这个问题产生疑惑,甚至有“在 InnoDB 下就不会出现幻读”的印象。然而,实际情况是 InnoDB 通过间隙锁(Gap Lock)或 Next-Key Lock 大部分时候能阻止幻读,但并不意味着“绝对不会”发生幻读。
本文将从以下几个方面展开:
- 事务隔离级别概览
- 幻读(Phantom Read)的定义
- InnoDB 中的一致性读与当前读
- 间隙锁(Gap Lock)及其作用
- 为什么 MySQL InnoDB 的 REPEATABLE READ 下仍可能出现幻读
- 示例与总结
2. 数据库隔离级别概览
按照 SQL 标准,从低到高通常分为以下四种隔离级别:
-
READ UNCOMMITTED
- 最低级别,允许读取尚未提交的数据(脏读)。也有可能出现不可重复读、幻读等各种异常。
-
READ COMMITTED
- 只能读取已经提交的数据,避免了脏读。但仍可能出现“不可重复读”和“幻读”。
-
REPEATABLE READ
- 保证在同一个事务中,多次读取同一范围的数据,读到的结果一致。在多数数据库里,它能避免不可重复读,但通常仍无法避免“幻读”。
- MySQL InnoDB 通过MVCC + 间隙锁的组合,在大部分范围查询的场景下能避免幻读。但是由于某些特殊场景(例如唯一索引等值查询),依旧可能出现幻读。
-
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 UPDATESELECT ... 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. 总结
-
MVCC 快照读:在不加锁的 SELECT 中,InnoDB 基于多版本控制(生成 Read View)的方式,通常可以保证在同一事务下多次读取结果一致,不会出现“幻读”。
-
当前读(加锁读):例如
SELECT ... FOR UPDATE、UPDATE、DELETE等,需要读取最新版本数据并加锁,如果不使用间隙锁(Gap Lock)或Next-Key Lock,其他事务就有机会往这个范围插入新记录,下次读取时就会出现幻读。 -
间隙锁/Next-Key Lock:InnoDB 默认会在 REPEATABLE READ 下使用这套锁机制(主要针对范围查询),从而“部分”解决幻读问题。但在一些特定场景(比如“唯一索引 + 等值查询无记录”),InnoDB 不会加间隙锁,仍然可能出现幻读。
-
SERIALIZABLE:如果想彻底杜绝幻读,必须使用更高的隔离级别或通过显式的方式(如手动加锁表或范围)确保不会出现新的插入,但这样做通常会降低并发性能。
9. 参考资料
- MySQL 官方文档 – InnoDB Transaction Model
- MySQL 官方文档 – InnoDB Locking
- 《高性能 MySQL》相关章节
总的来说,在 MySQL 中,REPEATABLE READ + InnoDB + Next-Key Lock 绝大多数情况都能避免幻读,但要注意它并不是“绝对”避免。在一些特殊查询下,还是有机会出现幻读。理解其根本原因(查询方式 与 锁机制 的不同)是我们真正驾驭数据库并发的关键。
欢迎在评论区交流对 MySQL 隔离级别、MVCC、幻读的看法或疑问。
浙公网安备 33010602011771号