SQL MERGE易死锁因隐式“读+写”两阶段及加锁顺序不一致:index_merge导致多索引加锁顺序冲突、非唯一索引引发间隙锁重叠、优化器路径不可控;须禁用index_merge、建覆盖联合索引、优先用主键匹配并显式ORDER BY预锁。

SQL MERGE 本身不直接导致死锁,但它的执行逻辑在高并发下极易触发死锁——根本原因是它隐式包含“读+写”两阶段操作,且加锁顺序依赖查询路径,而不同事务可能因索引选择、数据分布或优化器决策走不同加锁路径。
为什么 MERGE 比普通 UPDATE 更容易死锁
MERGE 在语义上等价于“先查再判再插/更”,InnoDB 必须在同一个事务内完成:查找匹配行(SELECT)、判断是否存在(可能触发间隙锁)、然后执行 INSERT 或 UPDATE。这个过程涉及多把锁,且顺序不透明:
-
MERGE的查找阶段可能使用index_merge(尤其当 WHERE 条件含多个非主键字段且各自有单列索引时),导致对多个二级索引分别加锁,再回表加主键锁——加锁顺序是“索引A→索引B→主键”,而另一个事务若走“索引B→索引A→主键”,就构成循环等待 - 即使没用
index_merge,若匹配条件落在非唯一索引上(如WHERE status = 'pending' AND tenant_id = 123),InnoDB 会对整个索引区间加Gap Lock或Next-Key Lock,范围重叠时多个事务互相阻塞 - ORM 或中间件生成的
MERGE往往未显式指定ORDER BY或FOR UPDATE,导致优化器自由选择访问路径,同一 SQL 在不同时间可能走不同索引,加锁行为不可控
MySQL 中 MERGE 死锁的典型现场特征
查 SHOW ENGINE INNODB STATUS\G 的 LATEST DETECTED DEADLOCK 段,常见模式是:
- 两个事务都显示持有
RECORD LOCKS在不同二级索引上(如idx_status和idx_tenant_id),又都在WAITING FOR对方持有的另一把索引锁 - 事务堆栈中出现类似
insert into ... on duplicate key update ...或REPLACE INTO(它们底层被当作MERGE类语义处理) - 锁类型混杂:一个事务持
INSERT_INTENTION锁等待Gap Lock,另一个持Gap Lock等待INSERT_INTENTION,形成经典“意向锁 vs 间隙锁”互等
真正有效的规避手段(不是重试)
靠应用层捕获 ERROR 1213 (40001): Deadlock 后重试只是兜底,治标不治本。关键要从执行路径上消除不确定性:
- 禁用
index_merge:在会话或全局设置optimizer_switch='index_merge=off',强制优化器走联合索引或主键,让加锁顺序收敛 - 为
MERGE的匹配条件创建**覆盖型联合索引**,确保查找只走一棵 B+ 树——例如WHERE a = ? AND b = ?就建INDEX idx_a_b (a, b),避免回表和多索引扫描 - 用主键或唯一键做匹配:把
MERGE ... ON (non_unique_col)改成ON (id),因为唯一键查找只加一把锁(二级索引项 + 主键项),不会产生范围锁 - 显式加锁控制顺序:如果业务允许,先用
SELECT ... FOR UPDATE ORDER BY id ASC预占锁,再执行UPDATE或INSERT,保证所有事务按同一物理顺序加锁
最常被忽略的一点:MERGE 的死锁往往不是“代码写错了”,而是“索引没建对”或“优化器被纵容了”。一旦出现,优先检查 EXPLAIN FORMAT=TRADITIONAL 输出里的 type 和 key 字段,而不是急着加重试逻辑。










