死锁是多个事务因锁顺序不一致互相等待而被数据库主动终止,关键在统一加锁顺序、减少锁粒度与持有时间,并通过监控将死锁变为可观测指标。

死锁不是事务执行失败,而是两个或多个事务互相等待对方持有的锁,形成循环依赖,数据库主动终止其中一个事务来打破僵局。关键不在“谁先执行”,而在“加锁顺序是否一致”。
看懂死锁图:定位冲突对象与等待链
SQL Server 的死锁图(Deadlock Graph)是 XML 格式可视化结果,核心看三部分:
- Victim Process:被选为牺牲者的进程,含 SPID、语句文本、等待资源类型(如 KEY、PAGE、OBJECT)
- Resource List:列出所有被争用的资源,重点关注 objectname(表名)、indexname(索引)、key(具体键值或哈希)
- Process List:每个参与事务的执行栈,注意 inputbuf(实际 SQL)、waitresource(等待什么)、owner-list(谁持有该资源)
典型模式:事务 A 持有表 T1 的行锁,等待 T2 的行锁;事务 B 持有 T2 的行锁,等待 T1 的行锁 → 形成 T1↔T2 循环。
锁顺序不一致是主因:从代码层统一访问次序
多数死锁源于应用逻辑中对多张表或同一表多行的更新顺序随机。例如:
- 订单服务先更新 orders 再更新 inventory
- 库存服务先更新 inventory 再更新 orders
解决方法不是加重试,而是约定全局顺序:
- 按字母顺序访问表:inventory → orders → log
- 对同一表多行更新,按主键升序排序后再执行:
UPDATE ... WHERE id IN (5,1,9) → 改为 WHERE id IN (1,5,9) - 在存储过程中显式用
SELECT ... WITH (UPDLOCK, HOLDLOCK)提前申请锁,避免后续 UPDATE 触发锁升级冲突
减少锁粒度与持有时间:让事务更“轻”
长事务 + 粗粒度锁 = 死锁温床。优化方向明确:
- 拆分大事务:把一个更新 1000 行的事务,改为每次 100 行 + 显式 COMMIT
- 避免在事务中调用外部服务、文件读写、长时间计算
- 用覆盖索引让 UPDATE/DELETE 走索引查找而非聚簇扫描,减少锁住的行数
- 确认隔离级别是否过度:READ COMMITTED 就够用时,不用 REPEATABLE READ
监控与预防:把死锁从事故变成指标
死锁不应只靠报错发现,而应纳入可观测体系:
- 开启 SQL Server 的 trace flag 1222 或使用 Extended Events 捕获死锁事件,自动解析并告警
- 定期查
sys.dm_tran_locks和sys.dm_exec_requests,识别长期持有锁的会话 - 在应用日志中结构化记录被终止事务的 SPID、SQL、影响行数,便于关联业务场景
- 压测阶段强制模拟高并发路径,验证锁顺序逻辑是否稳定
不复杂但容易忽略。










