ASP.NET Web API并发请求致数据库重复数据的解决方法咨询
嗨Lee,这个问题确实是Web开发中非常典型的竞态条件问题——多个请求同时绕过了空检查,导致重复插入。要彻底解决这个问题,得从「数据库兜底」和「应用层控制」两个层面入手,下面给你几个实用的方案:
1. 数据库层面添加唯一约束(最核心的防线)
不管应用层做了多少控制,数据库的唯一约束都是最可靠的最后一道关卡。你需要找到那个用来判断记录唯一性的业务字段(比如用户ID、订单编号这类),给它添加UNIQUE约束。
举个例子,假设你要保证BusinessKey字段唯一,在SQL里可以这么加:
ALTER TABLE YourTable ADD CONSTRAINT UQ_YourTable_BusinessKey UNIQUE (BusinessKey);
这样即使多个请求同时跑到插入步骤,数据库会直接抛出唯一约束冲突的异常,阻止重复数据写入。你的代码里只需要捕获这个异常(比如SQL Server的SqlException,错误码2601/2627),然后返回给前端「记录已存在」的提示即可。
2. 应用层使用异步锁控制并发
如果你的WebApi是单实例部署,可以用异步锁来保证同一时间只有一个请求执行检查+插入的逻辑。注意不要用普通的lock(它不支持异步),而是用SemaphoreSlim:
// 声明一个静态的信号量,保证所有控制器实例共享同一锁 private static readonly SemaphoreSlim _operationLock = new SemaphoreSlim(1, 1); // 注意:一定要把async void改成async Task!async void在WebApi中会导致严重问题 public async Task DoSomething() { await _operationLock.WaitAsync(); try { // 异步查询记录 var record = await _dbContext.YourEntities.FirstOrDefaultAsync(e => e.BusinessKey == targetKey); if (record == null) { // 异步插入新记录 _dbContext.YourEntities.Add(new YourEntity { BusinessKey = targetKey, ... }); await _dbContext.SaveChangesAsync(); } } finally { // 无论成功失败,都要释放锁 _operationLock.Release(); } }
如果是多实例部署,内存锁就失效了,这时候需要用分布式锁,比如基于Redis的RedLock实现,或者利用数据库的行级锁(比如执行SELECT ... FOR UPDATE语句,在查询时就锁住对应的资源范围)。
3. 用EF Core的原子Upsert操作(EF 7+支持)
如果你用的是EF Core 7及以上版本,可以直接用Upsert方法,它会帮你生成原子性的SQL语句,自动处理「存在则更新,不存在则插入」的逻辑,从根源上避免竞态条件:
await _dbContext.YourEntities .Upsert(new YourEntity { BusinessKey = targetKey, Name = "xxx" }) .On(entity => entity.BusinessKey) // 指定唯一判断的字段 .RunAsync();
这个方法的底层会生成MERGE(SQL Server)或者类似的原子性语句,不需要你自己处理锁和检查逻辑。
重要提醒:别用async void!
你的示例代码里用了public async void DoSomething(),这在ASP.NET Core中是绝对要避免的——async void方法无法被框架正确跟踪,会导致请求上下文丢失,而且抛出的异常无法被全局异常处理器捕获,很容易引发内存泄漏或者程序崩溃。一定要改成async Task(无返回值)或者async Task<IActionResult>(需要返回结果)。
总结一下:最稳妥的方案是数据库唯一约束 + 应用层异步锁/EF Upsert,这样从底层到应用层都能彻底杜绝重复记录的产生。
内容的提问来源于stack exchange,提问作者user9871834

