如何解决Entity Framework中的死锁问题?并行删除代码报错求助
解决EF并行删除引发的死锁问题
为啥会死锁?
你设置了Read Uncommitted隔离级别,但这个级别只影响读取操作的锁行为,删除操作(RemoveRange+SaveChanges)还是会给目标行加排他锁(X锁)。当多个并行事务同时处理不同Code的数据时,如果Code字段没索引,查询会扫全表,导致事务锁一堆无关行;或者不同事务锁资源的顺序乱了,就会触发死锁。
可行的解决办法
1. 给Code字段加索引
先查下dbsHouses表的Code有没有非聚集索引。没有的话,查询Where(d => d.Code == code)会扫全表,锁一大堆行,死锁概率直接拉满。
建索引的SQL(SQL Server为例):
CREATE NONCLUSTERED INDEX IX_dbsHouses_Code ON dbsHouses(Code);
有了索引,查询能直接定位到目标行,锁的范围小了,死锁概率会大幅降低。
2. 别用并行,串行执行
如果业务允许,直接把Parallel.ForEach换成普通foreach:
string[] to = new string[] { "A", "B" }; foreach (var code in to) { DeleteHouses(code); }
串行的时候,每个事务依次拿锁、释放锁,不会出现互相等锁的情况,直接解决死锁。要是必须并行,接着看下面的方案。
3. 简化事务逻辑,缩短锁持有时间
当前代码里事务包了查询+删除两步,可以优化成只包删除,或者直接用批量SQL删:
方案3.1 直接用原生SQL批量删
跳过EF先查再删的流程,直接让数据库执行删除,锁持有的时间更短:
private static void DeleteHouses(string code) { using var ctx = new BloggingContext(); string sql = "DELETE FROM dbsHouses WHERE Code = @code"; ctx.Database.ExecuteSqlRaw(sql, new SqlParameter("@code", code)); }
这种方式不用EF跟踪实体,减少了额外开销,也避免了查询阶段的锁问题。
方案3.2 缩小事务范围
如果非要用EF操作实体,把事务只包在删除和保存的环节:
private static void DeleteHouses(string code) { using var ctx = new BloggingContext(); // 查询放事务外,用AsNoTracking避免EF跟踪实体 var todel = ctx.dbsHouses.AsNoTracking().Where(d => d.Code == code).ToList(); if (todel.Any()) { using var trans = ctx.Database.BeginTransaction(); ctx.dbsHouses.RemoveRange(todel); ctx.SaveChanges(); trans.Commit(); } }
4. 统一锁获取顺序(必须并行时用)
如果业务必须并行,把要处理的code排序后再执行并行:
string[] to = new string[] { "A", "B" }.OrderBy(c => c).ToArray(); Parallel.ForEach(to, code => { DeleteHouses(code); });
统一的顺序能避免不同事务交叉锁资源,减少死锁可能。
5. 加死锁重试机制
作为兜底,遇到死锁自动重试:
private static void DeleteHousesWithRetry(string code, int maxRetries = 3) { int retryCount = 0; while (retryCount < maxRetries) { try { using var ctx = new BloggingContext(); string sql = "DELETE FROM dbsHouses WHERE Code = @code"; ctx.Database.ExecuteSqlRaw(sql, new SqlParameter("@code", code)); return; } catch (SqlException ex) { // SQL Server死锁的错误码是1205 if (ex.Number == 1205) { retryCount++; // 每次重试等一会儿,避免立刻又竞争 Thread.Sleep(100 * retryCount); } else { throw; } } } throw new InvalidOperationException($"重试{maxRetries}次后还是删不掉"); }
这样就算碰到死锁,也能自动重试,保证操作最终完成。
内容的提问来源于stack exchange,提问作者Jishnu Chandran
相关产品推荐
相关产品推荐

