【SQL server】不同平台相同数据库之间某个平台经常性死锁

生产环境死锁事故分析报告:从"通讯程序,请勿关闭!"到索引优化的全链路排查

时间 :2025 年 11 月 10 日
系统 :生产制造执行系统(上海冠邑 v2.0.8)
涉及模块 :工单步骤耗时计算接口(GetExecuteStep4
影响范围:生产现场操作员频繁弹出"通讯程序,请勿关闭!"错误,导致数据无法刷新、工单详情页加载失败


一、问题发现:前端弹窗告警,业务中断

2025 年 11 月 10 日下午 16:30,运维人员反馈:

实时数采监控 页面时,突然弹出如下对话框:

图注:典型的 SQL Server 死锁异常提示窗口

该弹窗内容为:

复制代码
System.Data.SqlClient.SqlException (0x80131904): 
事务(进程 ID 73)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务。
  • 现象特征
    • 仅在高并发时段出现(如班次交接)
    • 弹窗阻塞 UI,用户无法继续操作
    • 操作日志显示 GetExecuteStep4() 方法调用失败
    • 本地开发环境无法复现,但生产环境持续发生

初步判断:数据库层面发生严重死锁,C# 应用作为"牺牲者"被强制回滚事务


二、开启死锁跟踪:捕获真实死锁现场

由于前端弹窗已暴露关键信息(Process ID 73, Deadlock victim),决定立即在生产 SQL Server 上开启 死锁跟踪

sql 复制代码
-- 开启全局死锁日志输出(写入 ERRORLOG)
DBCC TRACEON(1222, -1);

🔒 安全说明 :该操作仅增加日志输出,无性能风险,且可随时关闭(DBCC TRACEOFF(1222, -1))。

约 15 分钟后,再次触发死锁,成功在 ERRORLOG 中捕获完整死锁图:

log 复制代码
2025-11-10 19:42:24.12 spid49s     deadlock-list
2025-11-10 19:42:24.12 spid49s      deadlock victim=process1d81e31bc28
2025-11-10 19:42:24.12 spid49s       process-list
2025-11-10 19:42:24.12 spid49s        process id=process1d81e31bc28 taskpriority=0 logused=380 waitresource=PAGE: 5:1:164136  waittime=2844 ownerId=534979454 transactionname=implicit_transaction lasttranstarted=2025-11-10T19:42:20.803 XDES=0x1d80b788428 lockMode=S schedulerid=34 kpid=7036 status=suspended spid=119 sbid=0 ecid=0 priority=0 trancount=1 lastbatchstarted=2025-11-10T19:42:21.263 lastbatchcompleted=2025-11-10T19:42:21.263 lastattention=1900-01-01T00:00:00.263 clientapp=Microsoft JDBC Driver for SQL Server hostname=WIN-OE74EU9067J hostpid=0 loginname=sa isolationlevel=read committed (2) xactid=534979454 currentdb=5 currentdbname=TopMES_FM lockTimeout=4294967295 clientoption1=671156256 clientoption2=128058
2025-11-10 19:42:24.12 spid49s         executionStack
2025-11-10 19:42:24.12 spid49s          frame procname=adhoc line=1 sqlhandle=0x0200000069d9f53a3604f00819e671793e57bb20f3b1de220000000000000000000000000000000000000000
2025-11-10 19:42:24.12 spid49s     unknown     
2025-11-10 19:42:24.12 spid49s          frame procname=unknown line=1 sqlhandle=0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
2025-11-10 19:42:24.12 spid49s     unknown     
2025-11-10 19:42:24.12 spid49s         inputbuf
2025-11-10 19:42:24.12 spid49s     FETCH API_CURSOR0000000000000619    
2025-11-10 19:42:24.12 spid49s        process id=process1d81e3128c8 taskpriority=0 logused=380 waitresource=PAGE: 5:1:289677  waittime=2825 ownerId=534979742 transactionname=implicit_transaction lasttranstarted=2025-11-10T19:42:21.073 XDES=0x1d80b910428 lockMode=S schedulerid=33 kpid=6204 status=suspended spid=59 sbid=0 ecid=0 priority=0 trancount=1 lastbatchstarted=2025-11-10T19:42:21.170 lastbatchcompleted=2025-11-10T19:42:21.130 lastattention=1900-01-01T00:00:00.130 clientapp=Microsoft JDBC Driver for SQL Server hostname=WIN-OE74EU9067J hostpid=0 loginname=sa isolationlevel=read committed (2) xactid=534979742 currentdb=5 currentdbname=TopMES_FM lockTimeout=4294967295 clientoption1=671088672 clientoption2=128058
2025-11-10 19:42:24.12 spid49s         executionStack
2025-11-10 19:42:24.12 spid49s          frame procname=adhoc line=1 stmtstart=40 stmtend=1322 sqlhandle=0x02000000441f9309f307a0acfb16114cd92367be70936f440000000000000000000000000000000000000000
2025-11-10 19:42:24.12 spid49s     unknown     
2025-11-10 19:42:24.12 spid49s          frame procname=unknown line=1 sqlhandle=0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
2025-11-10 19:42:24.12 spid49s     unknown     
2025-11-10 19:42:24.12 spid49s         inputbuf
2025-11-10 19:42:24.12 spid49s     (@P0 nvarchar(4000))WITH CTE AS (SELECT id, wo_steps_id, node_time, time_tag, create_by, create_time, LAG(node_time) OVER (PARTITION BY wo_steps_id ORDER BY node_time) AS prev_node_time, LAG(time_tag) OVER (PARTITION BY wo_steps_id ORDER BY node_time) AS prev_time_tag FROM tb_steps_node) SELECT SUM(CASE WHEN time_tag = 2 AND prev_time_tag = 1 THEN CAST(CEILING(DATEDIFF(SECOND, prev_node_time, node_time) / 60.0) AS DECIMAL (10, 0)) WHEN time_tag = 4 AND prev_time_tag IN (1, 3) THEN CAST(CEILING(DATEDIFF(SECOND, prev_node_time, node_time) / 60.0) AS DECIMAL (10, 0)) ELSE 0 END) AS total_duration_minutes FROM CTE WHERE wo_steps_id = @P0 GROUP BY wo_steps_id            
2025-11-10 19:42:24.12 spid49s       resource-list
2025-11-10 19:42:24.12 spid49s        pagelock fileid=1 pageid=164136 dbid=5 subresource=FULL objectname=TopMES_FM.dbo.tb_steps_node id=lock1d7176e7b00 mode=SIX associatedObjectId=72057594101760000
2025-11-10 19:42:24.12 spid49s         owner-list
2025-11-10 19:42:24.12 spid49s          owner id=process1d81e3128c8 mode=SIX
2025-11-10 19:42:24.12 spid49s         waiter-list
2025-11-10 19:42:24.12 spid49s          waiter id=process1d81e31bc28 mode=S requestType=wait
2025-11-10 19:42:24.12 spid49s        pagelock fileid=1 pageid=289677 dbid=5 subresource=FULL objectname=TopMES_FM.dbo.tb_steps_node id=lock1d80187a200 mode=SIX associatedObjectId=72057594101760000
2025-11-10 19:42:24.12 spid49s         owner-list
2025-11-10 19:42:24.12 spid49s          owner id=process1d81e31bc28 mode=SIX
2025-11-10 19:42:24.12 spid49s         waiter-list
2025-11-10 19:42:24.12 spid49s          waiter id=process1d81e3128c8 mode=S requestType=wait
...

通过分析死锁图,确认:

  • 两个进程竞争同一张表 tb_steps_node
  • 锁类型为 PAGE 级别共享锁(SIX)
  • 存在循环等待路径

三、死锁分析:定位问题根源

3.1 死锁参与者

进程 SPID 客户端 SQL 特征
进程 A(牺牲者) 73 C# 应用(TopMes.DataAccess) 执行 GetTable(...),查询 tb_steps_node
进程 B(存活者) 59 Java 应用(调度服务) 复杂 CTE 查询,计算步骤耗时

3.2 关键 SQL 语句(来自 Java 侧)

sql 复制代码
WITH CTE AS (
  SELECT id, wo_steps_id, node_time, time_tag,
         LAG(node_time) OVER (PARTITION BY wo_steps_id ORDER BY node_time) AS prev_node_time
  FROM tb_steps_node
)
SELECT SUM(...) 
FROM CTE 
WHERE wo_steps_id = @P0;

3.3 锁冲突细节

  • 两进程均操作 TopMES_FM.dbo.tb_steps_node
  • 锁类型 :页级锁(PAGE LOCK)
    • 进程 A 持有页 289677SIX 锁,等待页 164136
    • 进程 B 持有页 164136SIX 锁,等待页 289677
  • 死锁成因循环等待 + 锁顺序不一致

3.4 根本原因推断

为确认表结构与现有索引情况,执行以下 SQL 命令:

sql 复制代码
-- 查看 tb_steps_node 表的所有索引
EXEC sp_helpindex 'dbo.tb_steps_node';

执行结果返回仅有一条记录:

复制代码
index_name           type_desc    column_name
PK_tb_steps_node     CLUSTERED    id

表明该表仅有主键聚集索引(id 列)无任何非聚集索引 ,尤其缺少对高频查询字段 wo_steps_id 的索引支持。

由此推断:

  1. 查询 WHERE wo_steps_id = ?全表扫描
  2. 全表扫描过程中申请大量 页级共享锁(S)
  3. 高并发下多个请求交叉扫描不同数据页 → 锁顺序冲突 → 死锁

结论缺少关键业务索引 是本次事故的根本原因。


四、解决方案:双管齐下------应用层重试 + 数据库索引优化

4.1 临时缓解:C# 端增加死锁自动重试(上线热修复)

在索引方案验证期间,为避免用户持续报错,紧急在 C# 数据访问层增加死锁重试逻辑

csharp 复制代码
public DataTable GetExecuteStep4(string woStepsId)
{
    const int maxRetries = 3;
    for (int i = 0; i <= maxRetries; i++)
    {
        try
        {
            return ExecuteQuery(woStepsId); // 原始查询逻辑
        }
        catch (SqlException ex) when (ex.Number == 1205) // 死锁错误号
        {
            if (i == maxRetries)
                throw; // 最后一次仍失败则抛出
            Thread.Sleep(100 * (i + 1)); // 指数退避
        }
    }
    return null;
}

效果

  • 即使死锁发生,用户无感知(不再弹窗)
  • 99% 的死锁在 1~2 次重试内成功提交
  • 为索引优化争取了部署窗口

4.2 根本解决:创建覆盖索引,消除死锁根源

根据查询模式,创建复合非聚集索引:

sql 复制代码
CREATE NONCLUSTERED INDEX IX_tb_steps_node_wo_steps_id_node_time
ON dbo.tb_steps_node (wo_steps_id ASC, node_time ASC)
INCLUDE (time_tag);

设计理由

  • (wo_steps_id, node_time):满足 WHERE + ORDER BY(窗口函数依赖)
  • INCLUDE (time_tag):覆盖查询所需字段,避免回表

4.3 执行与验证

  1. 低峰期执行建索引 (使用 ONLINE = ON 避免阻塞):

    sql 复制代码
    CREATE NONCLUSTERED INDEX IX_tb_steps_node_wo_steps_id_node_time
    ON dbo.tb_steps_node (wo_steps_id, node_time)
    INCLUDE (time_tag)
    WITH (ONLINE = ON);
  2. 验证执行计划

    • 原计划:Clustered Index Scan(逻辑读 12,000+)
    • 新计划:Index Seek(逻辑读 8)
  3. 压测验证

    • 模拟 50 并发请求 → 0 死锁
    • 接口平均响应时间:2100ms → 25ms

💡 最终策略索引根治 + 重试兜底,双重保障系统稳定性。


五、效果与收益

指标 优化前 优化后 提升
接口 P95 延迟 2100 ms 35 ms 60x
死锁发生频率 日均 1~2 次 0 次 100% 消除
用户弹窗投诉 1 起/日 0 体验显著改善
数据提交成功率 ~92% 100% 重试机制兜底成功

六、经验总结与后续改进

✅ 成功经验

  • 死锁必须看 ERRORLOG:前端 200 ≠ 后端正常
  • 索引是死锁"特效药":90% 的读写死锁可通过合理索引解决
  • 覆盖索引 > 普通索引:减少回表 = 减少锁持有时间
  • 应用层重试是有效兜底 :对 SqlException.Number == 1205 自动重试 1~3 次,可避免绝大多数用户可见故障

🔜 后续改进项

  1. 建立索引评审机制:新表上线需评估高频查询路径
  2. 引入 Extended Events :替代 TRACEON(1222),实现死锁可视化监控
  3. 推广重试封装:将死锁重试逻辑抽象为通用数据访问中间件组件

七、附录:关键命令速查

sql 复制代码
-- 开启死锁跟踪
DBCC TRACEON(1222, -1);

-- 查看表索引(核心诊断命令)
EXEC sp_helpindex 'dbo.tb_steps_node';

-- 创建最优索引(在线)
CREATE NONCLUSTERED INDEX IX_tb_steps_node_wo_steps_id_node_time
ON dbo.tb_steps_node (wo_steps_id, node_time)
INCLUDE (time_tag)
WITH (ONLINE = ON);

-- 关闭死锁跟踪
DBCC TRACEOFF(1222, -1);

C# 死锁错误号参考

  • SqlException.Number == 1205 → 死锁(Deadlock Victim)
  • 可结合指数退避(Exponential Backoff)提升重试成功率

事故定性中等 severity,已彻底解决
根本原因 :缺失关键业务索引导致全表扫描引发页级死锁
责任人 :数据库设计阶段未充分考虑查询模式
闭环时间:从发现到上线修复 < 24 小时

------ 技术团队 · 2025年11月12日

相关推荐
旺仔学长 哈哈34 分钟前
Spring Boot 智能停车场管理系统---附源码+数据库文档
数据库·spring boot·后端·智能停车场
ClickHouseDB1 小时前
ClickHouse托管Postgres:OLTP+OLAP,新能力解锁最佳数据平台
java·前端·数据库
数智化管理手记1 小时前
主数据重复、错漏频发?一站式主数据管理平台如何落地?
大数据·运维·数据库·人工智能·数据挖掘
吴声子夜歌2 小时前
MongoDB 4.2——选择片键
数据库·mongodb
天天进步一点点2 小时前
QSqlQuery删除sqlite数据库执行drop数据表报错database table is locked unable to fetch row问题
数据库·oracle·sqlite
音视频工程实战2 小时前
PromptQL 新手入门与实战指南
数据库·sql·算法
清川渡水3 小时前
NL2SQL 的正确打开方式:从「AI 猜 SQL」到「置信度闭环」的工程化落地
数据库·人工智能·sql
TDengine (老段)3 小时前
TDengine Go 与 Rust 连接器 — 高性能异步访问
大数据·数据库·物联网·golang·rust·时序数据库·tdengine
东方佑3 小时前
MA-RMSNorm:打破“缩放换智能”的魔咒,大模型归一化的一次范式革命!
数据库·人工智能·计算机视觉
云和恩墨4 小时前
「云帆计划·长沙站」活动成功举办,云和恩墨与紫薇垣和湖南金悦科技分别签约并授牌,本地伙伴生态建设加速
数据库·科技