EF Core 5.0调用AddAsync保存数据时重复插入记录问题
问题原因
你遇到的不确定重复插入问题,和AddAsync方法本身无关,核心是以下几个逻辑漏洞共同导致的:
- 并发竞态漏洞:当前采用的「先查询校验、再写入」的逻辑不是原子操作。如果两个携带相同业务参数的请求同时执行到查询步骤,此时数据库中还没有对应记录,两个请求都会判断为
exist == null进入插入分支,最终写入两条重复数据。并发越高,重复出现的概率越大。 - 查询与插入字段逻辑不一致:查询重复记录时你对
rec.ItemId做了Trim()处理,但插入实体时ProductCode直接赋值未做Trim的原始值。如果传入的ItemId前后存在空白字符,会出现查询匹配不到、但实际已插入相似记录的情况,同批次数据循环处理时就会生成重复条目。 - DbContext生命周期配置错误:如果注入的
_cFCDbContext不是Scoped(请求级)生命周期,而是配置为单例或跨请求复用,EF Core的变更追踪器会残留之前请求/操作的实体状态,即使查询判断数据库无对应记录,本地追踪缓存中待提交的实体会在SaveChanges时被一并提交,造成重复插入。 - 异常静默吞掉:catch块直接执行
continue跳过所有异常,既不记录日志也不做回滚处理,当出现唯一键冲突、数据库连接重试、变更追踪状态异常等情况时,异常被掩盖后逻辑继续执行,很容易触发重复提交。 - 冗余Update调用风险:更新分支中手动调用
Update()方法是多余的——从当前DbContext查询出的实体默认处于追踪状态,直接修改属性后调用SaveChanges即可生成更新SQL,手动调用Update会强制实体进入Modified状态,在状态混乱时可能触发非预期的写入行为。
修复方案
- 先加数据库层唯一约束兜底:这是防重复的最可靠手段,不要把防重复逻辑完全寄托在应用层校验上。在
PurchaseSaleLog表上针对你判断重复的业务维度(ProductCode、DaxDate、IsActive、IsDeleted)建立联合唯一索引,从数据库层面彻底阻止重复记录写入。EF Core中配置示例:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<PurchaseSaleLog>() .HasIndex(p => new { p.ProductCode, p.DaxDate, p.IsActive, p.IsDeleted }) .HasFilter("IsDeleted = 0") // 适配软删除场景,仅对未删除数据生效 .IsUnique(); }
- 统一入参处理逻辑:循环内先把所有入参做标准化处理,查询、插入、更新环节都用标准化后的值,避免前后逻辑不一致:
// 提前统一处理入参 var productCode = rec.ItemId.Trim(); var purchQty = string.IsNullOrEmpty(rec.PurchQty) ? 0 : Convert.ToInt32(rec.PurchQty); var branchStock = string.IsNullOrEmpty(rec.BranchStock) ? 0 : Convert.ToInt32(rec.BranchStock);
- 保证校验与写入的原子性:不要拆成无锁的查询+写入两步,用事务加更新锁的方式阻塞同维度的并发查询,从根源避免竞态条件:
using var transaction = await _cFCDbContext.Database.BeginTransactionAsync(); // 查询时加更新锁,相同条件的并发请求会在此处排队等待锁释放,避免同时读到无数据的状态 var exist = await _cFCDbContext.PurchaseSaleLog .Where(a => a.IsActive && a.ProductCode == productCode && a.DaxDate.Equals(rec.PurchDate) && !a.IsDeleted) .OrderByDescending(a => a.Id) .FirstOrDefaultAsync(); // 后续新增/更新逻辑使用提前处理好的标准化参数 if (exist == null) { // 组装新实体执行新增 } else { // 直接修改exist的属性即可,不需要手动调用Update } await _cFCDbContext.SaveChangesAsync(); await transaction.CommitAsync();
- 检查DbContext生命周期配置:EF Core的DbContext默认应为Scoped生命周期,保证每个HTTP请求对应一个独立的DbContext实例,禁止配置为单例,避免变更追踪状态跨请求污染。
- 移除无意义的异常吞没逻辑:catch块不要直接跳过所有异常,至少要记录错误日志,针对唯一键冲突这类特定异常可以做查询重试的补偿逻辑,避免异常掩盖导致问题不可排查。
- 移除多余的Update调用:对于当前上下文追踪到的查询实体,直接修改属性后SaveChanges即可,不需要手动调用Update方法,避免非预期的全字段更新。
内容的提问来源于stack exchange,提问作者Confiz Waqar
相关产品推荐
相关产品推荐

