.NET 6 API并发请求下Azure Service Bus主题出现重复消息的问题及两大疑问
这两个问题其实都和.NET的任务调度、变量捕获机制密切相关,咱们一个个拆解清楚:
1. 为什么主请求结束后,内嵌任务还能把所有消息发出去?
你之前的理解有误——请求的主任务结束,并不意味着所有关联的后台任务会被立刻终止。
你创建的Task是通过task.Start()调度到线程池线程上执行的。ASP.NET的请求上下文(比如HttpContext)会在主请求完成后被回收,但线程池是独立于请求上下文存在的:只要你的API进程还在运行,线程池就会继续处理这些排队的后台任务,直到它们执行完毕。除非你的应用进程在任务完成前意外终止(比如重启、崩溃),否则这些发送消息的任务都会跑完,所以10条消息都能成功抵达Service Bus。
另外补充一点:你用new Task(async () => { ... })的方式创建任务,其实是把异步委托包装在了一个普通Task里,这种方式会导致外层Task在异步委托执行到第一个await时就标记为完成,但内层的异步操作还是会继续执行,直到SendMessageAsync完成,这也是消息能发送成功的原因之一。
2. 为什么会出现ID重复、部分消息缺失的情况?
这是典型的闭包变量捕获陷阱,核心原因是:异步任务捕获的是变量的引用,而不是变量的当前值。
假设你的message是一个引用类型的实例(比如自定义的Message类),如果这个实例存在被复用的情况(比如用了对象池、或者是共享的类字段),当异步任务被线程池调度执行时,message的Name属性可能已经被后续的请求修改了——比如请求0的任务还没开始执行,请求2已经拿到了同一个message实例并修改了Name,这时候请求0的任务执行时,拿到的就是请求2的ID,导致重复。
就算message是请求方法里的局部变量,如果你的测试是用循环批量处理请求(而非真正的10个独立并发请求),循环变量的引用会被所有异步任务共享,当循环快速迭代时,任务执行到message.Name时,循环变量已经指向了后续的实例,同样会导致ID重复或缺失。
快速修复方案
要避免这个问题,你可以在创建异步任务前,先把需要用到的变量值单独提取出来,让任务捕获这些值而非原变量的引用:
// 先把需要的值保存到局部变量,避免捕获原对象的引用 var messageName = message.Name; var messageBody = message.Body; // 推荐用Task.Run替代new Task(...),更符合异步任务的调度规范 _ = Task.Run(async () => { var messageObject = new { messageName, messageBody }; var messageJson = JsonConvert.SerializeObject(messageObject); using var sender = _serviceBusClient.CreateSender(_topicName); // 记得用using释放sender资源 await sender.SendMessageAsync(new ServiceBusMessage(messageJson)); });
另外,生产环境用BackgroundService是非常正确的选择,它能更好地管理后台任务的生命周期,避免这类陷阱。
内容的提问来源于stack exchange,提问作者David Mason

