C# async/await场景下Controller遇并发请求时如何实现按队列串行处理
实现全局请求串行处理的方案
核心原理
ASP.NET Core默认采用并行模式处理请求,要实现严格串行效果(即上一个请求的DB保存全流程完成后再处理下一个),核心是选用可异步等待的全局共享同步原语,替代传统不支持异步流程的同步锁。这里推荐使用单例模式下计数为1的SemaphoreSlim:它既可以配合await异步等待、不占用阻塞线程池资源,内部自带FIFO等待队列,也完全适配async/await的异步执行逻辑。
具体实现步骤
1. 注册全局共享的信号量单例
不能在Controller内直接new信号量实例——因为ASP.NET Core默认每个请求都会创建独立的Controller对象,各自持有信号量的话根本起不到跨请求的锁作用。需要把信号量注册为应用级别的单例:
// .NET 6+ 顶级语句格式的Program.cs中 builder.Services.AddSingleton(new SemaphoreSlim(initialCount: 1, maxCount: 1));
参数说明:初始计数为1、最大并发数为1,即同一时间最多只允许一个执行流拿到许可,其余请求全部按照到达顺序进入等待队列。
2. Controller中注入信号量包裹核心逻辑
把需要串行执行的DB保存逻辑,用异步等待包裹,确保逻辑执行完成后自动释放许可:
[ApiController] [Route("api/data")] public class DataController : ControllerBase { private readonly SemaphoreSlim _serialLock; private readonly AppDbContext _dbContext; // 构造注入单例的信号量 public DataController(SemaphoreSlim serialLock, AppDbContext dbContext) { _serialLock = serialLock; _dbContext = dbContext; } [HttpPost("save")] public async Task<IActionResult> SaveRecord([FromBody] RecordModel record) { // 异步等待获取许可,不阻塞线程池 await _serialLock.WaitAsync(); try { // 以下为需要严格串行的核心DB操作 await _dbContext.Records.AddAsync(record); await _dbContext.SaveChangesAsync(); return Ok("保存成功"); } finally { // 无论逻辑成功或抛异常,都必须释放许可 _serialLock.Release(); } } }
常见踩坑说明
- 绝对不要用传统
lock关键字实现:lock是基于线程的同步原语,不支持在块内使用await,强行使用会出现编译错误、死锁或者锁失效的问题 - 必须在
finally块中释放许可:如果业务逻辑抛异常后没执行Release,信号量的计数会永久卡在0,后续所有请求都会无限等待直到应用重启 - 信号量必须是单例:如果是Controller的实例字段、或者作用域Scoped注册的,每个请求拿到的都是独立的信号量实例,完全无法实现全局串行效果
- 队列顺序天然符合需求:
SemaphoreSlim内部的等待队列是按照请求到达的先后顺序唤醒的,刚好匹配“第一个请求完成DB保存后再处理下一个”的要求
如果后续需要更复杂的队列能力(比如队列持久化、超时自动丢弃请求、失败重试),可以基于System.Threading.Channels实现独立的后台消费队列,把所有请求的执行项丢进队列后由单个后台任务逐个执行,核心串行逻辑和上述实现一致。
内容的提问来源于stack exchange,提问作者lukaG96
相关产品推荐
相关产品推荐

