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

C# .NET WebAPI无EF并发更新:代金券领取死锁与重复码问题

代金券领取并发冲突问题解决

问题描述

开发代金券领取功能,逻辑为从代金券码列表获取首个可用码,修改状态完成领取。预期并发调用时每个请求返回不同代金券码,但实际测试出现以下问题:

  • 使用Serializable隔离级别时,出现死锁(系统自动终止一个进程,另一个正常执行)
  • 改用其他隔离级别时,所有并发请求返回相同的代金券码

相关代码

public async Task<ClaimVoucherResponse> ClaimVoucher(ClaimVoucherRequest req)
{
    using var connection = new SqlConnection(MSSQLConnectionString);
    await connection.OpenAsync();
    var transaction = connection.BeginTransaction(System.Data.IsolationLevel.Serializable);
    var cmd = connection.CreateCommand();
    cmd.Connection = connection;
    cmd.Transaction = transaction;

    try
    {
        // 执行类似 "SELECT TOP 1 * FROM table WHERE voucherId={voucherId} AND status=1" 的查询
        var voucherCode = await GetAvailableCode(voucherId, cmd);

        // 执行类似 "UPDATE table SET status={status} WHERE voucherCodeId={voucherCodeId}" 的更新
        await UpdateVoucherCode(voucherCode.Id, new SetVoucherCodeRequest
        {
            Status = 2,
        }, cmd);
        transaction.Commit();
        return new ClaimVoucherResponse
        {
            VoucherCode = voucherCode.VoucherCode,
        };
    }
    catch (Exception ex)
    {
        Console.WriteLine("Commit Exception Type: {0}", ex.GetType());
        Console.WriteLine("  ----- Message: {0}", ex.Message);
        transaction.Rollback();
    }
}

已尝试方案

  • 隔离级别设为Serializable:出现死锁错误
  • 设为其他隔离级别:并发调用返回相同代金券码

核心问题分析

当前逻辑将查询可用码和标记已领取拆分为两个独立操作,低隔离级别下会出现幻读/脏读,导致多个请求读取到同一个未被锁定的可用码;Serializable隔离级别下由于范围锁的特性,不同请求的锁范围重叠引发死锁。

解决方案:原子化UPDATE+OUTPUT操作

将查询与更新合并为单个原子操作,利用SQL Server的OUTPUT子句直接返回被锁定并更新的代金券码,彻底避免并发冲突。

调整后的数据库操作逻辑

替换原有的GetAvailableCode和UpdateVoucherCode两个步骤,使用以下SQL语句:

UPDATE TOP(1) [table]
SET Status = 2
OUTPUT inserted.VoucherCode, inserted.Id
WHERE VoucherId = @voucherId AND Status = 1;

代码优化示例

无需手动设置高隔离级别,默认Read Committed即可满足需求:

public async Task<ClaimVoucherResponse> ClaimVoucher(ClaimVoucherRequest req)
{
    using var connection = new SqlConnection(MSSQLConnectionString);
    await connection.OpenAsync();

    const string claimSql = @"
        UPDATE TOP(1) [table]
        SET Status = 2
        OUTPUT inserted.VoucherCode, inserted.Id
        WHERE VoucherId = @voucherId AND Status = 1;";

    using var cmd = new SqlCommand(claimSql, connection);
    cmd.Parameters.AddWithValue("@voucherId", req.VoucherId);

    using var reader = await cmd.ExecuteReaderAsync();
    if (await reader.ReadAsync())
    {
        return new ClaimVoucherResponse
        {
            VoucherCode = reader.GetString(reader.GetOrdinal("VoucherCode")),
        };
    }

    // 无可用代金券时返回对应结果
    return new ClaimVoucherResponse { VoucherCode = null };
}

方案优势

  1. 原子性保障:单条SQL操作由数据库原子执行,自动锁定目标行,杜绝并发竞争
  2. 无死锁风险:无需依赖Serializable隔离级别,避免范围锁引发的死锁
  3. 性能提升:减少一次数据库往返,降低延迟

内容的提问来源于stack exchange,提问作者Russell

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 23:08:27