EF Core多线程插入SQL Server遇死锁,求更优解决办法
核心问题分析
你遇到的死锁本质是高并发下,EF Core默认的「INSERT + SELECT scope_identity()」两次请求导致的锁竞争:插入操作持有行锁,后续查询操作又需要获取相关锁,多个线程交叉执行时容易形成死锁。而你之前添加的手动事务扩大了锁的持有范围,是性能暴跌5倍的主要原因。
优化方案
1. 移除多余的手动事务,保留重试策略
EF Core的SaveChangesAsync()本身就是原子性操作(默认在一个事务内执行),额外套的手动事务完全没必要,反而增加了事务开启、提交/回滚的开销。修改后的代码如下:
public async Task InsertLogApi(LogApi logApi) { var executionStrategy = _context.Database.CreateExecutionStrategy(); await executionStrategy.ExecuteAsync(async () => { _context.Add(logApi); await _context.SaveChangesAsync(); }); }
这样既保留了死锁后的重试能力,又消除了手动事务带来的额外性能损耗。
2. 让EF Core合并INSERT与ID查询为单条SQL
部分版本的EF Core会分开执行INSERT和SELECT获取自增ID,你可以通过配置让EF使用OUTPUT子句一次性完成插入并返回ID,减少一次数据库请求,同时缩短锁持有时间。
在DbContext的OnModelCreating方法中配置实体主键:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<LogApi>() .Property(e => e.Id) .UseIdentityColumn(); // 确保使用自增列并启用OUTPUT返回ID }
配置后EF会生成类似这样的SQL:
INSERT INTO [LogApi] (...) VALUES (@p0, @p1, ...) OUTPUT INSERTED.[Id];
将两次请求合并为一次,从根源上减少锁竞争的概率。
3. 批量插入替代单条插入
高并发场景下,单条插入的性能瓶颈非常明显。可以通过批量收集请求(比如用内存队列攒N条数据,或按时间窗口批量),再用AddRange批量插入:
public async Task InsertLogApiBatch(List<LogApi> logApiList) { var executionStrategy = _context.Database.CreateExecutionStrategy(); await executionStrategy.ExecuteAsync(async () => { _context.AddRange(logApiList); await _context.SaveChangesAsync(); }); }
批量插入不仅减少了数据库请求次数,EF Core还会优化生成批量INSERT语句,同时通过OUTPUT一次性返回所有自增ID,锁的持有时间也会大幅缩短,死锁概率和性能开销都会显著降低。
4. 开启数据库的READ_COMMITTED_SNAPSHOT隔离级别
在SQL Server中执行以下语句开启快照隔离:
ALTER DATABASE [你的数据库名] SET READ_COMMITTED_SNAPSHOT ON;
开启后,默认的ReadCommitted隔离级别下,读操作会使用数据快照而非加共享锁,彻底避免读写操作之间的锁竞争,从数据库层面解决死锁问题。这个设置对现有业务无侵入,是高并发写入场景的常用优化手段。
优先级建议
优先尝试方案1+方案2,这两个改动最小,能快速恢复性能并降低死锁概率;如果并发量极高,再引入方案3的批量插入;方案4作为兜底的数据库层面优化,可以长期生效。
内容的提问来源于stack exchange,提问作者Zaldrize

