如何用Entity Framework防止客户端并发插入重复记录?
如何避免API并发调用时的重复数据插入?
哥们,你当前写的IsExistIn10Seconds逻辑在4个客户端并发调用的场景下根本挡不住重复插入,核心问题在于「检查-插入」不是原子操作:四个请求可能同时跑到查询步骤,这时候数据库里还没有任何记录,所以都能通过检查,接着各自执行插入,最后还是会出现4条重复数据。
为什么当前逻辑失效?
这种先查询再判断的模式,在高并发下会出现竞态条件——查询和插入之间有时间差,其他请求完全可以在这个空隙里完成同样的查询,然后一起插入数据。
更优的实现方案(按可靠性排序)
1. 数据库层面的联合唯一约束(最推荐)
这是最底层、最可靠的保障,直接让数据库帮你挡住重复数据。你可以给Histories表创建一个联合唯一索引,把Number和「截断到10秒窗口的EntryDateTime」绑定在一起:
比如在SQL Server里,你可以创建这样的索引:
CREATE UNIQUE NONCLUSTERED INDEX IX_Histories_Number_EntryTimeWindow ON Histories (Number, DATEADD(second, DATEDIFF(second, 0, EntryDateTime)/10*10, 0))这个索引会把
EntryDateTime截断到每10秒的起始点(比如10:00:00、10:00:10、10:00:20...),同一个号码在同一个10秒窗口里只能插入一条数据。代码层面只需捕获唯一约束冲突的异常,然后返回「已存在」的结果即可:
public async Task<bool> TryInsertHistory(string number, DateTime entryTime) { try { DbContext.Histories.Add(new History { Number = number, EntryDateTime = entryTime }); await DbContext.SaveChangesAsync(); return true; } catch (DbUpdateException ex) { // 检查是否是唯一约束冲突(不同数据库异常类型可能有差异) if (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2601) { return false; // 表示重复插入,已存在 } throw; } }
2. 分布式锁(跨服务/多实例场景)
如果你的API是多实例部署的,单数据库锁可能不够用,这时候可以用Redis这类分布式锁,以Number为锁键,锁的过期时间设为10秒:
public async Task<bool> TryInsertHistory(string number, DateTime entryTime) { // 假设_redisLockService是你封装的Redis锁服务 using var redisLock = await _redisLockService.LockAsync($"history_lock:{number}", TimeSpan.FromSeconds(10)); if (!redisLock.IsAcquired) { return false; // 并发请求,直接返回已存在或操作中 } // 这里再执行检查和插入(加锁后竞态条件就不存在了) var existing = await DbContext.Histories .Where(o => o.Number == number && o.EntryDateTime >= DateTime.Now.AddSeconds(-10)) .FirstOrDefaultAsync(); if (existing == null) { DbContext.Histories.Add(new History { Number = number, EntryDateTime = entryTime }); await DbContext.SaveChangesAsync(); return true; } return false; }
3. 数据库悲观锁(单实例场景)
如果你的API是单实例部署,也可以用EF Core的悲观锁,在查询时锁住符合条件的行,阻止其他请求的查询操作:
public async Task<bool> TryInsertHistory(string number, DateTime entryTime) { using var transaction = await DbContext.Database.BeginTransactionAsync(); try { // 加上悲观写锁,锁住查询到的行(如果没有行,就锁住对应的范围) var existing = await DbContext.Histories .Where(o => o.Number == number && o.EntryDateTime >= DateTime.Now.AddSeconds(-10)) .FirstOrDefaultAsync().WithLock(LockMode.PessimisticWrite); if (existing == null) { DbContext.Histories.Add(new History { Number = number, EntryDateTime = entryTime }); await DbContext.SaveChangesAsync(); await transaction.CommitAsync(); return true; } await transaction.RollbackAsync(); return false; } catch { await transaction.RollbackAsync(); throw; } }
总结
- 优先用数据库唯一约束,它是最可靠的,即使应用层出问题,数据库也能守住最后一道防线;
- 多实例部署选分布式锁;
- 单实例场景可以用悲观锁,但性能上不如唯一约束。
内容的提问来源于stack exchange,提问作者Jourmand
相关产品推荐
相关产品推荐

