MySQL事务隔离级别与幻读问题:可重复读真的能防止幻读吗
本文通过实际操作和原理解析,带你验证MySQL默认隔离级别可重复读(Repeatable Read, RR)对幻读的防范机制。
1. 准备实验环境
为了直观演示幻读现象及解决方案,我们需要先构建一张测试表并初始化数据。
- 登录 MySQL数据库。
- 创建一个名为
test_db的数据库(如果尚未存在)并 选择使用它。
CREATE DATABASE IF NOT EXISTS test_db;
USE test_db;
- 创建一张简单的账户表
account,包含id(主键)、name和balance字段。
CREATE TABLE account (
id INT PRIMARY KEY,
name VARCHAR(50),
balance INT
) ENGINE=InnoDB;
- 插入三条初始数据。
INSERT INTO account VALUES (1, 'Alice', 100);
INSERT INTO account VALUES (5, 'Bob', 200);
INSERT INTO account VALUES (10, 'Charlie', 300);
- 确认当前事务隔离级别为
REPEATABLE-READ。
SELECT @@GLOBAL.transaction_isolation;
如果返回结果不是 REPEATABLE-READ,请 执行以下命令 修改并 退出客户端重新登录以生效。
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
2. 理解核心概念:什么是幻读
幻读是指在一个事务内,前后两次进行同一条件范围的查询,结果中出现了之前不存在的行,或者原有的行消失了,就像是产生了“幻觉”。
要理解MySQL如何解决这个问题,必须区分两种读取方式:
- 快照读:普通的
SELECT语句,读取的是历史版本,不加锁。 - 当前读:读取最新数据,并加锁,如
SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT。
3. 验证快照读:MVCC如何防止幻读
快照读依赖于 MVCC(多版本并发控制)。在 RR 级别下,事务内第一次 SELECT 时会生成一个 Read View(读视图),后续的查询都复用这个视图,因此看不到其他事务已提交的新增数据。
请打开两个终端(或两个数据库连接窗口),分别记为 终端A 和 终端B。
操作步骤:
-
在 终端A 中,开启一个事务。
BEGIN; -
在 终端A 中,查询
account表的所有记录。SELECT * FROM account;此时你会看到3条记录:id为 1, 5, 10。
-
在 终端B 中,开启一个新事务,并 插入一条新记录。
BEGIN; INSERT INTO account VALUES (8, 'Dave', 500); -
在 终端B 中,提交事务。
COMMIT; *此时id=8的记录已真实存在于数据库中。* -
回到 终端A,再次执行相同的查询语句。
SELECT * FROM account;
观察结果:
你会发现 终端A 的查询结果依然只有3条记录,没有出现 id=8 的 Dave。这说明在快照读模式下,MySQL 通过 MVCC 成功避免了幻读。
-
提交 终端A 的事务以结束测试。
COMMIT;
4. 验证当前读:Next-Key Lock如何防止幻读
当前读需要读取最新数据并确保数据一致性,MVCC 在这里不适用,必须依靠锁机制。MySQL InnoDB 使用 Next-Key Lock(记录锁 + 间隙锁)来锁住记录及其之间的间隙,阻止其他事务在这个范围内插入新数据。
继续使用 终端A 和 终端B。
操作步骤:
-
在 终端A 中,开启事务。
BEGIN; -
在 终端A 中,执行当前读查询,锁定
id > 5的记录。SELECT * FROM account WHERE id > 5 FOR UPDATE;此时该语句会命中 id=10 的记录,并锁定 (5, 10] 和 (10, +∞) 这两个间隙。
-
在 终端B 中,开启事务。
BEGIN; -
在 终端B 中,尝试在间隙中插入一条记录,例如 id=8。
INSERT INTO account VALUES (8, 'Eve', 600);
观察结果:
此时你会发现 终端B 的操作卡住了(处于等待状态),并没有立即报错或成功。这是因为 终端A 的 Next-Key Lock 锁住了 id=8 可能存在的位置 (5, 10),终端B 无法插入。
-
在 终端A 中,提交事务,释放锁。
COMMIT; -
观察 终端B。
此时 终端B 的插入语句会立即执行成功。
-
在 终端B 中,回滚或 提交事务。
ROLLBACK;
这说明在当前读模式下,MySQL 通过 Next-Key Lock 物理上阻止了其他事务插入符合条件的新行,从而防止了幻读。
5. 特殊情况分析:为什么说“部分解决”?
虽然上述实验证明 RR 级别能防止幻读,但在极端的历史版本或特定场景下,仍需注意:
- 历史快照的一致性:MVCC 解决的是“读取”时的幻读。如果你在事务A中 更新 了一条事务B刚插入的记录(虽然事务A之前看不到,但更新成功后就能看到了),这被称为“写后读”的幻读风险。
- 唯一索引约束:如果基于唯一索引进行当前读,Next-Key Lock 会优化为 Record Lock,仅锁住具体记录,而不锁间隙。但由于唯一约束本身会阻止重复插入,幻读依然被阻止。
- 数据类型与范围:如果查询条件未命中索引,InnoDB 会将所有记录和间隙都锁住(退化为表锁),这虽然极度影响性能,但也从物理上彻底杜绝了幻读。
为了更清晰地展示不同隔离级别的差异,请参考下表:
| 隔离级别 | 脏读可能性 | 不可重复读可能性 | 幻读可能性 | MySQL 默认 |
|---|---|---|---|---|
| 读未提交 | 是 | 是 | 是 | 否 |
| 读已提交 | 否 | 是 | 是 | 否 |
| 可重复读 | 否 | 否 | 否 | 是 |
| 串行化 | 否 | 否 | 否 | 否 |
注:表中“否”代表该级别在理论实现或MySQL具体实现中能够防止对应问题。
6. 总结核心逻辑
MySQL 在 REPEATABLE-READ 隔离级别下防止幻读采用的是混合策略:
- 依靠 MVCC 解决快照读的幻读:通过 Read View 保证事务内看到的数据版本一致。
- 依靠 Next-Key Lock 解决当前读的幻读:通过 Record Lock(行锁)和 Gap Lock(间隙锁)的结合,锁住索引记录之间的空隙,禁止其他事务插入满足条件的新行。
通过理解这两种机制的分工,才能准确判断业务代码中是否会产生幻读。

暂无评论,快来抢沙发吧!