找回密码
 立即注册
快捷导航

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

[复制链接]
admin 2024-12-14 07:34:05 | 显示全部楼层

在 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 方法检索这些任务进行处理。

任务队列实现

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

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 等外部库。我们创建了一个后台服务来处理任务,并展示了一种从队列中将任务排入队列并发送电子邮件的方法。此方法可帮助您很好地处理耗时的任务,同时保持应用程序的响应性。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

温馨提示

关于 注册码 问题

      由于近期经常大量注册机器人注册发送大量广告,本站开启免费入群领取注册码注册网站账号,注册码在群公告上贴着...

关于 注册码 问题

      由于近期经常大量注册机器人注册发送大量广告,本站开启免费入群领取注册码注册网站账号,注册码在群公告上贴着...

Archiver|手机版|小黑屋|DLSite

GMT+8, 2025-1-18 13:13

Powered by Discuz! X3.5 and PHP8

快速回复 返回顶部 返回列表