C#多事务并行执行时重复校验失效问题求助
并行数据库同步事务内重复键冲突问题解决思路
场景与问题描述
当前通过Parallel.ForEachAsync并行执行数据库同步逻辑,使用Scoped注入的UnitOfWork模式(单DbContext实例),同步方法内部通过RepeatableRead隔离级别创建事务。尽管在代码中做了重复数据检查,但并行线程间无法感知对方新增的数据,导致UserAndProject关联表出现重复键插入错误。
核心代码片段
并行执行逻辑:
var dependedRules = clients.Select(x => SynchronizationPipeLine(x.Key,_scopeFactory.CreateScope().ServiceProvider.GetRequiredService<IUnitOfWork>())); await Parallel.ForEachAsync(dependedRules, async (r, _) => await r);
同步方法内事务逻辑:
using (var transaction = await _unitOfWork.BeginTransactionAsync(IsolationLevel.RepeatableRead)) { try { // 检查项目是否已存在,不存在则新增 if (!await _unitOfWork.BaseRepositories.ProjectRepository.CheckIfRecordExists(resultProject.ProjectId)) { resultProject.Id = await _unitOfWork.BaseRepositories.ProjectRepository.AddAsync(resultProject); } else { // 存在则查询包含关联的项目记录 var projectResult = await _unitOfWork.BaseRepositories.ProjectRepository.GetByProjectId(resultProject.ProjectId); resultProject = projectResult; } await _unitOfWork.BaseRepositories.ProjectRepository.SaveChangesAsync(); await transaction.CommitAsync(); } catch (Exception) { await transaction.RollbackAsync(); } }
触发错误的用户关联代码:
var user = await DbContext.Users.Include(x => x.Projects).SingleOrDefaultAsync(x => x.Id == userId && x.Projects.Any(y=>y.Id == projectId)); if(user != null) return; var sqlString = $@"INSERT INTO public.""UserAndProject""(""ProjectsId"", ""UsersId"") VALUES ({projectId}, {userId}) ON CONFLICT DO NOTHING"; var sql = FormattableStringFactory.Create(sqlString); await DbContext.Database.ExecuteSqlAsync(sql);
问题根源分析
- 隔离级别局限性:
RepeatableRead隔离级别仅能保证当前事务读取的数据不被其他事务修改,但无法阻止其他事务插入新数据(即幻读)。并行场景下,多个线程可能同时通过"不存在"的检查,随后尝试插入重复数据。 - 检查与插入非原子化:前置的EF查询检查基于DbContext的本地缓存或事务快照,无法实时感知其他线程刚提交的插入操作,导致判断失效。
ON CONFLICT未明确约束:原SQL中的ON CONFLICT DO NOTHING未指定具体的唯一约束列,可能无法正确触发冲突处理逻辑。
解决方案
1. 数据库层面强制唯一约束(必须前置)
首先为UserAndProject表创建ProjectsId + UsersId的复合唯一约束,这是数据一致性的底层保障,无论代码逻辑如何,数据库都会直接阻止重复插入。
2. 原子化插入操作(推荐)
移除不可靠的前置EF检查,直接使用带有明确约束的INSERT ... ON CONFLICT语句,将检查与插入合并为数据库原子操作:
// 明确指定冲突的复合唯一约束列 var sqlString = $@"INSERT INTO public.""UserAndProject""(""ProjectsId"", ""UsersId"") VALUES ({projectId}, {userId}) ON CONFLICT (""ProjectsId"", ""UsersId"") DO NOTHING"; var sql = FormattableStringFactory.Create(sqlString); await DbContext.Database.ExecuteSqlAsync(sql);
此操作完全由数据库保证原子性,不会出现并行插入冲突。
3. 全局锁控制并行(牺牲性能换一致性)
如果业务必须保留前置检查逻辑,可以通过全局信号量将同步逻辑串行化,避免并行竞争:
// 定义全局静态信号量 private static readonly SemaphoreSlim _syncSemaphore = new SemaphoreSlim(1, 1); // 在SynchronizationPipeLine方法内加锁 await _syncSemaphore.WaitAsync(); try { // 原有的事务、检查、插入逻辑 } finally { _syncSemaphore.Release(); }
注意:此方案会完全丧失并行性能,仅适用于低并发场景。
4. 调整隔离级别(不推荐)
将事务隔离级别改为Serializable可以解决幻读问题,但会大幅增加数据库锁竞争概率,极易引发死锁,不适合高并发并行场景。
总结
最可靠且兼顾性能的方案是数据库复合唯一约束+原子化INSERT语句,彻底规避并行场景下的检查失效问题。前置的内存/快照检查在并行环境下本质不可靠,必须依赖数据库层面的原子操作保障数据一致性。
内容的提问来源于stack exchange,提问作者Сергей Пантюхов
相关产品推荐
相关产品推荐

