EF6批量导入ERP数据时SQL死锁问题求助
解决批量导入中标记Deleted字段时的SQL死锁问题
死锁原因分析
死锁发生在更新Deleted字段的语句上,核心原因是并发批次之间的锁竞争:
- 当前导入逻辑中,
GetBatchFromDb查询现有工单时使用默认的共享锁(读提交隔离级别下),锁会持有到事务提交(即SaveChanges执行完成)。 - 当多个并发批次同时处理同一批工单时,每个批次都先以共享锁读取相同的行,之后尝试将共享锁升级为排他锁以更新
Deleted字段,此时就会形成互相等待的死锁,最终SQL Server选择其中一个批次作为受害者终止。
解决方案
1. 查询时获取更新锁(UPDLOCK)
在查询现有工单时直接获取更新锁,避免后续升级锁时的冲突。更新锁允许其他事务读取数据,但阻止其他事务获取更新锁或排他锁,确保当前事务拥有优先更新的权限。
修改GetBatchFromDb方法中的查询逻辑(以EF Core为例):
private static async Task<Dictionary<string, WorkOrder>> GetBatchFromDb(DatabaseEntities db, SemaphoreSlim dbLimiter, List<DataEntryStruct> batch) { await dbLimiter.WaitAsync(); try { var objIds = batch.Select(b => b.ObjID).ToList(); return await db.WorkOrders .Where(w => objIds.Contains(w.ObjID)) .With(LockBehavior.UpdLock) // 添加更新锁提示 .ToDictionaryAsync(w => w.ObjID); } finally { dbLimiter.Release(); } }
如果是EF6,可通过原生SQL实现:
private static Task<Dictionary<string, WorkOrder>> GetBatchFromDb(DatabaseEntities db, SemaphoreSlim dbLimiter, List<DataEntryStruct> batch) { dbLimiter.Wait(); try { var objIds = batch.Select(b => b.ObjID).ToArray(); var workOrders = db.WorkOrders .SqlQuery("SELECT * FROM WorkOrders WHERE ObjID IN (@p0) WITH (UPDLOCK)", objIds) .ToList(); return Task.FromResult(workOrders.ToDictionary(w => w.ObjID)); } finally { dbLimiter.Release(); } }
2. 启用数据库快照隔离
开启SQL Server的READ_COMMITTED_SNAPSHOT选项,让读提交隔离级别下的查询使用版本化数据,不再持有共享锁,从根源避免读锁和写锁的冲突:
ALTER DATABASE YourDatabaseName SET READ_COMMITTED_SNAPSHOT ON;
此操作需要数据库处于单用户模式,执行前需确保没有其他连接。
3. 优化批次划分逻辑
确保同一工单不会被多个并发批次重复处理,比如按ObjID的范围或哈希值划分批次,减少跨批次的锁竞争概率。
4. 缩小事务范围
当前逻辑中,从查询到保存变更的整个过程都在同一个DbContext实例中,锁的持有时间较长。可以考虑将查询和变更操作拆分,或者在完成所有数据准备后再创建DbContext执行更新,缩短锁的持有时间。
验证建议
- 调整后重新运行导入程序,观察SQL Server Profiler是否还有死锁事件。
- 若仍出现死锁,可启用SQL Server的死锁图形捕获(通过Profiler或Extended Events),进一步分析死锁的资源和参与进程,定位是否有其他外部操作干扰。
内容的提问来源于stack exchange,提问作者Tibso
相关产品推荐
相关产品推荐

