如何解决Web应用并行请求下重复创建Foo表同名条目的问题?
解决并行请求下创建唯一名称Foo条目的冲突问题
当两个并行请求同时执行「检查Foo条目是否存在→不存在则创建」的逻辑时,会因为竞态条件导致重复创建同名Foo,违反系统唯一性要求。以下是几种靠谱的解决方法:
1. 数据库层面添加唯一约束(核心保障)
这是最底层、最可靠的防线——直接给Foo表的Name字段添加唯一索引或约束。不管应用层逻辑有没有漏洞,数据库都会拦截重复插入的请求。
应用层需要做的是捕获唯一键冲突异常,然后重新查询已存在的Foo条目,继续后续逻辑。修改后的伪代码如下:
Process(Request request) { // 耗时约3000ms的业务逻辑 var foo = context.Foos.SingleOrDefault(f => f.Name == request.Name); if (foo == null) { try { foo = new Foo { Name = request.Name }; context.Foos.Add(foo); context.SaveChanges(); } catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex)) { // 捕获唯一键冲突,重新拉取已创建的Foo foo = context.Foos.Single(f => f.Name == request.Name); } } car.Foo = foo; context.Cars.Add(car); context.SaveChanges(); }
其中IsUniqueConstraintViolation是自定义方法,需要根据你使用的数据库(SQL Server/MySQL/PostgreSQL)判断异常是否为唯一键冲突(比如检查SQL错误码)。
2. 用数据库原子操作消除竞态
把「先查询再插入」的两步操作改成数据库端的原子操作,从根源避免竞态条件。不同数据库有对应的语法:
- MySQL:
INSERT ... ON DUPLICATE KEY UPDATE - SQL Server:
MERGE - PostgreSQL:
INSERT ... ON CONFLICT DO NOTHING
用EF执行原生SQL的示例:
// 原子操作:插入新Foo或获取已存在的Foo的ID var fooId = context.Database.ExecuteSqlInterpolated( @"INSERT INTO Foos (Name) VALUES ({request.Name}) ON DUPLICATE KEY UPDATE Id = Id; SELECT Id FROM Foos WHERE Name = {request.Name}" ); foo = context.Foos.Find(fooId);
这种方式不需要先查询,直接让数据库处理原子性,效率更高。
3. 多实例场景用分布式锁
如果你的Web应用是多实例部署,应用层的内存锁(比如C#的lock)无法跨实例生效,这时候需要用分布式锁(基于Redis、ZooKeeper等实现)。
在执行「检查+创建」逻辑前,先获取以request.Name为标识的分布式锁,只有拿到锁的请求才能执行操作,其他请求等待锁释放后再重新查询。伪代码示例:
Process(Request request) { // 耗时约3000ms的业务逻辑 using (var distributedLock = await DistributedLockProvider.AcquireLockAsync($"foo_{request.Name}")) { var foo = context.Foos.SingleOrDefault(f => f.Name == request.Name); if (foo == null) { foo = new Foo { Name = request.Name }; context.Foos.Add(foo); context.SaveChanges(); } } car.Foo = foo; context.Cars.Add(car); context.SaveChanges(); }
注意要给分布式锁设置合理的超时时间,避免死锁,同时确保锁能被正确释放(比如用using自动释放)。
总结
- 优先用数据库唯一约束+异常捕获,这是最稳妥的方案,成本最低;
- 想从根源消除竞态,就用数据库原子操作;
- 多实例部署时,再搭配分布式锁进一步降低冲突概率。
内容的提问来源于stack exchange,提问作者Ľuboš Pilka
相关产品推荐
相关产品推荐

