ASP.NET Web API中30分钟长运行SQL脚本无外部组件处理方案咨询
纯内部实现方案(无需引入任何外部组件)
核心逻辑为异步提交+状态跟踪+结果轮询,完全依托ASP.NET Core内置能力实现,不会引入额外依赖。
具体实现步骤
- 定义任务状态存储结构
声明任务状态枚举和实体类,用于记录长SQL任务的执行进度、结果、错误信息。
public enum TaskStatus { Pending, Running, Success, Failed } public class SqlTaskState { public Guid TaskId { get; set; } public TaskStatus Status { get; set; } public string? Result { get; set; } public string? ErrorMessage { get; set; } public DateTime CreatedAt { get; set; } }- 定义任务状态存储结构
- 实现后台长任务托管服务
继承ASP.NET Core内置的BackgroundService实现后台任务调度,内部用线程安全的队列接收待执行的SQL任务,异步消费执行,全程不会阻塞接口响应。
public class LongRunningSqlService : BackgroundService { private readonly ConcurrentQueue<Guid> _taskQueue = new(); private readonly ConcurrentDictionary<Guid, SqlTaskState> _taskStates; private readonly IServiceScopeFactory _scopeFactory; public LongRunningSqlService(ConcurrentDictionary<Guid, SqlTaskState> taskStates, IServiceScopeFactory scopeFactory) { _taskStates = taskStates; _scopeFactory = scopeFactory; } // 外部调用提交任务 public void EnqueueTask(Guid taskId) { _taskQueue.Enqueue(taskId); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { if (_taskQueue.TryDequeue(out var taskId)) { if (_taskStates.TryGetValue(taskId, out var state)) { state.Status = TaskStatus.Running; try { // 创建独立服务Scope,避免和请求生命周期绑定 using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<你的DbContext类名>(); // 执行长SQL,务必设置CommandTimeout大于30分钟,这里设为40分钟留余量 var result = await dbContext.Database.ExecuteSqlRawAsync( "你的长SQL脚本内容", stoppingToken ); // 如果是查询类SQL可以自行替换为对应的查询逻辑 state.Result = result.ToString(); state.Status = TaskStatus.Success; } catch (Exception ex) { state.Status = TaskStatus.Failed; state.ErrorMessage = ex.ToString(); } } } // 队列空时休息1秒避免CPU空转 await Task.Delay(1000, stoppingToken); } } }- 实现后台长任务托管服务
- 注册服务到依赖注入容器
在Program.cs中添加如下注册逻辑:
// 注册线程安全的任务状态存储,单例模式全局共享 builder.Services.AddSingleton<ConcurrentDictionary<Guid, SqlTaskState>>(); // 注册后台长任务服务 builder.Services.AddHostedService<LongRunningSqlService>();- 注册服务到依赖注入容器
- 实现业务接口
改造原有接口为异步提交接口,新增结果查询接口供调用方轮询:
[ApiController] [Route("api/[controller]")] public class LongSqlTaskController : ControllerBase { private readonly ConcurrentDictionary<Guid, SqlTaskState> _taskStates; private readonly LongRunningSqlService _sqlTaskService; public LongSqlTaskController(ConcurrentDictionary<Guid, SqlTaskState> taskStates, LongRunningSqlService sqlTaskService) { _taskStates = taskStates; _sqlTaskService = sqlTaskService; } // 触发长SQL执行的接口,会在毫秒级返回,不会触发230秒超时 [HttpPost("trigger")] public IActionResult TriggerLongSql() { var taskId = Guid.NewGuid(); var taskState = new SqlTaskState { TaskId = taskId, Status = TaskStatus.Pending, CreatedAt = DateTime.UtcNow }; _taskStates.TryAdd(taskId, taskState); _sqlTaskService.EnqueueTask(taskId); // 返回202状态码,附带结果查询地址 return Accepted($"api/LongSqlTask/result/{taskId}", new { TaskId = taskId, Message = "任务已提交,可通过返回的查询地址获取执行结果" }); } // 结果查询接口,供调用方轮询 [HttpGet("result/{taskId:guid}")] public IActionResult GetTaskResult(Guid taskId) { if (!_taskStates.TryGetValue(taskId, out var state)) { return NotFound("指定任务不存在"); } return Ok(state); } }- 实现业务接口
注意事项
- 上述实现默认用内存存储任务状态,单实例场景下完全可用,如果App Service发生重启、进程回收,未完成的任务和历史状态会丢失,对任务可靠性要求高的场景可以把任务状态和队列存储到现有业务数据库中,后台服务定时扫库拉取待执行任务即可,也无需引入外部组件。
- 建议打开Azure App Service的Always On开关,避免空闲进程被系统回收导致后台任务中断,该配置属于平台基础配置,不算引入外部组件。
- 如果调用方不想轮询,可以在任务执行完成后主动调用调用方提供的回调地址推送结果,该逻辑也可以完全在现有项目内实现。
多实例兼容方案
如果你的App Service开启了多实例缩放,默认内存存储的状态无法跨实例共享,可通过以下两种方式兼容,无需引入外部组件:
- 开启App Service的会话粘性(ARR Affinity),让同一个客户端的所有请求都落到同一个实例上,即可正常查询状态
- 把任务状态存储到所有实例都能访问的共享数据库中,所有实例的后台服务都从同一个库拉取任务、更新状态
内容的提问来源于stack exchange,提问作者sprash
相关产品推荐
相关产品推荐

