间隙锁是怎么来的
默认隔离级别下,InnoDB 做范围扫描时不仅锁住命中的记录,还把记录之间的「空隙」一起锁上,这套组合锁的作用是阻止其他事务往区间里插入新行,从而压住幻读。PostgreSQL 没有间隙锁这个概念,它靠多版本快照与读一致性来应对幻读,写冲突则交给后续的冲突检测。理解这一点,才能明白为什么同一段并发逻辑在两个数据库上的等待行为完全不同。
基本信息
| 适用平台 | MySQL 8.0(InnoDB)/ PostgreSQL 12 及以上 |
|---|---|
| MySQL 机制 | 范围锁定与部分等值查找会锁住记录间的空隙 |
| PostgreSQL 机制 | 只做行级锁,幻读依靠快照与串行化冲突检测处理 |
| 关闭条件 | MySQL 在较低隔离级别下间隙锁基本关闭,但外键与唯一键检查仍会用 |
| 主要代价 | 并发正确性提高的同时,事务之间更容易相互等待 |
典型使用场景
- 区间批量处理:按编号区间锁定一批订单再逐个处理
- 编号占用校验:先查区间再插入,防止唯一编号被并发抢用
- 排查锁等待:定位为什么两条不相干的写入在互相阻塞
- 隔离级别评估:从默认级别降到读已提交前,评估幻读风险
- 从 MySQL 迁到 PostgreSQL 时重新验证并发写入逻辑
观察与验证
观察与验证
-- MySQL:范围锁定读,区间内的插入会被阻塞
BEGIN;
SELECT * FROM orders
WHERE id BETWEEN 100 AND 200
FOR UPDATE;
-- 另一个事务在该区间插入新行会等待,直到本事务提交或回滚
COMMIT;
-- 观察当前锁等待情况
SELECT * FROM performance_schema.data_locks; -- 当前持有的锁
SELECT * FROM performance_schema.data_lock_waits; -- 正在等待的锁
-- PostgreSQL:范围读取不会阻塞插入,需要重新设计并发校验
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 100 AND 200 FOR UPDATE;
-- 该语句只锁已存在的行,区间内新增的行走不到这里的保护
COMMIT;锁行为对照
| 场景 | MySQL InnoDB | PostgreSQL |
|---|---|---|
| 按区间加锁读取 | 锁定命中行与记录间空隙 | 只锁定已存在的行 |
| 区间内并发插入 | 被阻塞,直到持锁事务结束 | 不被阻塞,可以插入 |
| 唯一索引等值命中 | 命中已存在记录时只加记录锁 | 只加行锁 |
| 幻读防护 | 依赖间隙锁 | 依赖快照与串行化检测 |
| 锁等待排查 | 有专门的锁信息表可查 | 用系统视图与等待事件排查 |
用非索引条件做锁定读取会扩大锁定范围,严重时近似锁住整张表:执行计划显示全表扫描时,几乎所有行都会被加上锁。加锁查询前先看执行计划,确认走的是索引。
把 MySQL 的间隙锁经验直接搬到 PostgreSQL 会造成误判:PG 的范围读取不阻塞插入,原本依赖「锁住区间防止重复插入」的逻辑必须改用唯一约束或显式冲突检测来实现。