InnoDB 事务隔离级别:把幻读这件事实测一遍

米阳 2022-7-9 4 7/9

隔离级别这四个字背了很多年,但真正理解 RR(可重复读)是怎么“看起来”解决幻读的,还是自己开两个会话做实验之后。

准备

CREATE TABLE t (id INT PRIMARY KEY, v INT, KEY idx_v(v)) ENGINE=InnoDB;
INSERT INTO t VALUES (1,10),(2,20),(3,30);
SELECT @@transaction_isolation;  -- REPEATABLE-READ

实验一:快照读

会话 A:

START TRANSACTION;
SELECT * FROM t WHERE v = 20;   -- 1 行

会话 B 插入并提交:

INSERT INTO t VALUES (4,20);

会话 A 再查:

SELECT * FROM t WHERE v = 20;   -- 还是 1 行

看不到新插入的行。原因不是“锁住了”,而是 MVCC:RR 级别下,事务第一次读的时候建立了 Read View,之后一直用它,看到的是同一个快照。

实验二:当前读

会话 A 执行加锁读:

SELECT * FROM t WHERE v = 20 FOR UPDATE;   -- 2 行

看到了 B 插入的那行。因为 FOR UPDATE 是当前读,读最新版本,并且加 next-key lock 锁住范围。

关键结论

RR 并没有真正消灭幻读,只是在快照读下用 MVCC 让你看不到,在当前读下用间隙锁(gap lock)挡住插入。两种读的语义不一致,这就是为什么同一个事务里 SELECT 和 SELECT ... FOR UPDATE 结果可能不同。

间隙锁的副作用

WHERE v = 20 FOR UPDATE 锁的不只是 v=20 那几行,还有它周围的间隙。两个事务对不同“不存在的值”加锁,可能互相阻塞甚至死锁:

A: SELECT * FROM t WHERE v = 25 FOR UPDATE;  -- 锁 (20,30) 间隙
B: INSERT INTO t VALUES (5, 26);             -- 等待

所以生产环境尽量避免大范围 FOR UPDATE,锁的范围越大,冲突越多。

各隔离级别的实际取舍

级别 脏读 不可重复读 幻读 实际使用
读未提交 有 有 有 几乎不用
读已提交 无 有 有 部分业务为了少锁用
可重复读 无 无 快照读下无 MySQL 默认
串行化 无 无 无 极少数对账场景

RC 级别下没有间隙锁,锁冲突少、并发高,代价是同一事务内两次读可能不一致。很多互联网业务会改成 RC,用业务逻辑兜住不一致的地方。

我踩过的坑

在 RR 下写了个“先查余额再扣减”的逻辑,测试环境一直正常。上线后并发一高就出现超扣——因为两次查询都在快照里,看不到别的事务已经扣过。改成 UPDATE ... SET balance = balance - 100 WHERE id = 1 AND balance >= 100,靠行锁和条件判断,问题消失。

结论:读取快照不能替代写操作的原子性,条件更新才是并发扣减的正确姿势。

- THE END -

米阳

7月09日10:10

最后修改:2022年7月9日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论