Cách Xây Dựng Backgound Job Queue Trong Bộ Nhớ Cho ASP.NET Core Từ Đầu

Trong các ứng dụng web, việc cung cấp API nhanh chóng và đáp ứng tốt là rất quan trọng cho trải nghiệm người dùng. Tuy nhiên, một số thao tác như gửi email, tạo báo cáo hoặc thêm người dùng hàng loạt có thể mất nhiều thời gian. Thực hiện các tác vụ này trong quá trình xử lý yêu cầu có thể chặn các thread, dẫn đến thời gian phản hồi chậm.

Để giảm thiểu đáng kể vấn đề này, sẽ hiệu quả hơn nếu chạy các tác vụ dài trong một quá trình nền. Thay vì đợi yêu cầu hoàn thành, chúng ta có thể xếp hàng đợi tác vụ, cung cấp phản hồi ngay lập tức cho người dùng và thực hiện công việc trong một quá trình nền riêng biệt.

Mặc dù có các thư viện như Hangfire hoặc message broker như RabbitMQ cho mục đích này, bạn không cần bất kỳ phụ thuộc bên ngoài nào. Trong bài đăng blog này, chúng ta sẽ xây dựng một hàng đợi công việc nền trong bộ nhớ từ đầu trong ASP.NET Core chỉ sử dụng các tính năng .NET tích hợp.

Các Thành Phần Cốt Lõi Của Hệ Thống Background Job

IHostedService và BackgroundService là gì?

IHostedService là một interface được sử dụng trong ASP.NET Core để triển khai các tác vụ nền chạy dài được quản lý bởi vòng đời ứng dụng. Khi ứng dụng của bạn khởi động, nó sẽ khởi động tất cả các ứng dụng IHostedService đã đăng ký.

BackgroundService là một lớp cơ sở trừu tượng triển khai IHostedService. Nó cung cấp cho bạn một cách hiệu quả hơn để tạo một dịch vụ định thời bằng cách cung cấp một phương thức có thể ghi đè duy nhất ExecuteAsync(CancellationToken stoppingToken).

Cấu Trúc Producer/Consumer

  • Producer: Đây là phần ứng dụng của bạn tạo ra các công việc và thêm chúng vào hàng đợi. Trong ví dụ của chúng tôi, producer sẽ là một API Controller.
  • Consumer: Đây là một dịch vụ nền liên tục theo dõi hàng đợi, kéo công việc từ hàng đợi và thực thi chúng. Ứng dụng BackgroundService của chúng tôi sẽ là consumer.
  • Queue: Đây là cấu trúc dữ liệu giữa producer và consumer chứa các công việc đang chờ được xử lý.

Tại Sao Lại Sử Dụng System.Threading.Channels?

Được giới thiệu trong .NET Core 3.0, System.Threading.Channels cung cấp một cấu trúc dữ liệu đồng bộ được thiết kế cho các kịch bản producer, consumer không đồng bộ.

Nó xử lý tất cả các hoạt động khóa và đồng bộ hóa phức tạp bên trong, có thể sử dụng từ nhiều thread và được tối ưu hóa cho tốc độ và phân bổ bộ nhớ thấp.

Triển Khai Background Job Queue

Tạo Dự Án

dotnet new webapi -n BackgroundJobQueue.Api

Định Nghĩa Interface Job Queue

Tạo file IBackgroundTaskQueue.cs:


namespace BackgroundJobQueue;

public interface IBackgroundTaskQueue
{
ValueTask EnqueueAsync(Func<CancellationToken, ValueTask> workItem);
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken);
}

Triển Khai Queue Service Với Channels

Tạo file BackgroundTaskQueue.cs:


using System.Threading.Channels;

namespace BackgroundJobQueue;

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

public BackgroundTaskQueue(int capacity)
{
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
}

public async ValueTask EnqueueAsync(Func<CancellationToken, ValueTask> workItem)
{
if (workItem is null)
{
throw new ArgumentNullException(nameof(workItem));
}
await _queue.Writer.WriteAsync(workItem);
}

public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken)
{
var workItem = await _queue.Reader.ReadAsync(cancellationToken);
return workItem;
}
}

Tạo Consumer Service (QueuedHostedService)

Tạo file QueuedHostedService.cs:


namespace BackgroundJobQueue;

public class QueuedHostedService : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly ILogger<QueuedHostedService> _logger;

public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger)
{
_taskQueue = taskQueue;
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queued Hosted Service is running.");

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

try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred executing {WorkItem}.", nameof(workItem));
}
}
_logger.LogInformation("Queued Hosted Service is stopping.");
}
}

Đăng Ký Dịch Vụ Trong Program.cs


using BackgroundJobQueue;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IBackgroundTaskQueue>(_ =>
{
if (!int.TryParse(builder.Configuration["QueueCapacity"], out var queueCapacity))
{
queueCapacity = 100;
}
return new BackgroundTaskQueue(queueCapacity);
});

builder.Services.AddHostedService<QueuedHostedService>();

var app = builder.Build();
app.Run();

Tạo Producer (API Controller)

Tạo JobsController.cs:


using Microsoft.AspNetCore.Mvc;

namespace BackgroundJobQueue.Controllers;

[ApiController]
[Route("[controller]")]
public class JobsController : ControllerBase
{
private readonly IBackgroundTaskQueue _queue;
private readonly ILogger<JobsController> _logger;

public JobsController(IBackgroundTaskQueue queue, ILogger<JobsController> logger)
{
_queue = queue;
_logger = logger;
}

[HttpPost]
public async Task<IActionResult> EnqueueJob()
{
await _queue.EnqueueAsync(async token =>
{
var guid = Guid.NewGuid();
_logger.LogInformation("Job {Guid} started.", guid);
await Task.Delay(TimeSpan.FromSeconds(5), token);
_logger.LogInformation("Job {Guid} finished.", guid);
});

return Ok("Job has been enqueued.");
}
}

Cân Nhắc Quan Trọng

  • Shutdown: CancellationToken quan trọng khi dừng ứng dụng
  • Error Handling: try-catch ngăn chặn crash toàn bộ service
  • Giới Hạn: Queue trong bộ nhớ sẽ mất dữ liệu nếu ứng dụng restart
  • Khi Nào Dùng External Libraries: Khi cần đảm bảo persistency hoặc distributed system

Kết Luận

Chúng ta đã xây dựng một background job queue hiệu suất cao trong bộ nhớ cho ASP.NET Core chỉ sử dụng các tính năng framework tích hợp. Đây là một cách để tăng khả năng phản hồi của API và cải thiện trải nghiệm người dùng bằng cách xử lý các tác vụ chạy dài trong nền.

Sử dụng IHostedService và System.Threading.Channels, chúng ta đã tạo ra một triển khai hiệu quả của mô hình producer-consumer. Trong khi cách tiếp cận trong bộ nhớ này có một số hạn chế, nó là một công cụ mạnh mẽ cho nhiều kịch bản phổ biến.

Chỉ mục