幻读指的是在同一个事务中,前后两次执行相同的范围查询,后一次查询看到了前一次查询没有看到的行数据,这些新增的行数据通常是其他事务在两次查询之间插入并提交的数据。幻读和不可重复读容易混淆,不可重复读是同一行数据的内容被修改,幻读是出现了之前不存在的新行。

幻读产生的典型场景
我们可以通过一个具体的MySQL InnoDB引擎下的示例来还原幻读的产生过程,首先准备一张测试表,表结构如下:
-- 创建测试表
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
age INT NOT NULL,
INDEX idx_age (age)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入初始数据
INSERT INTO user (name, age) VALUES ('张三', 20), ('李四', 22), ('王五', 25);
假设当前数据库的事务隔离级别是可重复读(Repeatable Read),这是MySQL InnoDB的默认隔离级别,我们开启两个事务来模拟并发场景:
事务执行步骤
| 步骤 | 事务A | 事务B |
|---|---|---|
| 1 | START TRANSACTION; | START TRANSACTION; |
| 2 | SELECT * FROM user WHERE age > 20; -- 返回李四(22)、王五(25)两条数据 | |
| 3 | INSERT INTO user (name, age) VALUES ('赵六', 23); COMMIT; | |
| 4 | SELECT * FROM user WHERE age > 20; -- 此时还是返回李四、王五两条数据,可重复读级别下普通查询看不到新插入的行 | |
| 5 | UPDATE user SET age = 24 WHERE age > 20; -- 此时会更新3行数据,包括事务B插入的赵六 | |
| 6 | SELECT * FROM user WHERE age > 20; -- 此时返回李四(24)、王五(25)、赵六(24)三条数据,出现了之前查询没有的行,产生幻读 |
幻读产生的底层原因
幻读的产生和数据库的多版本并发控制(MVCC)以及锁机制有关,在可重复读隔离级别下:
- 普通的
SELECT查询属于快照读,事务启动时生成一致性视图,整个事务期间读取的都是这个视图的数据,所以不会看到其他事务新插入并提交的数据,这也是步骤4中事务A第二次查询没有看到赵六的原因。 - 而
UPDATE、DELETE、INSERT以及加锁的SELECT(比如SELECT ... FOR UPDATE)属于当前读,读取的是最新的数据版本,并且会对读取到的行加锁。步骤5中事务A执行UPDATE是当前读,会读取到事务B已经提交的赵六这条数据,并且更新它。 - 当
UPDATE操作修改了之前查询范围外的数据后,再次执行快照读时,由于MVCC的可见性判断规则,已经被当前读修改过的数据会被纳入当前事务的可见范围,因此步骤6的查询就会看到赵六这条新行,幻读就产生了。
不同隔离级别对幻读的处理
SQL标准定义了四种事务隔离级别,不同级别对幻读的支持情况不同:
- 读未提交(Read Uncommitted):会出现脏读、不可重复读、幻读,因为可以读取到其他事务未提交的插入数据。
- 读已提交(Read Committed):不会出现脏读,但会出现不可重复读和幻读,每次查询都会生成新的一致性视图,所以能看到其他事务已提交的插入数据。
- 可重复读(Repeatable Read):MySQL的InnoDB引擎通过间隙锁(Gap Lock)机制,在加锁查询时可以完全避免幻读,但普通快照读场景下还是可能出现上述示例中的幻读情况。
- 串行化(Serializable):完全避免幻读,所有事务串行执行,读操作也会加锁,其他事务无法插入符合查询范围的数据。
如何避免幻读
如果业务场景中需要完全避免幻读,可以采取以下方案:
- 将事务隔离级别设置为串行化,但会大幅降低并发性能,适合并发量低的场景。
- 在可重复读隔离级别下,对范围查询使用
SELECT ... FOR UPDATE加锁,触发间隙锁,阻止其他事务在查询范围内插入数据,示例代码如下:
-- 加锁查询,防止其他事务插入age>20的数据 START TRANSACTION; SELECT * FROM user WHERE age > 20 FOR UPDATE; -- 此时其他事务无法插入age>20的新行,直到当前事务提交 -- 后续的业务操作 COMMIT;
需要注意的是,间隙锁只在可重复读及以上隔离级别生效,读已提交隔离级别下没有间隙锁,无法通过这种方式避免幻读。