RC 默认不加间隙锁,RR 默认加临键锁
间隙锁(gap lock)是 RR 隔离级别为避免幻读而自动施加的“安全锁”——它不仅锁定匹配的记录,还会锁住索引记录之间的空隙。举例说明:表中存在 id 分别为 1、5、10 的三条记录,当执行 UPDATE t SET name='x' WHERE id BETWEEN 3 AND 8 时,RR 将锁定 (1,5] 与 (5,10) 两个间隙区间,此时其他事务若尝试插入 id=4 或 id=6,则必须等待。而相同语句在 RC 下,仅对实际命中的 id=5 施加一个 record lock,间隙完全开放,任何插入操作均可立即执行。
死锁的产生,多数情况下源于多个事务以不同顺序争夺同一组间隙锁。RC 下根本不涉及跨间隙的锁竞争,因此死锁概率自然大幅降低。
RC 的半一致性读让 UPDATE 不盲目加锁
当 UPDATE 语句扫描到已被其他事务持有 X lock 的记录时,RC 会触发半一致性读(semi-consistent read):首先读取该行最新已提交的版本,判断其是否真正满足 WHERE 条件;仅当条件成立时才尝试加锁。而 RR 则不论条件如何,对扫描到的行直接施加 next-key lock,若锁被占用,则只能阻塞等待。
这一差异意味着什么?
- 在 RC 下,无索引查询最多只会锁定最终更新的几行
- 同样的语句在 RR 下可能锁定整个扫描范围(全表扫描 + 全表间隙锁),锁等待链一旦拉长,死锁几乎不可避免
RC 锁释放更早,减少锁持有时间重叠
在 RC 中,UPDATE 或 DELETE 语句执行完毕后,未命中行的锁会立即释放;而 RR 会对所有扫描过的行(包括不满足 WHERE 条件的)都施加 next-key lock,并一直持有到事务提交或回滚。
通过一个例子可以更清晰地理解:执行 UPDATE users SET status=2 WHERE created_at > '2026-01-01' 时,RR 可能锁定几千行(即使实际仅修改了 3 行),导致其他事务无法更新任何一行;而 RC 仅锁定那 3 行,其余扫描行的锁瞬间释放——锁持有窗口越窄,事务间的冲突窗口自然就越小。
RC 下 INSERT 死锁仍可能发生,但诱因完全不同
切勿认为 RC 完全与间隙锁绝缘——在唯一键冲突检查、外键约束校验等场景下,MySQL 仍会施加 gap lock,此时 INSERT 死锁仍有可能出现。但这只是极少数显式约束触发的特殊情况,与 RR 对每个范围查询都默认施加间隙锁的“地毯式覆盖”完全不可同日而语。
因此,真正需要警惕的并非“RC 到底有没有间隙锁”,而是以下两点:
- 不要在 RC 下想当然地认为“绝对安全”,而忽略了唯一索引冲突时隐式间隙锁的存在
- 不要因为应用层未能妥善处理不可重复读,就生硬地依靠加锁来硬扛,结果人为制造出新的死锁点
归根结底,间隙锁死锁的减少,本质上是将锁的默认行为从 RR 的保守全覆盖,转向了 RC 的按需最小化——但这一“按需”,最终仍需要你通过正确的 SQL 编写、合理的索引设计以及清晰的业务逻辑才能真正落地生效。
