隔离级别这四个字背了很多年,但真正理解 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,靠行锁和条件判断,问题消失。
结论:读取快照不能替代写操作的原子性,条件更新才是并发扣减的正确姿势。
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://mi-blog.cn/archives/660
共有 0 条评论