外观
现在有个表,一列 id,一列 name,说一下幻读是怎么发生的?
⭐ 题目日期:
字节 - 2024/12/17
📝 题解:
幻读现象的产生(以表 users(id, name)
为例)
假设表结构如下:
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);
初始数据:
INSERT INTO users (id, name) VALUES (1, 'A'), (3, 'C'), (5, 'E');
幻读发生场景示例
事务 A 和 事务 B 按以下步骤操作:
时间点 | 事务 A | 事务 B |
---|---|---|
T1 | BEGIN; | |
T2 | SELECT * FROM users WHERE id > 3; → 结果:(5, 'E') | |
T3 | BEGIN; | |
T4 | INSERT INTO users (id, name) VALUES (6, 'F'); → 插入新记录 (6, 'F') | |
T5 | COMMIT; | |
T6 | SELECT * FROM users WHERE id > 3; → 结果:(5, 'E'), (6, 'F') (多出一条) | |
T7 | COMMIT; |
结果:事务 A 在 T2 和 T6 的两次查询中,发现 id > 3
的记录从 1 条变为 2 条,幻读发生。
幻读的核心原因
- 范围查询:事务 A 的查询条件是基于范围(
id > 3
)。 - 插入新记录:事务 B 插入了一条满足该范围的新记录(
id=6
)。 - 隔离级别:若隔离级别为 可重复读(Repeatable Read),但未正确使用锁机制时,仍可能发生幻读。
MySQL 如何解决幻读?
在 可重复读(默认隔离级别) 下,InnoDB 通过 Next-Key Locks 防止幻读:
- Next-Key Lock 范围:
- 当事务 A 执行
SELECT ... WHERE id > 3
时,锁定(3, +∞)
的索引范围和间隙。 - 事务 B 插入
id=6
时会被阻塞,直到事务 A 提交。
- 当事务 A 执行
- 加锁后的操作流程:
时间点 | 事务 A | 事务 B |
---|---|---|
T1 | BEGIN; | |
T2 | SELECT * FROM users WHERE id > 3 FOR UPDATE; → 结果:(5, 'E') | |
T3 | BEGIN; | |
T4 | INSERT INTO users (id, name) VALUES (6, 'F'); → 阻塞等待锁释放 | |
T5 | COMMIT; → 释放锁 | |
T6 | 插入成功 | |
T7 | COMMIT; |
结果:事务 B 的插入操作被阻塞,事务 A 的两次查询结果一致,幻读被避免。
幻读与不可重复读的区别
现象 | 不可重复读 | 幻读 |
---|---|---|
数据变化类型 | 同一记录的 值被修改 | 范围内 新增或删除记录 |
关注点 | 单行数据的一致性 | 查询结果集的行数变化 |
解决机制 | MVCC(多版本快照) | Next-Key Locks(锁定范围+间隙) |
总结
- 幻读的触发条件:
- 隔离级别为 可重复读 时,未显式加锁的范围查询。
- 其他事务插入或删除符合查询条件的数据。
- 解决方案:
- 使用
SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
显式加锁。 - 依赖 InnoDB 的 Next-Key Locks 机制(默认启用)。
- 使用
- 实际开发建议:
- 对需要严格一致性的范围查询显式加锁。
- 理解隔离级别与锁机制的关系,避免高并发场景下的性能问题。