多API调用竞争原子数据库操作的竞态条件解决咨询
解决并发创建重复联系人的竞态条件方案
针对你的Azure SQL Database + .NET6 + EF Core技术栈,进程内锁(Semaphore)和跨进程Mutex无效的问题,核心原因是分布式/多进程场景下,客户端锁无法保证全局一致性,且你的Mutex代码存在异步场景下的线程关联问题(Mutex绑定到持有它的线程,异步操作会切换线程,导致ReleaseMutex时出错)。以下是几种可靠的解决思路:
一、数据库唯一约束+重试机制(最推荐)
这是分布式场景下最稳妥的方案,从根源上阻止重复数据:
- 在联系人表的接收者标识字段(比如手机号、邮箱、用户ID)上创建唯一约束,确保数据库层面不允许重复值。
- 修改API逻辑:直接尝试创建联系人,若捕获到
SqlException且错误码为2601/2627(SQL Server唯一约束冲突错误),则跳过创建步骤,直接执行消息发送。 - 配合EF Core的重试策略,确保临时冲突时自动重试。
示例代码:
public async Task SendMessageAsync(string recipientId, string message) { try { await using var dbContext = new YourDbContext(); // 尝试创建联系人 var contact = new Contact { RecipientId = recipientId, /* 其他字段 */ }; dbContext.Contacts.Add(contact); await dbContext.SaveChangesAsync(); } catch (SqlException ex) when (ex.Number is 2601 or 2627) { // 唯一约束冲突,说明联系人已存在,无需处理 } // 发送消息逻辑 await SendMessageToContactAsync(recipientId, message); }
二、使用数据库原子UPSERT操作
利用SQL Server的MERGE语句实现“不存在则创建,存在则忽略”的原子操作,避免先查询再创建的竞态:
- 编写原生SQL的MERGE语句,在数据库层面完成原子性的检查和创建。
- 通过EF Core执行该SQL,或者封装成存储过程调用。
示例EF Core执行原生SQL:
public async Task EnsureContactExistsAsync(string recipientId) { await using var dbContext = new YourDbContext(); var sql = @" MERGE INTO Contacts AS Target USING (SELECT @RecipientId) AS Source (RecipientId) ON Target.RecipientId = Source.RecipientId WHEN NOT MATCHED THEN INSERT (RecipientId, CreatedAt) VALUES (Source.RecipientId, GETDATE()); "; await dbContext.Database.ExecuteSqlRawAsync(sql, new SqlParameter("@RecipientId", recipientId)); }
三、查询时添加行级锁提示
在查询联系人是否存在时,使用SQL Server的UPDLOCK和HOLDLOCK锁提示,确保并发查询时只有一个请求能进入创建流程:
- 查询联系人时,添加锁提示,锁定查询范围,阻止其他事务读取该范围的未提交数据。
- 在同一个事务内完成“查询-创建”操作,确保锁的持有时间覆盖整个创建流程。
示例代码:
public async Task SendMessageAsync(string recipientId, string message) { await using var dbContext = new YourDbContext(); using var transaction = await dbContext.Database.BeginTransactionAsync(); try { // 查询时添加UPDLOCK和HOLDLOCK,锁定该行(或范围) var existingContact = await dbContext.Contacts .FromSqlRaw("SELECT * FROM Contacts WITH (UPDLOCK, HOLDLOCK) WHERE RecipientId = {0}", recipientId) .FirstOrDefaultAsync(); if (existingContact == null) { dbContext.Contacts.Add(new Contact { RecipientId = recipientId }); await dbContext.SaveChangesAsync(); } await SendMessageToContactAsync(recipientId, message); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } }
关于你的命名Mutex无效的原因
你的Mutex代码存在异步场景下的致命问题:Mutex是线程绑定的,WaitOne在当前线程持有锁后,异步操作await CriticalAction(...)会释放当前线程,后续代码在另一个线程执行,此时ReleaseMutex会抛出异常(因为当前线程不是持有锁的线程),导致锁无法正确释放,进而导致其他请求永远等待。
内容的提问来源于stack exchange,提问作者Neville Lam
相关产品推荐
相关产品推荐

