在 ASP.NET Core 中构建自定义后台任务队列,无需 hangfire

科技   2024-11-09 05:11   上海  


在 ASP.NET Core 中构建可扩展的 Web 应用程序时,通常需要执行耗时的任务,例如发送电子邮件、数据处理或调用外部 API,而不会阻止主要的请求-响应流。在后台运行这些操作可以显著提高应用程序性能。

在这里,我们将学习如何在不使用 Hangfire 等库的情况下创建自定义后台任务队列和处理器。我们将演示如何使用 QueueBackgroundWorkItem 方法将作业传递到后台服务,并从 _API 控制器_触发_后台_任务,包括发送电子邮件作为示例。

为什么使用后台作业?

_后台_作业对于不需要阻止用户与应用程序交互的任务至关重要。例如:

  • 电子邮件通知: 在用户操作后发送电子邮件。

  • 长时间运行的进程: 执行数据密集型操作。

  • 第三方 API 调用: 与外部服务的非阻塞交互。

通过将这些任务排队以在后台运行,我们可以释放服务器来处理不同的请求,从而提高应用程序的总体效率。

了解后台任务队列

ASP.NET Core 的 BackgroundService 提供了一种实现长时间运行的后台任务的方法。为了使其更具适应性,我们可以设置一个后台任务队列,以便我们添加要稍后处理的任务。排队的任务将由后台 worker 异步处理。

设置任务队列

首先,我们将为后台任务队列定义一个接口:

public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem);
Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

QueueBackgroundWorkItem 方法允许对任务进行排队,而 DequeueAsync 方法检索这些任务进行处理。

任务队列实现

接下来,我们使用 ConcurrentQueue 和 SemaphoreSlim 实现此接口,以便在新任务可用时发出信号:

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly SemaphoreSlim _signal = new(0);
private readonly ConcurrentQueue<Func<IServiceProvider, CancellationToken, Task>> _workItems = new();

public void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem)
{
if (workItem == null) throw new ArgumentNullException(nameof(workItem));

_workItems.Enqueue(workItem);
_signal.Release(); // Signal that a new item is available
}

public async Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);

return workItem!;
}
}

这个类允许我们使用 ConcurrentQueue 以线程安全的方式将任务排入队列,并在添加任务时向后台服务发出信号以开始处理。

创建后台处理器

队列就位后,我们需要一个后台服务来处理排队的任务。ASP.NET Core 的 BackgroundService 是实现此目的的理想候选项:

public class QueuedProcessorBackgroundService : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;

public QueuedProcessorBackgroundService(IBackgroundTaskQueue taskQueue,
IServiceProvider serviceProvider,
ILoggerFactory loggerFactory)
{
_taskQueue = taskQueue;
_serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger<QueuedProcessorBackgroundService>();
}

protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Queued Processor Background Service is starting.");

while (!cancellationToken.IsCancellationRequested)
{
var workItem = await _taskQueue.DequeueAsync(cancellationToken);

try
{
await workItem(_serviceProvider, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error occurred executing {nameof(workItem)}.");
}
}

_logger.LogInformation("Queued Processor Background Service is stopping.");
}
}

此服务持续检查任务队列,并在任务出队时对其进行处理。如果服务已停止,则取消令牌可确保正常关闭任何正在进行的任务。

从 API 将作业排队

现在,我们可以创建一个 API 终端节点,用于将作业排队以进行后台处理。该作业将从 IServiceProvider 中解析所需的服务(如 _IEmailService_),并异步处理它们。

[ApiController]
[Route("api/[controller]")]
public class JobController : ControllerBase
{
private readonly IBackgroundTaskQueue _taskQueue;

public JobController(IBackgroundTaskQueue taskQueue)
{
_taskQueue = taskQueue;
}

[HttpPost("enqueue-email")]
public IActionResult EnqueueEmailJob([FromBody] EmailRequest emailRequest)
{
_taskQueue.QueueBackgroundWorkItem(async (serviceProvider, token) =>
{
var logger = serviceProvider.GetRequiredService<ILogger<JobController>>();
var emailService = serviceProvider.GetRequiredService<IEmailService>();

logger.LogInformation("Email job started");

try
{
// Send the email
await emailService.SendEmailAsync(emailRequest.To, emailRequest.Subject, emailRequest.Body, token);
logger.LogInformation("Email job completed successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Error occurred while sending email.");
}
});

return Ok("Email job has been queued.");
}
}

此 API 终端节点接收电子邮件请求,并使用任务队列对电子邮件发送作业进行排队。

完整示例:在后台发送电子邮件

要在后台发送电子邮件,我们将定义一个模型 EmailRequest 来处理传入的电子邮件数据,并定义一个电子邮件服务来模拟发送电子邮件。

EmailRequest 模型

public class EmailRequest  
{
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}

IEmailService 和 EmailService 实现

public interface IEmailService
{
Task SendEmailAsync(string to, string subject, string body, CancellationToken token);
}

public class EmailService : IEmailService
{
private readonly ILogger<EmailService> _logger;

public EmailService(ILogger<EmailService> logger)
{
_logger = logger;
}

public async Task SendEmailAsync(string to, string subject, string body, CancellationToken token)
{
_logger.LogInformation($"Sending email to {to} with subject {subject}.");

// Simulate email sending delay
await Task.Delay(2000, token);

_logger.LogInformation($"Email to {to} sent successfully.");
}
}

此服务模拟发送具有较小延迟的电子邮件。在实际场景中,这将涉及与 SMTP 服务器或第三方电子邮件提供商(如 SendGrid)集成。

在 Startup.cs 中注册服务

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
services.AddHostedService<QueuedProcessorBackgroundService>();
services.AddSingleton<IEmailService, EmailService>();
services.AddControllers();
services.AddLogging();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}

测试 API
现在,您可以通过向 /api/job/enqueue-email 终端节点发送 POST 请求来触发电子邮件作业:

POST /api/job/enqueue-email  
Content-Type: application/json

{
"to": "example@example.com",
"subject": "Test Email",
"body": "This is a test email body."
}

这会将电子邮件作业排入队列,后台服务将处理它,而不会阻止 API 响应。

最佳实践

尊重取消令牌: 始终确保您的后台任务遵循 CancellationToken 以允许正常关闭任务。

错误处理: 在后台作业中实施适当的错误处理,以处理任何故障并提供适当的日志记录。

依赖项解析: 在 QueueBackgroundWorkItem 中正确使用 IServiceProvider 以确保正确的服务生存期(例如,范围服务)。

监测: 考虑使用日志记录或监控工具来跟踪排队和已处理的任务。

在这里,我们构建了一个轻量级解决方案,用于在 ASP.NET Core 中运行后台作业,而无需依赖 Hangfire 等外部库。我们创建了一个后台服务来处理任务,并展示了一种从队列中将任务排入队列并发送电子邮件的方法。此方法可帮助您很好地处理耗时的任务,同时保持应用程序的响应性。

如果你喜欢我的文章,请给我一个赞!谢谢

架构师老卢
资深软件架构师, 分享编程、软件设计经验, 教授前沿技术, 分享技术资源(每天发布电子书),每天进步一点点...
 最新文章