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

SaveChanges()不由事务管理吗?不同systemId并行更新为何触发死锁

首先明确:死锁并不只有读写同一行数据才会触发,只要两个事务互相持有对方需要的锁资源,且陷入无限等待,就会触发死锁。即使你每次操作的是不同systemId的记录,也可能因为以下原因出现死锁:


死锁触发原因

  • 无有效索引导致全表扫描、锁范围过大
    如果PartnersUpdate表没有针对SystemId、Status、UpdateAt的联合索引,SQL执行你的筛选查询时会走全表扫描,扫描过程中会给所有路过的行加共享锁,哪怕这些行不属于你当前操作的systemId。此时不同并行任务扫到同一批行时,A任务持有某行的共享锁要升级为排他锁做更新,B任务也持有这行的共享锁要升级排他锁,就会触发死锁。
  • IQueryable延迟执行拉长锁持有时间
    你代码中的partners是IQueryable类型,直到foreach遍历的时候才会真正执行SQL查询,查询过程中数据库持有锁的时间和你遍历处理业务逻辑的时间绑定,大大提升了锁冲突的概率。
  • 锁获取顺序不一致
    你的代码先查vPartnersSystems,再查并更新PartnersUpdate,如果底层表的访问顺序在不同执行计划中出现颠倒,或者不同任务的锁资源获取顺序相反,哪怕操作的行没有重叠也会触发死锁。
  • 锁升级问题
    当单次操作的行数较多时,SQL会把行锁升级为表锁,直接导致不同systemId的任务抢同一把表锁,必然会出现死锁冲突。

解决方案

  1. 新增覆盖索引
    给PartnersUpdate表创建联合索引:IX_PartnersUpdate_SystemId_Status_UpdateAt,包含列PartnerId,让你的筛选、排序、取PartnerId的操作完全走索引,避免全表扫描,锁只会加在当前systemId对应的行上。同时给vPartnersSystems视图依赖的底层表也新增SystemId、Id、Validity的联合索引,减少扫描范围。
  2. 提前物化查询结果
    把partners的查询末尾加.ToList(),一次性把所有要处理的数据拉到内存,减少数据库锁的持有时间,避免边遍历边查:
var partners = ctx.PartnersUpdate
    .Where(w => w.SystemId.Equals(systemId) && statusToEvaluate.Contains(w.Status))
    .OrderByDescending(p => p.UpdateAt)
    .Take(500)
    .ToList();
  1. 开启快照隔离级别
    在SQL Server数据库开启「读已提交快照隔离(RCSI)」,读操作不会再加共享锁,不会和写操作的排他锁冲突,从根源上降低90%以上的死锁概率。
  2. 增加死锁重试机制
    给Hangfire任务加[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)]特性,死锁属于临时可重试错误,重试即可解决偶发问题。
  3. 调整事务粒度
    如果业务允许,可以拆分大事务,把查询和更新分开,或者减少单次SaveChanges的批量操作行数,避免锁升级为表锁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 18:45:04