如何从Web请求执行耗时任务以避免超时并保持站点响应?
ASP.NET Core 从Web请求触发耗时后台任务的实现方案
核心思路
不用局限于计时器型的Hosted Service,我们可以实现基于队列的后台任务处理机制:Web请求只负责把任务加入队列,立即返回响应避免超时;后台服务持续监听队列,异步执行耗时任务,同时支持任务状态查询。
1. 定义任务模型与队列接口
首先创建存储任务信息的模型,以及线程安全的任务队列接口:
// 任务状态枚举 public enum TaskStatus { Pending, Running, Completed, Failed } // 任务模型,包含ID、状态和要调用的API信息 public class BackgroundTaskItem { public Guid TaskId { get; set; } public TaskStatus Status { get; set; } = TaskStatus.Pending; public string TargetApiUrl { get; set; } // 可添加其他业务参数 } // 任务队列接口,用于生产/消费任务 public interface IBackgroundTaskQueue { void EnqueueTask(BackgroundTaskItem task); Task<BackgroundTaskItem> DequeueTaskAsync(CancellationToken cancellationToken); }
2. 实现线程安全的任务队列
用ConcurrentQueue和SemaphoreSlim实现线程安全的队列,保证多线程场景下的可靠性:
public class BackgroundTaskQueue : IBackgroundTaskQueue { private readonly ConcurrentQueue<BackgroundTaskItem> _taskQueue; private readonly SemaphoreSlim _signal; public BackgroundTaskQueue() { _taskQueue = new ConcurrentQueue<BackgroundTaskItem>(); _signal = new SemaphoreSlim(0); } public void EnqueueTask(BackgroundTaskItem task) { if (task == null) throw new ArgumentNullException(nameof(task)); _taskQueue.Enqueue(task); _signal.Release(); // 释放信号通知后台服务有新任务 } public async Task<BackgroundTaskItem> DequeueTaskAsync(CancellationToken cancellationToken) { await _signal.WaitAsync(cancellationToken); // 等待新任务信号 _taskQueue.TryDequeue(out var task); return task; } }
3. 实现后台任务处理服务
继承BackgroundService,监听队列并执行耗时任务,同时维护任务状态:
public class TaskProcessingHostedService : BackgroundService { private readonly IBackgroundTaskQueue _taskQueue; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger<TaskProcessingHostedService> _logger; // 内存存储任务状态,生产环境建议替换为数据库/Redis private readonly ConcurrentDictionary<Guid, BackgroundTaskItem> _taskStatusStore = new(); public TaskProcessingHostedService(IBackgroundTaskQueue taskQueue, IHttpClientFactory httpClientFactory, ILogger<TaskProcessingHostedService> logger) { _taskQueue = taskQueue; _httpClientFactory = httpClientFactory; _logger = logger; } // 提供外部查询任务状态的方法 public BackgroundTaskItem GetTaskStatus(Guid taskId) { _taskStatusStore.TryGetValue(taskId, out var status); return status; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("后台任务处理服务已启动"); while (!stoppingToken.IsCancellationRequested) { try { var taskItem = await _taskQueue.DequeueTaskAsync(stoppingToken); _logger.LogInformation("开始处理任务: {TaskId}", taskItem.TaskId); _taskStatusStore.TryAdd(taskItem.TaskId, taskItem); taskItem.Status = TaskStatus.Running; // 调用目标REST API执行耗时操作 var httpClient = _httpClientFactory.CreateClient(); var response = await httpClient.PostAsync(taskItem.TargetApiUrl, null, stoppingToken); response.EnsureSuccessStatusCode(); taskItem.Status = TaskStatus.Completed; _logger.LogInformation("任务处理完成: {TaskId}", taskItem.TaskId); } catch (OperationCanceledException) { // 服务停止时忽略该异常 } catch (Exception ex) { _logger.LogError(ex, "处理后台任务时发生错误"); // 捕获任务ID并标记为失败 if (ex.Data.Contains("TaskId") && Guid.TryParse(ex.Data["TaskId"].ToString(), out var taskId)) { if (_taskStatusStore.TryGetValue(taskId, out var failedTask)) { failedTask.Status = TaskStatus.Failed; } } } } _logger.LogInformation("后台任务处理服务已停止"); } }
4. 注册服务到依赖注入容器
在Program.cs中注册队列、后台服务和HttpClient工厂:
var builder = WebApplication.CreateBuilder(args); // 注册单例任务队列 builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>(); // 注册后台任务处理服务 builder.Services.AddHostedService<TaskProcessingHostedService>(); // 注册HttpClient工厂,用于调用外部API builder.Services.AddHttpClient(); // 注册控制器等其他服务 builder.Services.AddControllersWithViews(); var app = builder.Build(); // 配置中间件 app.UseRouting(); app.MapControllers(); app.Run();
5. 编写Web接口,触发任务并查询状态
创建控制器处理用户请求,快速返回响应并支持任务状态查询:
[ApiController] [Route("api/tasks")] public class TaskController : ControllerBase { private readonly IBackgroundTaskQueue _taskQueue; private readonly TaskProcessingHostedService _taskService; public TaskController(IBackgroundTaskQueue taskQueue, TaskProcessingHostedService taskService) { _taskQueue = taskQueue; _taskService = taskService; } [HttpPost("start")] public IActionResult StartLongRunningTask([FromBody] TaskStartRequest request) { var taskId = Guid.NewGuid(); var taskItem = new BackgroundTaskItem { TaskId = taskId, TargetApiUrl = request.TargetApiUrl // 填充其他业务参数 }; _taskQueue.EnqueueTask(taskItem); // 立即返回任务ID,无需等待任务完成 return Ok(new { TaskId = taskId, Message = "任务已启动,可通过任务ID查询执行状态" }); } [HttpGet("status/{taskId}")] public IActionResult GetTaskStatus(Guid taskId) { var taskStatus = _taskService.GetTaskStatus(taskId); if (taskStatus == null) { return NotFound("指定任务不存在"); } return Ok(new { TaskId = taskStatus.TaskId, Status = taskStatus.Status.ToString() }); } } // 任务启动请求模型 public class TaskStartRequest { public string TargetApiUrl { get; set; } // 可添加其他业务参数 }
使用说明
- 用户调用
POST /api/tasks/start接口提交任务,接口会立即返回任务ID,完全避免Web请求超时。 - 用户可以通过
GET /api/tasks/status/{taskId}接口轮询任务状态(因为无法用AJAX,用户可以手动刷新页面或定时请求该接口)。 - 后台服务会自动从队列中取出任务,异步调用指定的REST API,执行完成后更新任务状态。
生产环境优化建议
- 任务状态持久化:示例用内存字典存储状态,重启应用会丢失,生产环境建议用SQL Server、Redis等持久化存储。
- 重试机制:给REST API调用添加重试逻辑,处理临时网络故障。
- 并发控制:在后台服务中限制同时执行的任务数量,避免资源耗尽。
内容的提问来源于stack exchange,提问作者Jonathan Wood
相关产品推荐
相关产品推荐

