在数据库事务一致性中,不可重复读(Non-repeatable Read)和幻读(Phantom Read)是两个容易混淆但本质不同的问题。为什么他们都是“同一个事务中两次读的结果不一样”,还非要分成两种呢,因为发生的根源、影响范围和需要的应对策略完全不同。
另外,一个常见问题:“如果数据被删除,再次读不到,算不可重复读还是幻读?”
不可重复读(Non-repeatable Read)
1. 定义
在一个事务中,前后两次读取同一行记录,但第二次读取的值和第一次不同,原因是另一个事务在中间修改或删除了该行数据并提交了。
2. 举例
假设有一张 account
表:
id | name | balance |
---|---|---|
1 | 张三 | 1000 |
两个事务并发执行:
事务 A:
BEGIN;
SELECT balance FROM account WHERE id = 1; -- 得到 1000
事务 B:
BEGIN;
UPDATE account SET balance = 800 WHERE id = 1;
COMMIT;
事务 A 继续:
SELECT balance FROM account WHERE id = 1; -- 得到 800,和第一次不一致
这种前后两次读取同一行结果不同的现象,就是不可重复读。
3. 删除是否也属于不可重复读?
是的。若事务 B 在中间 删除了该行,事务 A 第二次读的时候发现记录不存在,也属于不可重复读的一种形式。
-- 事务 B 删除该行:
DELETE FROM account WHERE id = 1;
COMMIT;
-- 事务 A 再次读:
SELECT balance FROM account WHERE id = 1; -- 无结果(行被删除)
虽然不是字段值变化,而是整行消失,本质上仍然是“你试图读同一行,结果不同”,因此归类为不可重复读。
二、幻读
1. 定义
在一个事务中,前后两次对某个范围(WHERE 条件)进行查询时,第二次读出的行数比第一次多或少了,原因是另一个事务插入或删除了满足条件的新记录。
2. 举例
假设有一张 orders
表:
id | user_id | amount |
---|---|---|
1 | 1 | 100 |
2 | 1 | 200 |
事务 A:
BEGIN;
SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 返回 2 条
事务 B:
INSERT INTO orders (id, user_id, amount) VALUES (3, 1, 300);
COMMIT;
事务 A 继续:
SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 返回 3 条,多了一条
这就是幻读:查询范围相同,但结果多出了“幻影”一样的新记录。
不可重复读 vs 幻读:对比总结
项目 | 不可重复读 | 幻读 |
---|---|---|
关注点 | 同一条记录 | 查询范围内的数据行 |
原因 | 其他事务修改或删除了已存在的记录 | 其他事务插入或删除了新记录 |
表现 | 同一行数据两次读取不一致或消失 | 相同条件下读取结果行数变多或变少 |
解决方式 | 行锁(Record Lock) | 间隙锁(Gap Lock)或 Serializable 隔离级别 |
为什么非要区分这两者?
虽然现象相似,但两者必须区分,原因如下:
-
业务影响不同:
- 不可重复读更多影响的是一致性判断和逻辑校验。
- 幻读会影响聚合、统计、分页、唯一性校验等范围逻辑。
-
隔离级别影响不同:
REPEATABLE READ
可以防止不可重复读,但可能仍会发生幻读(除非使用间隙锁)。- 只有
SERIALIZABLE
能完全防止幻读。
-
底层机制不同:
- 不可重复读可通过加锁特定记录防止(行锁)。
- 幻读需要锁定“空隙”或范围(间隙锁、next-key lock),这是 InnoDB 引擎特有的处理。
一个生活类比
你在图书馆查阅:
- 不可重复读:你两次查阅同一本书,第一次看到第1版,第二次被人换成了第2版,或整本书消失了。
- 幻读:你两次查阅“数据库类书籍”区域,第一次有5本,第二次突然多了1本新书。
两个现象不同,因此数据库必须分别以不同方式应对,以免 应对不足|或者|应对过多资源浪费。
总结
不可重复读强调的是“同一条记录变了”,包括更新和删除;
幻读强调的是“范围内的数据增删”,出现了新的行 / 少了某些行。