Entity Framework Core中事务为何无法避免删除旧数据的并发更新异常?
为什么当前事务方案无法解决问题?
你的代码存在两个核心问题,导致Serializable事务和执行策略无法阻止并发删除异常:
复用了同一个DbContext实例(执行策略外部创建)
执行策略的作用是在遇到短暂性故障时重试逻辑,但你的db实例是在executionStrategy.ExecuteAsync外部定义的。第一次执行时,DbContext的ChangeTracker会加载符合条件的实体;如果此时因为并发冲突抛出异常,重试时ChangeTracker仍然保留着这些已被其他事务删除的实体,再次执行RemoveRange和SaveChangesAsync时,EF Core会尝试删除这些已经不存在于数据库中的实体,必然触发DbUpdateConcurrencyException。先查询后删除的模式依赖内存中的实体状态
你使用的RemoveRange(db.MyTable.Where(...))会先将符合条件的实体加载到本地内存,再生成基于主键的DELETE语句。即使在Serializable事务中,这种模式也会依赖ChangeTracker中的实体状态,而非直接在数据库端执行批量删除。如果在查询和删除的间隙出现意外的状态不一致(比如DbContext缓存了旧数据),就会导致删除操作找不到对应实体。
正确的解决方法
要彻底解决并发删除的问题,应该避免加载实体到内存,直接在数据库端执行批量删除,同时确保每次重试都使用全新的DbContext实例:
方案1:使用EF Core 7+的ExecuteDeleteAsync(推荐)
如果你的项目可以升级到EF Core 7或更高版本,直接使用批量删除API,无需加载实体:
var expireBefore = DateTime.Now.AddDays(-30); var executionStrategy = db.Database.CreateExecutionStrategy(); await executionStrategy.ExecuteAsync(async () => { await using var newDb = new YourDbContext(); // 每次重试新建DbContext await using var transaction = await newDb.Database.BeginTransactionAsync(IsolationLevel.Serializable); await newDb.MyTable .Where(t => t.CreatedDate <= expireBefore) .ExecuteDeleteAsync(); await transaction.CommitAsync(); });
方案2:使用原生SQL语句(兼容EF Core 6)
如果无法升级EF Core版本,直接执行原生DELETE语句:
var expireBefore = DateTime.Now.AddDays(-30); var executionStrategy = db.Database.CreateExecutionStrategy(); await executionStrategy.ExecuteAsync(async () => { await using var newDb = new YourDbContext(); await using var transaction = await newDb.Database.BeginTransactionAsync(IsolationLevel.Serializable); var sql = "DELETE FROM MyTable WHERE CreatedDate <= @ExpireBefore"; await newDb.Database.ExecuteSqlRawAsync(sql, new { ExpireBefore = expireBefore }); await transaction.CommitAsync(); });
额外注意事项
- DbContext必须线程安全:绝对不要在多个线程或并发执行的逻辑中复用同一个DbContext实例,每次操作(包括重试)都应创建新的实例。
- Serializable隔离级别的必要性:对于批量删除操作,Serializable隔离级别可以防止幻读(避免在事务执行期间有新的符合条件的数据被插入),确保删除操作的原子性。
- 执行策略的正确用法:执行策略应包裹整个需要重试的逻辑,包括DbContext的创建和事务的完整生命周期,避免重试时使用状态不一致的DbContext。
内容的提问来源于stack exchange,提问作者innominate227

