You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何防止扩缩容后ASP.NET Core托管服务并行执行

最简实现方案:复用现有SQL库做原子化任务认领,无需引入额外组件

你当前的架构下完全不需要新增SQS这类消息组件,也不用承担全表锁的性能和死锁风险,基于现有SQL数据库的原子更新逻辑就能解决多实例并发跑后台任务的问题,代码改动量极小。

核心实现逻辑

不要用「先查所有未发送记录、再处理、最后更新状态」的非原子流程,把「认领待处理任务」和「标记任务归属」合并成单条原子SQL语句,靠数据库自身的行级锁机制,保证同一条任务只会被一个服务实例抢到,从根源上避免重复处理。

你只需要给现有任务表加两个可选的辅助字段(不想加字段也可以用状态位实现):

  • LockedAt:datetime类型,记录任务被认领的时间,用于超时回收
  • LockedBy:字符串类型,记录认领任务的实例ID,方便问题排查

以和ASP.NET Core栈最匹配的SQL Server为例,认领任务的SQL语句可以这么写:

UPDATE TOP (20) YourTaskTable
SET
    LockedAt = GETUTCDATE(),
    LockedBy = @CurrentServiceInstanceId
OUTPUT INSERTED.Id, INSERTED.EmailAddress, INSERTED.ReportData
WHERE
    IsEmailSent = 0
    AND IsReportSent = 0
    AND (LockedAt IS NULL OR LockedAt < DATEADD(MINUTE, -5, GETUTCDATE()))
ORDER BY CreateTime ASC

语句逻辑很简单:每次批量捞最多20条未发送、且未被认领(或认领超过5分钟超时)的任务,直接标记为当前实例持有,同时把任务详情通过OUTPUT子句返回给当前实例处理。整个语句是原子执行的,哪怕同时有10个服务实例跑这段逻辑,同一条记录也只会被一个实例抢到。

为什么这个方案比你列的两个选项更合适

  • 对比数据库全表锁方案:没有长时间锁表的问题,仅在更新瞬间对命中的行加行级更新锁,语句执行耗时在毫秒级,几乎不会对数据库性能造成影响,也基本没有死锁风险
  • 对比引入SQS方案:完全复用你现有的技术栈,不需要额外学习、配置、维护新的AWS服务,也不用改造现有任务的存储逻辑,开发量可以压到小时级

落地注意事项

  • 每个Fargate服务实例启动时生成一个唯一Guid作为自身的实例ID,传入@CurrentServiceInstanceId参数即可,不需要额外的服务注册逻辑
  • 任务处理完成后,单独执行更新语句把对应记录的IsEmailSent、IsReportSent字段置为1即可
  • 超时时间根据你单条任务的平均处理时长调整即可,比如邮件、报表发送一般单条处理不超过1分钟,设5分钟超时足够覆盖异常场景:如果某个实例拿到任务后意外崩溃,超时后任务会自动被其他实例重新认领,不会出现任务丢失
  • 后台服务的轮询间隔设为10~30秒即可,不会给数据库造成额外压力,完全满足这类非实时通知任务的时效要求

内容的提问来源于stack exchange,提问作者Vlado Pandžić

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 08:18:28