.NET Core API实现FIFO处理,避免活动预订并发冲突
解决活动预订并发超卖问题的方案
一、EF Core原生解决方案
这是最简洁的实现路径,直接利用数据库锁机制避免并发冲突,无需额外组件。
1. 悲观锁(行级锁定)
通过数据库的行锁语法,确保同一时间只有一个请求能修改目标活动的名额数据,彻底串行化对同一场活动的预订操作:
public async Task<bool> BookTickets(int eventId, int userId, int quantity) { using var transaction = await _dbContext.Database.BeginTransactionAsync(); try { // 锁定活动行,直到事务结束(SQL Server语法,其他数据库需调整锁语句) var eventEntity = await _dbContext.Events .FromSqlRaw("SELECT * FROM Events WHERE Id = {0} WITH (UPDLOCK, HOLDLOCK)", eventId) .FirstOrDefaultAsync(); if (eventEntity == null || eventEntity.RemainingTickets < quantity) { await transaction.RollbackAsync(); return false; } eventEntity.RemainingTickets -= quantity; _dbContext.Bookings.Add(new Booking { EventId = eventId, UserId = userId, Quantity = quantity }); await _dbContext.SaveChangesAsync(); await transaction.CommitAsync(); return true; } catch { await transaction.RollbackAsync(); throw; } }
2. 乐观锁(版本控制)
给活动表添加版本列,EF Core会自动检测并发修改冲突,适合并发量不是极高的场景,实现更轻量化:
public class Event { public int Id { get; set; } public int RemainingTickets { get; set; } [Timestamp] public byte[] RowVersion { get; set; } } public async Task<bool> BookTickets(int eventId, int userId, int quantity) { try { var eventEntity = await _dbContext.Events.FindAsync(eventId); if (eventEntity == null || eventEntity.RemainingTickets < quantity) return false; eventEntity.RemainingTickets -= quantity; _dbContext.Bookings.Add(new Booking { EventId = eventId, UserId = userId, Quantity = quantity }); await _dbContext.SaveChangesAsync(); return true; } catch (DbUpdateConcurrencyException) { // 并发冲突,返回失败让前端引导用户重试 return false; } }
二、Azure生态下的分布式场景方案
如果系统是多实例部署,单实例锁机制失效,可选用Azure的组件解决:
1. Azure Service Bus队列
将预订请求异步排入队列,用单消费者(或单消费者组)串行处理,彻底避免并发冲突:
- 前端调用API时,直接发送预订消息到队列,返回"排队处理中"给用户
- 后端部署Worker Service或Azure Function,从队列拉取消息逐个执行预订逻辑(配合EF Core锁保证数据库操作安全)
- 处理完成后通过SignalR等方式通知用户结果
这种方式适合高并发场景,但需要额外处理消息重试、死信队列等异步逻辑。
2. Azure Redis分布式锁
用Redis实现分布式信号量,确保同一活动的预订请求同一时间只有一个在执行:
public async Task<bool> BookTicketsWithRedisLock(int eventId, int userId, int quantity) { var lockKey = $"event:{eventId}:booking-lock"; using var redis = await ConnectionMultiplexer.ConnectAsync("your-redis-connection-string"); var db = redis.GetDatabase(); // 获取锁,设置5秒过期时间防止死锁 var lockToken = Guid.NewGuid().ToString(); var lockAcquired = await db.LockTakeAsync(lockKey, lockToken, TimeSpan.FromSeconds(5)); if (!lockAcquired) return false; try { // 复用EF Core的预订逻辑 return await BookTickets(eventId, userId, quantity); } finally { await db.LockReleaseAsync(lockKey, lockToken); } }
三、单实例内存级信号量方案
如果是单实例部署,直接用.NET原生SemaphoreSlim实现内存锁,按活动ID隔离:
private static readonly Dictionary<int, SemaphoreSlim> _eventSemaphores = new(); private static readonly object _semaphoreSyncLock = new(); public async Task<bool> BookTicketsWithSemaphore(int eventId, int userId, int quantity) { SemaphoreSlim semaphore; lock (_semaphoreSyncLock) { if (!_eventSemaphores.TryGetValue(eventId, out semaphore)) { semaphore = new SemaphoreSlim(1, 1); _eventSemaphores[eventId] = semaphore; } } await semaphore.WaitAsync(); try { return await BookTickets(eventId, userId, quantity); } finally { semaphore.Release(); // 可选:活动结束后移除对应信号量节省内存 } }
方案选择建议
- 单实例+低并发:优先用EF Core乐观锁,实现最简洁
- 单实例+高并发:
SemaphoreSlim搭配EF Core悲观锁 - 分布式部署:选Azure Redis分布式锁(同步场景)或Azure Service Bus队列(异步场景)
内容的提问来源于stack exchange,提问作者achilles
相关产品推荐
相关产品推荐

