You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多API调用竞争原子数据库操作的竞态条件解决咨询

解决并发创建重复联系人的竞态条件方案

针对你的Azure SQL Database + .NET6 + EF Core技术栈,进程内锁(Semaphore)和跨进程Mutex无效的问题,核心原因是分布式/多进程场景下,客户端锁无法保证全局一致性,且你的Mutex代码存在异步场景下的线程关联问题(Mutex绑定到持有它的线程,异步操作会切换线程,导致ReleaseMutex时出错)。以下是几种可靠的解决思路:

一、数据库唯一约束+重试机制(最推荐)

这是分布式场景下最稳妥的方案,从根源上阻止重复数据:

  1. 在联系人表的接收者标识字段(比如手机号、邮箱、用户ID)上创建唯一约束,确保数据库层面不允许重复值。
  2. 修改API逻辑:直接尝试创建联系人,若捕获到SqlException且错误码为2601/2627(SQL Server唯一约束冲突错误),则跳过创建步骤,直接执行消息发送。
  3. 配合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语句实现“不存在则创建,存在则忽略”的原子操作,避免先查询再创建的竞态:

  1. 编写原生SQL的MERGE语句,在数据库层面完成原子性的检查和创建。
  2. 通过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锁提示,确保并发查询时只有一个请求能进入创建流程:

  1. 查询联系人时,添加锁提示,锁定查询范围,阻止其他事务读取该范围的未提交数据。
  2. 在同一个事务内完成“查询-创建”操作,确保锁的持有时间覆盖整个创建流程。

示例代码:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 09:01:09