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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 02:45:35