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 }; }
方案优势
- 原子性保障:单条SQL操作由数据库原子执行,自动锁定目标行,杜绝并发竞争
- 无死锁风险:无需依赖Serializable隔离级别,避免范围锁引发的死锁
- 性能提升:减少一次数据库往返,降低延迟
内容的提问来源于stack exchange,提问作者Russell
相关产品推荐
相关产品推荐

