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:
或直接执行原生SQL(并发安全性更高):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); }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
相关产品推荐
相关产品推荐

