You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);

问题根源分析

  1. 隔离级别局限性:RepeatableRead隔离级别仅能保证当前事务读取的数据不被其他事务修改,但无法阻止其他事务插入新数据(即幻读)。并行场景下,多个线程可能同时通过"不存在"的检查,随后尝试插入重复数据。
  2. 检查与插入非原子化:前置的EF查询检查基于DbContext的本地缓存或事务快照,无法实时感知其他线程刚提交的插入操作,导致判断失效。
  3. 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,提问作者Сергей Пантюхов

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 08:10:38