如何在ASP.NET Core 6 Web API中实现多请求排队?
ASP.NET Core 6 Web API 打印请求串行排队实现方案
一、核心思路
用单线程任务队列串行处理所有打印请求,同时通过创建独立服务作用域解决DBContext丢失问题——之前的方案之所以丢DBContext,是因为直接把请求线程里的Scoped对象传到后台线程,请求结束后DBContext会被框架回收;而在队列任务里单独创建作用域,就能保证DBContext和任务生命周期绑定,不会提前释放。
二、具体实现步骤
1. 注册单例任务队列服务
先写一个线程安全的任务队列,确保同一时间只有一个任务在执行:
public class PrintTaskQueue { private readonly ConcurrentQueue<Func<Task>> _tasks = new(); private readonly SemaphoreSlim _semaphore = new(0); private readonly IServiceProvider _serviceProvider; private bool _isProcessing; public PrintTaskQueue(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } // 入队打印任务,参数是需要作用域的委托 public void EnqueuePrintTask(Func<IServiceScope, Task> printTask) { _tasks.Enqueue(async () => { // 每个任务单独创建服务作用域 using var scope = _serviceProvider.CreateScope(); await printTask(scope); }); _semaphore.Release(); StartProcessingIfNeeded(); } // 启动任务处理循环,确保只开一个线程处理 private void StartProcessingIfNeeded() { if (_isProcessing) return; _isProcessing = true; Task.Run(async () => { while (true) { await _semaphore.WaitAsync(); if (_tasks.TryDequeue(out var task)) { try { await task(); } catch (Exception ex) { // 这里加日志记录,比如打印失败的原因 } } else { _isProcessing = false; break; } } }); } }
然后在Program.cs里把这个队列注册成单例:
builder.Services.AddSingleton<PrintTaskQueue>();
2. 实现Scoped打印服务
把打印逻辑和DBContext操作封装到Scoped服务里,这样每次任务都会拿到新鲜的DBContext:
public class PrintService { private readonly AppDbContext _dbContext; public PrintService(AppDbContext dbContext) { _dbContext = dbContext; } public async Task ProcessPrintJob(PrintObject printObj) { // 这里放心用DBContext,因为在当前任务的作用域内 // 比如先把打印记录存到数据库 var printLog = new PrintLog { Content = printObj.Content, CreateTime = DateTime.Now }; _dbContext.PrintLogs.Add(printLog); await _dbContext.SaveChangesAsync(); // 调用打印机SDK执行实际打印 await SendToPrinter(printObj); } private Task SendToPrinter(PrintObject printObj) { // 模拟打印耗时操作,替换成实际的打印机调用逻辑 return Task.Delay(2000); } }
同样在Program.cs注册成Scoped:
builder.Services.AddScoped<PrintService>();
3. API控制器处理请求
控制器里只负责把请求丢进队列,不用等打印完成,直接返回响应:
[ApiController] [Route("api/print")] public class PrintController : ControllerBase { private readonly PrintTaskQueue _printTaskQueue; public PrintController(PrintTaskQueue printTaskQueue) { _printTaskQueue = printTaskQueue; } [HttpPost] public IActionResult TriggerPrint([FromBody] PrintObject printObj) { _printTaskQueue.EnqueuePrintTask(async scope => { // 从当前作用域获取打印服务 var printService = scope.ServiceProvider.GetRequiredService<PrintService>(); await printService.ProcessPrintJob(printObj); }); return Ok("打印请求已排队,稍后处理"); } }
三、要注意的几个点
- DBContext不丢的关键:必须在任务内部创建
IServiceScope,通过Scope获取的服务都是当前任务专属的,不会被其他请求回收。 - 串行执行保证:队列的处理循环是单线程的,加上SemaphoreSlim控制任务触发,确保同一时间只有一个打印任务在跑。
- 异常不崩队列:任务执行时加try-catch,单个任务失败不会导致整个队列停摆,同时可以记录异常日志排查问题。
- 接口响应快:客户端调用API后立即收到返回,不用等打印完成,体验更好。
四、可选替代方案:Hangfire
如果需要更高级的功能(比如任务重试、后台监控),可以用Hangfire:
- 安装Hangfire相关包,配置存储(比如SQL Server)
- 发布打印任务时指定同一个队列名,然后配置Hangfire只启动一个Worker处理该队列,就能实现串行执行。不过这个方案比自定义队列重一些,简单场景用自定义队列足够。
内容的提问来源于stack exchange,提问作者StarX
相关产品推荐
相关产品推荐

