You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SQL事务死锁排查:Web API批量处理引发触发器死锁问题

死锁问题分析与解决方案

看起来你遇到了典型的「低负载稳定运行,高负载触发死锁」的场景,结合你提供的代码、死锁图和排查过程,咱们来一步步拆解问题并给出修复方案:

问题背景回顾

你的PurchaseController原本处理100条以内的采购数据毫无压力,当数据量突增至600条后出现超时,于是你修改代码将请求拆分为可配置批次,并调整事务粒度,但修改后频繁触发SQL死锁:

Transaction (Process ID 62) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

将事务移至foreach循环内后死锁频率降低,但并未彻底解决,最终通过死锁图定位到Event_Registration表的触发器是核心问题。

死锁根源分析

从你提供的死锁图可以清晰看到:两个进程都卡在触发器的更新语句上,形成了循环等待:

  • 进程54持有PAGE: 6:1:84432的U锁,等待进程90持有的PAGE: 6:1:1848653的U锁
  • 进程90持有PAGE: 6:1:1848653的IX锁,等待进程54持有的PAGE: 6:1:84432的U锁

原触发器的逻辑存在致命缺陷:

UPDATE [Event_Registration] SET event_number = UPPER(event_number) WHERE event_number in (select event_number from inserted);

这条语句会扫描整个Event_Registration表(如果event_number没有覆盖索引),并锁定所有匹配该event_number的行所在的数据页面。在低数据量时,扫描范围小,锁冲突概率极低;但当数据量暴增后,每次触发器执行都会锁定大量页面,高并发下多个事务交叉锁定不同页面,就会形成循环等待,触发死锁。

具体修复方案

1. 重构触发器逻辑(核心修复)

原触发器的问题是更新范围过大,我们应该只针对本次插入/更新的具体行进行处理,而不是所有匹配event_number的行。修改后的触发器如下:

ALTER Trigger [dbo].[tr_event_registration] on [dbo].[Event_Registration] 
AFTER insert, update 
as 
if ((trigger_nestlevel() > 1) or (@@rowcount = 0)) 
    return;

-- 仅更新本次操作涉及的行,缩小锁范围
UPDATE er
SET er.event_number = UPPER(er.event_number)
FROM [Event_Registration] er
INNER JOIN inserted i 
    ON er.entity_number = i.entity_number 
    AND er.event_number = i.event_number;

这样修改后,触发器只会锁定当前事务操作的具体行,而不是扫描全表并锁定大量页面,从根源上消除了锁冲突的基础。

2. 补充索引优化

为Event_Registration表添加复合索引:

CREATE NONCLUSTERED INDEX IX_Event_Registration_Entity_Event ON [dbo].[Event_Registration]
(
    entity_number ASC,
    event_number ASC
)
INCLUDE (-- 如果触发器或业务有其他需要返回的列,可以添加在这里);

这个索引能让触发器的UPDATE语句快速定位到目标行,避免全表扫描,进一步缩短锁的持有时间。

3. 事务与批量处理的兜底优化

  • 保持事务在foreach循环内部的设计,确保每个采购项的事务粒度尽可能小,减少锁的持有时间
  • 添加死锁重试逻辑,作为兜底方案,当捕获到死锁异常时自动重试:
int maxRetries = 3;
int currentRetry = 0;
bool success = false;

while (currentRetry < maxRetries && !success)
{
    try
    {
        using (var dbTransaction = Members.Database.BeginTransaction())
        {
            // 处理单个Purchase的业务逻辑
            response = base.RecordEventRegistrations(Purchase.EventRegistrations);
            if (!response.IsSuccess) throw new Exception(response.Error);
            // ... 其他RecordXXX方法调用

            dbTransaction.Commit();
            success = true;
        }
    }
    catch (SqlException ex)
    {
        // 死锁错误号为1205
        if (ex.Number == 1205)
        {
            currentRetry++;
            // 指数退避等待,避免立即重试再次冲突
            Thread.Sleep(100 * (int)Math.Pow(2, currentRetry - 1));
        }
        else
        {
            // 非死锁异常,直接抛出
            throw;
        }
    }
}

if (!success)
{
    response.IsSuccess = false;
    response.Error = "多次重试后仍无法完成操作,请稍后再试";
}

验证建议

你已经验证了禁用触发器后死锁消失,现在应用上述优化后的触发器和索引,应该能彻底解决死锁问题,同时保留event_number转为大写的业务规则。可以先在测试环境模拟高并发场景验证,再部署到生产环境。


内容的提问来源于stack exchange,提问作者Aaron S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:59:47