主键范围删除能减少死锁,因其强制所有事务按主键升序一致加锁,消除循环等待;配合小批量、显式排序与应用层延时,可预测锁行为并压低死锁概率。

为什么主键范围删除能减少死锁
InnoDB 的行级锁在 DELETE 时会先加意向锁,再对匹配的聚簇索引记录加排他锁(X lock)。若多个事务按不同顺序扫描并锁住主键记录(比如一个按 id ASC、另一个按 id DESC 或带非主键条件),极易形成循环等待。而强制使用主键升序+分段范围删除,能让所有事务以完全一致的物理顺序加锁,从根本上消除锁序不一致这个死锁主因。
实际中常见错误是:用 WHERE status = 'pending' AND created_at 这类非主键条件删数据,即使有索引,优化器也可能走全表扫描或索引回表,导致锁住大量无关记录,且锁序不可控。
怎么写安全的主键范围删除语句
核心原则:只依赖 PRIMARY KEY(通常是 id)做单调递增切片,每次只删固定数量(如 1000 行),且必须带上 ORDER BY id ASC 和 LIMIT。
正确示例:
DELETE FROM orders WHERE id BETWEEN 100001 AND 101000 ORDER BY id ASC LIMIT 1000;
关键点:
- 必须显式写
ORDER BY id ASC—— 即使BETWEEN天然有序,MySQL 8.0+ 仍可能因优化器重排执行计划跳过排序,导致锁序不确定 -
LIMIT不可省略 —— 防止单次操作锁住过多行,放大阻塞时间 - 避免
IN (subquery)或嵌套查询 —— 容易触发临时表或物化,破坏锁粒度和顺序 - 范围起始值应来自上一批次的
MAX(id)结果,而非时间戳映射,确保无遗漏、无重复
如何配合应用层实现可靠分批删除 数据库层只是基础,真正防死锁靠的是应用控制节奏与状态。
推荐做法:
- 每次删除前查
SELECT id FROM orders WHERE id > ? ORDER BY id ASC LIMIT 1001,拿到本批 1000 个 ID 及下一批起点(第 1001 个id) - 用
DELETE ... WHERE id IN (..., ..., ...)删除这批 ID —— 显式列表比范围更精确,且 MySQL 对IN列表中的主键值会自动按升序排序加锁 - 每批次后加
SLEEP(0.1)(应用层延时),缓解锁竞争高峰;不要依赖数据库SLEEP()函数,它会持锁等待 - 记录已处理的最大
id到外部存储(如 Redis 或专用 checkpoint 表),支持中断恢复
容易被忽略的隐性陷阱
最常踩的坑不是语法,而是隔离级别和二级索引维护带来的额外锁:
-
READ COMMITTED下虽然不加间隙锁(gap lock),但若表上有唯一二级索引,DELETE仍会对该索引对应条目加 X 锁 —— 若并发删不同主键但命中同一二级索引值(如user_id相同),依然可能死锁 - 没加
ORDER BY的LIMIT删除,在 MySQL 5.7 中默认不保证顺序,可能跨批次锁住“跳跃”的主键,破坏一致性 - 用
pt-archiver工具时,默认按主键删,但它内部用SELECT ... FOR UPDATE+DELETE两阶段,若中间有其他事务更新了这些行,会导致DELETE匹配不到行数,需检查返回影响行数并重试


















