MySQL 中的 RR 隔离级别,到底有没有解决幻读问题
MySQL 的 RR(Repeatable Read,可重复读)隔离级别在默认配置下(开启innodb_support_xa和 MVCC),解决了 “大部分场景的幻读”,但未完全解决 “所有场景的幻读”,具体分析如下:
# 1. 先明确:什么是幻读?
幻读是指 “同一事务内,多次执行相同的查询语句,返回的结果集行数不一致”,通常发生在 “事务 A 查询某范围数据,事务 B 插入 / 删除该范围的数据,事务 A 再次查询时,结果集新增 / 减少了行”,示例:
- 事务 A(RR 隔离级别):
SELECT * FROM user WHERE age > 20,返回 10 行数据; - 事务 B:
INSERT INTO user (age) VALUES (25),提交事务; - 事务 A:再次执行
SELECT * FROM user WHERE age > 20,若返回 11 行数据,即发生幻读。
# 2. RR 隔离级别如何通过 MVCC 解决 “快照读” 的幻读?
MySQL 的 RR 隔离级别通过MVCC(多版本并发控制) 实现 “快照读”(普通SELECT语句,不加锁),确保同一事务内多次查询的结果集一致,从而解决幻读:
- MVCC 的核心逻辑:
- 事务启动时,会生成一个 “事务 ID(read_view)”,记录当前活跃的事务 ID 范围;
- 每行数据都包含
DB_TRX_ID(最后修改该数据的事务 ID)和DB_ROLL_PTR(指向 undo 日志的指针,用于恢复历史版本); - 快照读时,InnoDB 会根据
read_view筛选数据:仅返回DB_TRX_ID小于当前事务 ID 且未被删除的历史版本数据;
- 解决幻读的原理:
- 事务 A 启动后,生成
read_view; - 事务 B 插入的数据的
DB_TRX_ID大于事务 A 的read_view,事务 A 的快照读无法看到该数据; - 因此,事务 A 多次执行相同查询,返回的结果集一致,无幻读。
- 事务 A 启动后,生成
# 3. RR 隔离级别未解决 “当前读” 的幻读
“当前读” 是指 “加锁的查询语句”(如SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE)或 “写操作”(INSERT/UPDATE/DELETE),这类操作会读取数据的 “最新版本”,而非历史版本,因此仍可能发生幻读:
- 示例(当前读的幻读):
- 事务 A(RR 隔离级别):
SELECT * FROM user WHERE age > 20 FOR UPDATE,返回 10 行数据(加行锁); - 事务 B:
INSERT INTO user (age) VALUES (25),由于事务 A 未对age=25的行加锁(行锁仅锁已存在的行),事务 B 可成功插入并提交; - 事务 A:再次执行
SELECT * FROM user WHERE age > 20 FOR UPDATE,返回 11 行数据(包含事务 B 插入的行),发生幻读。
- 事务 A(RR 隔离级别):
# 4. 如何完全解决 RR 隔离级别的幻读?
通过间隙锁(Gap Lock) 可完全解决 RR 隔离级别的幻读,InnoDB 在 RR 隔离级别下默认开启间隙锁(通过innodb_locks_unsafe_for_binlog参数控制,默认关闭,即开启间隙锁):
- 间隙锁的作用:锁定 “数据之间的间隙”(如
age>20的间隙包括20~25、25~30等),防止其他事务在间隙中插入数据; - 解决当前读幻读的原理:
- 事务 A 执行
SELECT * FROM user WHERE age > 20 FOR UPDATE时,InnoDB 不仅对已存在的age>20的行加行锁,还对age>20的间隙加间隙锁; - 事务 B 插入
age=25的数据时,需获取该间隙的锁,但间隙锁已被事务 A 持有,事务 B 被阻塞; - 事务 A 再次查询时,无新数据插入,避免幻读。
- 事务 A 执行
# 5. 结论
- MySQL 的 RR 隔离级别通过 MVCC 解决了 “快照读” 的幻读,通过 “间隙锁 + 行锁”(Next-Key Lock)解决了 “当前读” 的幻读;
- 在默认配置下(开启间隙锁),RR 隔离级别可完全避免幻读,这是 MySQL RR 隔离级别与其他数据库(如 PostgreSQL)RR 隔离级别的核心区别(其他数据库的 RR 通常无法解决幻读);
- 若关闭间隙锁(
set innodb_locks_unsafe_for_binlog=1),RR 隔离级别仍会发生 “当前读” 的幻读。
上次更新: 12/30/2025