.NET 6服务SaveChanges触发唯一约束冲突,提前检查仍报错
问题分析与解决方案
核心问题原因
你遇到的问题本质是先查询后插入的非原子操作加上DbContext的ChangeTracker缓存特性导致的:
- 即使DbContext是Scoped生命周期,若文件监视器短时间内重复触发同一文件的处理事件(比如文件复制过程中系统会触发多次Created/Changed事件),可能导致同一作用域内复用DbContext;
FirstOrDefault默认会从DbContext的ChangeTracker缓存中读取数据,而非每次都查询数据库。如果前一次插入已将记录加入缓存但未提交,或另一个线程的插入已提交但当前DbContext缓存未更新,就会出现"检查不存在但插入时违反唯一约束"的矛盾情况;- 你使用的Read Committed隔离级别本身没问题,单服务操作场景下,核心问题还是客户端的缓存和操作非原子性。
根源性解决方案
1. 用数据库原子操作替代客户端两次查询
放弃客户端的两次存在性检查,直接使用Oracle的原子操作确保插入唯一性,比如MERGE语句:
// 执行原子插入逻辑 var sql = @" MERGE INTO [table] t USING (SELECT :fileName AS FileName FROM DUAL) s ON (t.FileName = s.FileName) WHEN NOT MATCHED THEN INSERT (FileName, [其他列名]) VALUES (:fileName, :otherColumnValue)"; _dbContext.Database.ExecuteSqlRaw(sql, new OracleParameter("fileName", obj.FileName), new OracleParameter("otherColumnValue", obj.OtherProperty));
这种方式将检查和插入合并为数据库端的原子操作,彻底避免客户端的竞态问题。
2. 禁用查询缓存,强制每次查询数据库
如果坚持保留客户端检查逻辑,需要在查询时添加AsNoTracking(),绕过DbContext的ChangeTracker缓存,确保每次查询都直接访问数据库:
// 检查存在性时强制查询数据库 var existingRecord = _dbContext.[table] .AsNoTracking() .FirstOrDefault(s => s.FileName == obj.FileName); if (existingRecord != null) { return true; } _dbContext.[table].Add(obj); _dbContext.SaveChanges();
3. 确保文件处理流程使用独立的DbContext实例
后台服务的文件监视器事件线程可能无法正确绑定Scoped上下文,导致DbContext被意外复用。可以在事件处理方法内手动创建作用域,获取独立的DbContext:
// 在文件处理方法内部创建作用域 using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<OracleDbContext>(); // 后续检查、插入操作均使用这个独立的dbContext var existing = dbContext.[table].AsNoTracking().FirstOrDefault(s => s.FileName == obj.FileName); // ...
4. 正确处理唯一约束异常
如果极端场景下仍触发异常,可捕获ORA-00001并做验证处理,而非直接忽略:
try { _dbContext.[table].Add(obj); _dbContext.SaveChanges(); } catch (DbUpdateException ex) when (ex.InnerException is OracleException oracleEx && oracleEx.Number == 1) { // 验证记录是否已存在 var exists = _dbContext.[table].AsNoTracking().Any(s => s.FileName == obj.FileName); if (exists) { return true; // 记录已存在,返回成功 } // 非预期情况重新抛出异常 throw; }
额外注意事项
- 检查文件监视器的触发逻辑,是否存在同一文件被多次触发的情况(比如文件写入完成前触发Created事件),可添加文件锁定检查或延迟处理,避免重复进入处理流程;
- 不要随意将事务隔离级别改为Serializable,会大幅降低并发性能且容易引发死锁,你的场景不需要这么高的隔离级别。
内容的提问来源于stack exchange,提问作者Andy
相关产品推荐
相关产品推荐

