Index Merge引发的死锁本质是优化器动态选择多索引路径导致加锁顺序不一致,形成循环等待;根本解法是用联合索引或FORCE INDEX统一访问路径,而非调隔离级别或关优化器。

Index Merge 引发的死锁不是配置错误,也不是 SQL 写错了,而是 MySQL 优化器在多个索引间“自由组合”时,不同事务加锁顺序不一致导致的循环等待。它常出现在 WHERE 条件含多个独立索引字段(如 status = 0 AND trans_id = 'xxx'),且两个字段各自有单列索引的场景。
Index Merge 死锁的本质是加锁顺序不可控
MySQL 在启用 index_merge 时,可能先走 idx_status 扫描匹配行,再回主键加锁;另一事务却先走 idx_trans_id,再回主键。两者对同一行的主键锁和二级索引锁申请顺序相反,就构成经典“事务 A 持有锁 a 等锁 b,事务 B 持有锁 b 等锁 a”。
- 这种加锁顺序由优化器动态决定,无法靠业务代码控制执行路径
- 即使两个事务执行完全相同的 SQL,只要并发稍有差异,就可能触发不同索引扫描顺序
-
READ COMMITTED和REPEATABLE READ都会发生,调低隔离级别不能解决该问题 —— 因为死锁根源是索引访问路径分裂,不是间隙锁范围扩大
如何禁用或绕过 index_merge 优化
禁用不是目的,关键是让优化器“只走一条路”,从而统一加锁顺序:
-
使用
FORCE INDEX显式指定主索引UPDATE t SET status = 1 WHERE status = 0 AND trans_id = 'xxx' FORCE INDEX (idx_trans_id);
这样强制走trans_id索引,避免合并,加锁路径唯一 -
创建覆盖查询条件的联合索引,比单列索引优先级更高
例如:已有KEY idx_status (status)和KEY idx_trans_id (trans_id),应删掉二者,新建KEY idx_status_trans_id (status, trans_id)
或更优(如果trans_id唯一性高):KEY idx_trans_id_status (trans_id, status)
联合索引能直接定位行,无需index_merge,也省去回表开销 -
关闭
index_merge全局开关(仅限确认无其他依赖场景)SET GLOBAL optimizer_switch = 'index_merge=off,index_merge_union=off,index_merge_sort_union=off,index_merge_intersection=off';
⚠️ 注意:该设置需重启连接才对新会话生效,且会影响所有使用多条件扫描的查询,不推荐线上随意关闭
为什么不能靠调整隔离级别解决
-
READ COMMITTED下虽然间隙锁退化,但index_merge导致的多索引加锁顺序混乱问题依然存在 -
REPEATABLE READ下还额外叠加间隙锁,让锁粒度更大、冲突面更广,但根本矛盾仍是“哪个索引先加锁” - 死锁日志里能看到两个事务分别在
idx_status和idx_trans_id上互相等待,和隔离级别无关
关键点在于:index_merge 是优化器的“自由发挥”,而死锁需要确定性。
你没法靠改配置让它的行为变可靠,只能用联合索引、FORCE INDEX 或业务层拆解(比如先查 ID 再按主键更新)把它“管住”。最容易被忽略的是——即使 SQL 看起来简单,只要 where 里有多个可走索引的等值条件,又没联合索引兜底,index_merge 就可能在后台悄悄上线并埋雷。


















