降序索引本身不直接导致死锁,真正原因是order by desc + limit + for update组合引发相反加锁顺序;显式desc索引会改变优化器选路,使物理扫描方向与asc索引相反,加剧事务间循环等待风险。

降序索引本身不会直接导致死锁
MySQL 8.0+ 支持显式 DESC 索引定义(如 CREATE INDEX idx ON t(a DESC, b ASC)),但 InnoDB 的行锁机制并不区分“升序扫描”和“降序扫描”——锁是加在索引记录(record)或间隙(gap)上的,与 B+ 树遍历方向无关。所谓“降序索引更容易死锁”,实际是**查询执行计划因降序索引改变而引发的锁顺序不一致问题**,根源不在索引方向,而在优化器选中了不同范围、不同顺序的锁路径。
真正触发死锁的是 ORDER BY + LIMIT + FOR UPDATE 的组合
当 SQL 同时包含 ORDER BY ... DESC、LIMIT 和 FOR UPDATE(或 SELECT ... LOCK IN SHARE MODE)时,InnoDB 必须按排序顺序逐行加锁,直到取够 LIMIT 行。若两个事务分别执行:
SELECT * FROM t WHERE a > 100 ORDER BY a DESC LIMIT 1 FOR UPDATE;SELECT * FROM t WHERE a > 100 ORDER BY a ASC LIMIT 1 FOR UPDATE;
它们可能以相反顺序扫描同一组索引记录:一个从高往低锁,一个从低往高锁,极易形成“事务 A 锁住 record_X 后等待 record_Y,事务 B 已锁 record_Y 并等待 record_X”的循环等待。这种场景下,即使没有显式 DESC 索引,仅靠 ORDER BY ... DESC + 范围条件就足以触发。
为什么显式 DESC 索引会让问题更隐蔽
显式降序索引会改变优化器对“范围扫描起始点”的判断,进而影响加锁顺序:
- 无
DESC索引时,WHERE a > 100 ORDER BY a DESC只能走a升序索引,需先定位到第一个a > 100的位置,再反向遍历 —— 此时加锁顺序仍是物理存储顺序(即升序),但逻辑上“从大到小”; - 有
INDEX(a DESC)时,优化器可能选择该索引,直接从 B+ 树右侧叶子节点开始正向扫描(因为 DESC 索引的物理布局是倒序的),此时加锁顺序变成“物理倒序”,与其他事务用升序索引扫描同一范围时的加锁顺序天然相反; - 更危险的是:ORM 或动态 SQL 可能混用
ASC/DESC排序,而开发人员未意识到底层索引已不同,导致多处业务代码对同一张表的同一批数据以相反顺序加锁。
排查和规避的关键动作
遇到疑似由降序索引引发的死锁,优先做这三件事:
- 用
SHOW ENGINE INNODB STATUS\G查看LATEST DETECTED DEADLOCK,确认两个事务的 SQL 是否都含ORDER BY ... DESC或是否一个用了DESC索引、另一个没用; - 用
EXPLAIN FORMAT=tree(MySQL 8.0.16+)对比两条 SQL 的执行计划,重点看access_type、rows_examined和是否使用了DESC索引; - 统一业务逻辑:所有对同一业务实体(如订单、账户)的排他性查询,强制使用相同排序方向(推荐统一用
ASC)+ 相同索引策略,避免混合使用ASC/DESC索引路径。
最常被忽略的一点:降序索引不是“性能开关”,而是“锁行为开关”——它不提速,只改加锁顺序。上线前必须验证并发压测下的锁等待图,不能只看单条 SQL 的 EXPLAIN。











