并发请求下数据库重复插入行问题的解决方案
解决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
相关产品推荐
相关产品推荐

