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

并发请求下数据库重复插入行问题的解决方案

解决C#异步接口并发插入重复数据的问题

你的代码出现重复插入的核心原因是竞态条件:当多个请求同时到达时,它们会同时执行查询操作(此时都还没有插入数据),都判定为不存在记录,随后各自执行插入,最终导致数据库中出现重复行。

以下是几种可靠的解决方案,按优先级排序:

1. 数据库层面添加联合唯一约束(最底层的保障)

这是最可靠的方案,直接从数据库层面阻止重复数据的插入,即使应用层逻辑出现疏漏,数据库也会抛出错误拦截操作。

针对CreatedBy、CreatedDate(日期部分)、ModuleType三个字段创建联合唯一索引:

-- SQL Server示例,其他数据库语法类似
CREATE UNIQUE NONCLUSTERED INDEX IX_StartWorkLog_UniqueDailyEntry 
ON StartWorkLog (CreatedBy, CONVERT(date, CreatedDate), ModuleType);

然后修改代码中的异常捕获逻辑,处理数据库抛出的重复键错误:

catch (DbUpdateException ex)
{
    // 判断是否是唯一约束冲突
    if (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2601)
    {
        return new ErrorDataResult<StartWorkResponseMobileDto>("今日已提交过该模块的开工记录");
    }
    return new ErrorDataResult<StartWorkResponseMobileDto>(Messages.AnErrorOccured);
}
catch (Exception EX)
{
    return new ErrorDataResult<StartWorkResponseMobileDto>(Messages.AnErrorOccured);
}

2. 应用层使用事务+悲观锁

通过在查询阶段添加锁,确保同一时间只有一个请求能进入插入流程,避免并发请求同时通过检查。

假设你的仓储基于Entity Framework,修改查询逻辑并包裹在事务中:

public async Task<IDataResult<StartWorkResponseMobileDto>> SendStartWork(StartWorkSendMobileDto startWorkSendMobileDto)
{
    using var transaction = await _dbContext.Database.BeginTransactionAsync();
    try
    {
        var userId = requestUserUtil.GetCurrentUserId();
        var today = DateTime.Now.Date;
        var moduleType = startWorkSendMobileDto.ModuleType;

        // 使用UPDLOCK+HOLDLOCK锁定查询范围,防止其他并发请求读取到相同的空结果
        var checkStartWork = await _dbContext.StartWorkLogs
            .FromSqlRaw(@"SELECT * FROM StartWorkLog 
                          WHERE CreatedBy = @userId 
                            AND CONVERT(date, CreatedDate) = @today 
                            AND ModuleType = @moduleType
                          WITH (UPDLOCK, HOLDLOCK)",
                          new SqlParameter("@userId", userId),
                          new SqlParameter("@today", today),
                          new SqlParameter("@moduleType", moduleType))
            .FirstOrDefaultAsync();

        if (checkStartWork != null)
        {
            await transaction.CommitAsync();
            return new SuccessDataResult<StartWorkResponseMobileDto>(null, Messages.SaveItemSuccess);
        }

        var startWork = _mapper.Map<StartWorkLog>(startWorkSendMobileDto);
        _dbContext.StartWorkLogs.Add(startWork);
        await _dbContext.SaveChangesAsync();

        await transaction.CommitAsync();
        return new SuccessDataResult<StartWorkResponseMobileDto>(null, Messages.SaveItemSuccess);
    }
    catch (DbUpdateException ex)
    {
        await transaction.RollbackAsync();
        if (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2601)
        {
            return new ErrorDataResult<StartWorkResponseMobileDto>("今日已提交过该模块的开工记录");
        }
        return new ErrorDataResult<StartWorkResponseMobileDto>(Messages.AnErrorOccured);
    }
    catch (Exception ex)
    {
        await transaction.RollbackAsync();
        return new ErrorDataResult<StartWorkResponseMobileDto>(Messages.AnErrorOccured);
    }
}

3. 优化查询逻辑的时间一致性

代码中DateTime.Now.Date可能会因为服务器时间的微小波动出现差异(比如跨零点请求),可以提前获取当前日期并复用:

var today = DateTime.Now.Date;
// 后续查询都使用这个today变量,而不是重复调用DateTime.Now.Date

内容的提问来源于stack exchange,提问作者Hilal Həsənli

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 20:17:41