MERGE语句在高并发下极易死锁,因其执行计划不可控、锁顺序依赖物理数据分布且默认未启用优化锁定;应优先拆分为有序的INSERT+UPDATE+DELETE,并配合适当索引、RCSI和OPTIMIZED_LOCKING配置。

MERGE 语句本身不保证线程安全,它在高并发下极易触发死锁,尤其当多路更新顺序不一致或缺少覆盖索引时。 SQL Server 2019 默认不启用优化锁定(Optimized Locking),MERGE 的 UPDATE 和 INSERT 分支仍按传统方式逐行加锁,若两个并发 MERGE 操作以不同顺序扫描/修改同一张表的重叠行集,死锁几乎必然发生。
为什么 MERGE 在并发场景下比分开写 INSERT + UPDATE 更容易死锁
根本原因在于 MERGE 的执行计划不可控性更强:它可能先走 UPDATE 分支再尝试 INSERT,也可能因统计信息偏差选择全表扫描+排序匹配,导致锁获取顺序与物理数据分布强耦合。而手写的 UPDATE / INSERT 可明确控制访问路径和顺序。
-
MERGE内部会同时持有源表扫描锁、目标表的意向锁(IX)以及实际更新行的排他锁(X),锁生命周期覆盖整个语句执行期 - 若两个并发
MERGE分别命中同一张表中不同但有交集的行集(例如都查WHERE status = 0),且各自按不同索引路径定位——一个走idx_status,一个走PRIMARY KEY——就极易形成“事务A持idx_status锁等PK锁,事务B反之”的循环 - SQL Server 2019 不默认启用
OPTIMIZE_FOR_SEQUENTIAL_KEY或优化锁定(需显式为数据库开启),无法自动压缩锁持有时间
必须加的索引:让 MERGE 只走一条确定路径
没有合适索引的 MERGE 几乎等于邀请死锁。关键不是“有没有索引”,而是“是否能让所有并发 MERGE 以完全一致的顺序访问和加锁同一组行”。
- 确保
ON子句中的所有匹配列(如ON t.id = s.id AND t.version = s.version)组成一个**最左前缀可覆盖的复合索引**,例如CREATE INDEX ix_merge_on ON target_table (id, version) - 如果
WHEN MATCHED后有UPDATE,且SET中修改了被索引的列(如status),则该列不能出现在索引键中——否则会导致锁升级;应改用包含列:INCLUDE (status) - 避免让
MERGE回到堆表或聚簇索引扫描:检查执行计划中是否出现Clustered Index Scan或Table Scan,出现即为高危信号
替代方案:拆成 INSERT + UPDATE + DELETE,显式控制顺序
多数业务场景下,放弃 MERGE 是更稳妥的选择。用三段独立语句 + 显式事务 + 统一访问顺序,死锁率可下降 80% 以上。
- 先
UPDATE:只更新已存在且满足条件的行,WHERE条件必须走索引,且所有实例都按同一字段排序(如ORDER BY id ASC) - 再
INSERT:用NOT EXISTS或LEFT JOIN排除已更新行,同样强制按id ASC插入(即使没显式写ORDER BY,也要确保执行计划里是索引有序插入) - 最后
DELETE(如需):同理,固定顺序处理 - 全程使用
WITH (UPDLOCK, HOLDLOCK)在SELECT或UPDATE前锁定候选行范围,防止其他事务插进来——但注意这会增加阻塞,仅在必要时用
必须启用的数据库级配置
SQL Server 2019 需手动打开两项关键能力,否则任何应用层优化都事倍功半:
- 启用读已提交快照隔离(RCSI):
ALTER DATABASE [YourDB] SET READ_COMMITTED_SNAPSHOT ON—— 让SELECT不阻塞UPDATE,大幅降低读写冲突概率 - 启用优化锁定(Optimized Locking):
ALTER DATABASE [YourDB] SET OPTIMIZED_LOCKING ON—— 这是 SQL Server 2019 新增特性,能让MERGE中的每行更新后立即释放行锁,只保留一个事务级 TID 锁,从根本上缩短锁持有窗口 - 禁用锁升级(可选但推荐):
ALTER TABLE target_table SET (LOCK_ESCALATION = DISABLE)—— 防止大量行更新时升级为表锁,扩大冲突面
真正难的不是写出能跑的 MERGE,而是让所有并发请求对同一张表的同一类操作,始终以完全相同的物理顺序触达、加锁、释放。索引设计、隔离级别、语句结构、数据库配置,四者缺一不可——漏掉任意一环,死锁就会在流量高峰准时出现。










