复合主键范围查询易死锁是因为REPEATABLE READ下Next-Key Lock导致不同事务加锁顺序不一致而形成循环等待,需通过拆分查询、固定排序、降级隔离级别等措施规避。

复合主键范围查询为什么容易死锁
复合主键本身不导致死锁,但范围查询(如 WHERE (a, b) > (1, 100) 或 WHERE a = 1 AND b > 100)在 REPEATABLE READ 隔离级别下会触发 Next-Key Lock(临键锁),即记录锁 + 间隙锁。InnoDB 按索引顺序加锁,而复合主键的 B+ 树结构决定了“范围”可能跨多个索引页、覆盖多个间隙——不同事务若扫描路径或终止点不一致,极易形成循环等待。
典型诱因包括:
- 事务 A 扫描
(a=1, b>100),锁住(1,101)到(1,200)的间隙; - 事务 B 同时执行
INSERT INTO t VALUES (1,150, ...),需在间隙中插入,被 A 阻塞; - 而事务 B 又已持有另一段间隙(比如
(1,50)到(1,100))的插入意向锁,恰好事务 A 下一步要扩展锁范围到那里——形成 A 等 B、B 等 A。
用 EXPLAIN 确认是否真的走复合主键索引
范围查询失效是死锁高发的隐藏原因。即使写了 WHERE a = ? AND b > ?,如果 a 列选择性极低(如全是 1),优化器可能放弃走主键索引,退化为全表扫描,导致行锁升级为表锁或大量无关行被锁定。
必须执行 EXPLAIN 验证:
EXPLAIN SELECT * FROM orders WHERE order_no = 'ORD001' AND created_at > '2026-08-01';
关键看三列:
-
key:必须显示复合主键名(如PRIMARY或你定义的idx_order_no_created); -
rows:值应明显小于表总行数,否则范围过大; -
Extra:不能出现Using where; Using filesort或Using temporary,这些意味着索引未被完全利用。
避免死锁的核心操作:拆分范围 + 固定扫描顺序
复合主键范围查询无法完全避免加锁,但能控制锁的粒度和行为。最有效的办法不是“不加锁”,而是“让所有事务按同一路径、同一节奏加锁”。
- 把大范围查询拆成小批次,例如每次查 100 行:
WHERE (a, b) > (?, ?) ORDER BY a, b LIMIT 100,用上一批最后一条的(a,b)值作为下一批起点; - 所有业务代码严格按
ORDER BY a, b排序后处理,禁止在事务内乱序更新或跳着处理; - 绝不混用等值 + 范围条件与纯范围条件:比如一个事务用
WHERE a = 1 AND b > 100,另一个用WHERE (a,b) > (1,100),InnoDB 内部加锁逻辑不同,极易冲突; - 批量更新时,先
SELECT ... FOR UPDATE获取主键列表,再按主键升序逐条UPDATE,确保加锁顺序绝对一致。
隔离级别与配置的务实取舍
对大多数互联网业务,REPEATABLE READ 下的间隙锁是复合主键范围查询死锁的根源。与其花大力气调优 SQL,不如评估是否真需要 RR。
- 确认 binlog 格式为
ROW(SHOW VARIABLES LIKE 'binlog_format';),这是降级前提; - 将业务连接默认隔离级别设为
READ COMMITTED:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;; - RC 下无间隙锁,范围查询只锁命中记录,死锁概率断崖下降;
- 代价是可能读到“不可重复读”,但订单状态变更、库存扣减等场景通常可接受,且比死锁更易观测和补偿。
真正难处理的不是“怎么选隔离级别”,而是 ORM 层自动拼接的动态条件(比如 MyBatis 的 <if>)导致相同业务逻辑在不同请求中生成不同 WHERE 子句,破坏了加锁顺序一致性——这种问题必须从 DAO 层统一约束,不能靠 DBA 配置兜底。


















