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

ASP.NET MVC+EF6.4随机插入重复记录问题求助

问题排查与解决:EF6随机插入重复业务问题答案记录

1. 并发竞态条件(核心诱因)

当前代码采用先查询后插入的逻辑,高并发场景下多个请求会同时命中answer == null的判断,进而执行重复插入——这和你提到的「75%重复记录时间戳完全一致」的特征完全匹配。

解决手段:

  • 数据库层面加唯一约束:直接给KpBusinessQuestionAnswer表添加联合唯一约束,从根源阻止重复插入:
    ALTER TABLE KpBusinessQuestionAnswer
    ADD CONSTRAINT UQ_KpBusinessQuestionAnswer_App_Business_Question
    UNIQUE (AppId, BusinessId, QuestionId);
    
  • 代码层面改为原子操作:替换先查后插的逻辑,用EF的AddOrUpdate或SQL的MERGE语句,避免竞态:
    用AddOrUpdate优化ProcessAnswers:
    public void ProcessAnswers(int applicationId, int questionId, int businessId, string response)
    {
        if (string.IsNullOrWhiteSpace(response)) return;
    
        var answer = new KpBusinessQuestionAnswer
        {
            AppId = applicationId,
            BusinessId = businessId,
            QuestionId = questionId,
            Response = response,
            IsDeleted = false
        };
    
        kyGenericRepo.Context.Set<KpBusinessQuestionAnswer>()
            .AddOrUpdate(a => new { a.AppId, a.BusinessId, a.QuestionId }, answer);
    }
    
    或直接执行原生SQL(并发安全性更高):
    var sql = @"MERGE INTO KpBusinessQuestionAnswer AS target
                USING (VALUES (@AppId, @BusinessId, @QuestionId, @Response)) AS source (AppId, BusinessId, QuestionId, Response)
                ON target.AppId = source.AppId AND target.BusinessId = source.BusinessId AND target.QuestionId = source.QuestionId
                WHEN MATCHED THEN 
                    UPDATE SET Response = source.Response
                WHEN NOT MATCHED THEN
                    INSERT (AppId, BusinessId, QuestionId, Response, IsDeleted)
                    VALUES (source.AppId, source.BusinessId, source.QuestionId, source.Response, 0);";
    
    kyGenericRepo.Context.Database.ExecuteSqlCommand(sql, 
        new SqlParameter("@AppId", applicationId),
        new SqlParameter("@BusinessId", businessId),
        new SqlParameter("@QuestionId", questionId),
        new SqlParameter("@Response", response));
    

2. DbContext生命周期异常

如果kyGenericRepo对应的DbContext是单例或生命周期过长,会导致多个请求共用上下文,缓存的实体状态和数据库不一致,进而触发重复插入。

排查与修复:

  • 确认DbContext是请求级生命周期(ASP.NET MVC默认应为每个请求创建实例),如果是自定义DI,注册为PerRequest或Transient。
  • 检查kyGenericRepo是否被静态实例化或单例注入,避免跨请求共享。

3. 查询逻辑的过滤漏洞

检查FindByApp方法是否正确排除了软删除的记录,如果漏了IsDeleted == false的条件,会导致已软删除的记录被忽略,进而重复插入。

排查要点:

确保FindByApp的实现包含软删除过滤:

// 正确的FindByApp示例
public IQueryable<T> FindByApp<T>(int applicationId, Expression<Func<T, bool>> predicate) where T : class, IHasAppId, IHasIsDeleted
{
    return context.Set<T>().Where(x => x.AppId == applicationId && x.IsDeleted == false).Where(predicate);
}

4. 前端幂等性补防

虽然禁用了提交按钮,但极端场景(如网络延迟导致按钮禁用不及时、用户通过控制台强制提交)仍可能产生重复请求,可通过后端幂等校验补防:

  • 前端提交时生成唯一RequestId,后端将其存入Redis或数据库,同一RequestId仅处理一次。
  • 接口校验RequestId,已处理过的直接返回成功,不执行数据库操作。

5. EF状态管理优化

当前Add方法中同时执行context.Entry(entity).State = EntityState.Added和_dbSet.Add(entity)属于重复操作,虽不直接导致重复插入,但可简化以避免潜在问题:

public T Add<T>(T entity) where T : class
{
    try
    {
        var _dbSet = context.Set<T>();
        return _dbSet.Add(entity); // Add方法会自动设置状态为Added
    }
    catch (Exception ex)
    {
         logger.Debug(ex);
         return null;
    }
}

内容的提问来源于stack exchange,提问作者rientelfon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 11:25:35