EF Core并发表更新问题:寻求无触发器且避免死锁的方案
并发场景下ReadyProduct数据更新冲突的解决方案
当前代码在并发调用时,会出现读取旧数据或更新冲突的问题:
// 并发调用时,有时会读取到旧数据或出现更新冲突 var readyProduct = await _readyProductRepository.FindAsync(p => p.ModelId == readyProductCreate.ModelId); if (readyProduct == null) { readyProduct = _mapper.Map<ReadyProductCreate, ReadyProduct>(readyProductCreate); await _readyProductRepository.AddAsync(readyProduct); } else { readyProduct.Count += readyProductCreate.Count; _readyProductRepository.Update(readyProduct); } var transaction = new ReadyProductTransaction { ModelId = readyProductCreate.ModelId, Count = readyProductCreate.Count, Status = ReadyProductTransactionType.Import, Date = DateTime.Now, }; await _transactionRepository.AddAsync(transaction); await _unitOfWork.SaveAsync();
之前尝试用Serializable隔离级事务包裹整个流程,但引发了死锁。考虑过通过ReadyProductTransaction表的插入触发器更新ReadyProduct表,但希望找到无需触发器的解决方案。
环境信息:
- DbContext注册为Scoped
- 包括UnitOfWork在内的所有仓储类均注册为Scoped
- 应用基于.NET 5.0
- 部署在IIS上
可行解决方案
1. 乐观锁(推荐)
给ReadyProduct实体加一个RowVersion字段,对应数据库的timestamp/rowversion类型,EF Core会自动检测并发冲突,冲突时重试即可:
首先修改实体类:
public class ReadyProduct { // 原有字段保留 public int Count { get; set; } // 添加行版本字段 public byte[] RowVersion { get; set; } }
在DbContext配置中映射该字段:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<ReadyProduct>() .Property(p => p.RowVersion) .IsRowVersion(); }
然后修改业务逻辑,捕获并发异常并重试:
// 设置重试次数,可根据业务调整 int retryTimes = 3; while (retryTimes-- > 0) { try { var readyProduct = await _readyProductRepository.FindAsync(p => p.ModelId == readyProductCreate.ModelId); if (readyProduct == null) { readyProduct = _mapper.Map<ReadyProductCreate, ReadyProduct>(readyProductCreate); await _readyProductRepository.AddAsync(readyProduct); } else { readyProduct.Count += readyProductCreate.Count; _readyProductRepository.Update(readyProduct); } var transaction = new ReadyProductTransaction { ModelId = readyProductCreate.ModelId, Count = readyProductCreate.Count, Status = ReadyProductTransactionType.Import, Date = DateTime.Now, }; await _transactionRepository.AddAsync(transaction); await _unitOfWork.SaveAsync(); // 成功就跳出循环 break; } catch (DbUpdateConcurrencyException ex) { // 刷新实体的最新数据,准备重试 foreach (var entry in ex.Entries) { if (entry.Entity is ReadyProduct product) { var dbValues = await entry.GetDatabaseValuesAsync(); if (dbValues != null) { entry.CurrentValues.SetValues(dbValues); } else { // 实体已被删除,根据业务需求处理,这里直接抛出 throw new InvalidOperationException("目标商品已被删除,无法完成操作", ex); } } else { throw new NotSupportedException("无法处理该类型的并发冲突", ex); } } } }
乐观锁不会长期占用锁资源,适合高并发场景,只有冲突发生时才重试,性能影响小。
2. 行级悲观锁
通过EF Core执行带锁的SQL查询,锁定目标行,避免其他事务修改:
// 以SQL Server为例,用UPDLOCK+HOLDLOCK锁定行 var readyProduct = await _context.ReadyProducts .FromSqlRaw("SELECT * FROM ReadyProducts WITH (UPDLOCK, HOLDLOCK) WHERE ModelId = {0}", readyProductCreate.ModelId) .FirstOrDefaultAsync(); // 后续逻辑和原代码一致 if (readyProduct == null) { readyProduct = _mapper.Map<ReadyProductCreate, ReadyProduct>(readyProductCreate); await _readyProductRepository.AddAsync(readyProduct); } else { readyProduct.Count += readyProductCreate.Count; _readyProductRepository.Update(readyProduct); } var transaction = new ReadyProductTransaction { ModelId = readyProductCreate.ModelId, Count = readyProductCreate.Count, Status = ReadyProductTransactionType.Import, Date = DateTime.Now, }; await _transactionRepository.AddAsync(transaction); await _unitOfWork.SaveAsync();
UPDLOCK:获取更新锁,允许其他事务读取但禁止修改HOLDLOCK:保持锁直到事务结束
这种方式能确保当前事务拿到最新数据并独占修改权,相比Serializable隔离级,只锁定单行,死锁概率低很多,但要注意控制事务时长,避免锁持有过久影响性能。
3. 原子UPSERT操作
直接用SQL执行原子的插入/更新(UPSERT),把判断和更新放在数据库层面完成,彻底避免先读后写的竞争:
// SQL Server环境下的UPSERT语句,其他数据库可调整语法 var upsertSql = @" IF EXISTS (SELECT 1 FROM ReadyProducts WHERE ModelId = @ModelId) UPDATE ReadyProducts SET Count = Count + @Count WHERE ModelId = @ModelId ELSE INSERT INTO ReadyProducts (ModelId, Count /* 其他必填字段 */) VALUES (@ModelId, @Count /* 对应字段值 */)"; // 执行原子更新 await _context.Database.ExecuteSqlRawAsync(upsertSql, new SqlParameter("@ModelId", readyProductCreate.ModelId), new SqlParameter("@Count", readyProductCreate.Count)); // 插入交易记录 var transaction = new ReadyProductTransaction { ModelId = readyProductCreate.ModelId, Count = readyProductCreate.Count, Status = ReadyProductTransactionType.Import, Date = DateTime.Now, }; await _transactionRepository.AddAsync(transaction); await _unitOfWork.SaveAsync();
这种方式性能最优,完全避免并发问题,但需要手动编写SQL,灵活性稍差,适合业务逻辑固定的场景。
内容的提问来源于stack exchange,提问作者Maruf
相关产品推荐
相关产品推荐

