Trong kiến trúc hệ thống phân tán, việc đồng bộ hóa truy cập tài nguyên là một thách thức lớn khi nhiều tiến trình chạy song song. Các vấn đề thường gặp bao gồm: xung đột khi ghi dữ liệu cùng một lúc vào cơ sở dữ liệu, các tác vụ nền được thực hiện lặp lại trên nhiều node khác nhau, hoặc hiện tượng ghi đè dữ liệu cache. Để giải quyết các vấn đề về xung đột đồng thời và đảm bảo tính nhất quán của dữ liệu, việc áp dụng cơ chế khóa phân tán (distributed lock) là điều cần thiết.
Bài viết này sẽ giới thiệu DistributedLock - một thư viện .NET hiệu quả và linh hoạt giúp giải quyết bài toán này.
Tổng quan về DistributedLock
DistributedLock là một thư viện .NET nhẹ, được thiết kế để đảm bảo an toàn luồng trong môi trường phân tán. Ưu điểm lớn nhất của thư viện này là tính linh hoạt trong việc lựa chọn backend lưu trữ trạng thái khóa. Nó hỗ trợ đa dạng các hệ thống lưu trữ phổ biến như:
- Redis
- SQL Server
- PostgreSQL
- MySQL
- MongoDB
- Bộ nhớ trong (In-memory) cho các trường hợp kiểm thử cục bộ
Việc chuyển đổi giữa các backend rất đơn giản, giúp ứng dụng có thể thích ứng với nhiều hạ tầng khác nhau mà không thay đổi logic nghiệp vụ.
Cấu hình và cài đặt
Để bắt đầu, bạn cần cài đặt các gói NuGet tương ứng với backend mà bạn dự định sử dụng:
dotnet add package DistributedLock
dotnet add package DistributedLock.Redis
dotnet add package DistributedLock.SqlServer
Sử dụng với Redis
Đối với Redis, thư viện tận dụng StackExchange.Redis để thiết lập kết nối và quản lý khóa. Dưới đây là ví dụ về cách triển khai:
var redisConfig = "localhost:6379,allowAdmin=true";
var resourceKey = "InventoryUpdate";
var multiplexer = await ConnectionMultiplexer.ConnectAsync(redisConfig);
var redisLock = new RedisDistributedLock(resourceKey, multiplexer.GetDatabase());
// Sử dụng await using để đảm bảo giải phóng khóa sau khi hoàn thành
await using (var handle = await redisLock.TryAcquireAsync())
{
if (handle != null)
{
// Logic thực thi khi đã chiếm giữ thành công khóa
Console.WriteLine("Đã có khóa Redis, đang xử lý...");
}
}
Sử dụng với SQL Server
Trong trường hợp sử dụng SQL Server, thư viện tận dụng các cơ chế khóa cấp ứng dụng hoặc cấp cơ sở dữ liệu của hệ quản trị này. Ví dụ sau minh họa cách sử dụng trong ứng dụng Console:
using Medallion.Threading.SqlServer;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LockExample
{
class Program
{
static async Task Main(string[] args)
{
var dbConnection = "Server=tcp:myserver,1433;Database=AppDb;User ID=sa;Password=pass;";
var syncObjectName = "OrderProcessing";
// Khởi tạo đối tượng khóa
var dbLock = new SqlDistributedLock(syncObjectName, dbConnection);
// Thử lấy khóa với thời gian chờ là 30 giây
using (var acquiredLock = dbLock.TryAcquire(TimeSpan.FromSeconds(30)))
{
if (acquiredLock != null) // Kiểm tra xem có lấy được khóa không
{
// Khu vực an toàn: thực hiện logic nghiệp vụ
Console.WriteLine("Đã chiếm giữ khóa thành công, đang thực hiện tác vụ...");
await Task.Delay(1000);
}
else
{
Console.WriteLine("Không thể lấy khóa do timeout.");
}
} // Khóa được tự động giải phóng ở đây (Dispose)
}
}
}
Cách tiếp cận này rất hữu ích trong các hệ thống sử dụng Quartz.NET hoặc Hangfire để ngăn chặn việc các job chạy chồng chéo nhau.
Tích hợp Dependency Injection
DistributedLock hỗ trợ đầy đủ Dependency Injection, giúp việc quản lý vòng đời của các đối tượng khóa dễ dàng hơn trong các ứng dụng ASP.NET Core.
Cấu hình trong appsettings.json:
{
"ConnectionStrings": {
"MyDb": "Server=tcp:myserver,1433;Database=AppDb;User ID=sa;Password=pass;"
}
}
Đăng ký dịch vụ trong Program.cs hoặc Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
var connectionString = Configuration.GetConnectionString("MyDb");
// Đăng ký provider dưới dạng Singleton
services.AddSingleton<IDistributedLockProvider>(
_ => new SqlDistributedSynchronizationProvider(connectionString));
services.AddScoped<InventoryService>();
}
Sử dụng trong Service:
public class InventoryService
{
private readonly IDistributedLockProvider _syncProvider;
public InventoryService(IDistributedLockProvider syncProvider)
{
_syncProvider = syncProvider;
}
public async Task ProcessStockAsync(string productId)
{
var lockObj = _syncProvider.CreateLock($"Stock_{productId}");
await using (await lockObj.AcquireAsync())
{
// Xử lý logic cập nhật tồn kho an toàn
Console.WriteLine($"Đang cập nhật tồn kho cho sản phẩm: {productId}");
await Task.Delay(500);
}
}
}
Gia hạn khóa và thuật toán RedLock
Đối với triển khai trên Redis, thư viện cung cấp cơ chế "giữ chân" (keep-alive) tương tự như watchdog. Nếu thời gian xử lý logic vượt quá thời gian hết hạn (TTL) của khóa, cơ chế này sẽ tự động gia hạn để khóa không bị giải phóng sớm, ngăn chặn tình trạng race condition.
Ngoài ra, để đảm bảo tính sẵn sàng cao trong các hệ thống quan trọng, thư viện hỗ trợ thuật toán RedLock. Thuật toán này yêu cầu lấy khóa thành công trên đa số các node Redis (ví dụ: ít nhất 3 trên 5 node) nhằm đảm bảo khóa vẫn hiệu quả ngay cả khi một phần cụm Redis gặp sự cố.
Các trường hợp áp dụng thực tế
- Ngăn chặn trùng lặp: Đảm bảo các tác vụ định thời (Cron Job, Hangfire) chỉ chạy trên một node duy nhất tại một thời điểm.
- Kiểm soát tính an toàn (Idempotency): Ngăn chặn việc gửi đơn hàng trùng lặp hoặc xử lý giao dịch nhiều lần.
- Bảo vệ Cache: Tránh việc hàng loạt request truy cập trực tiếp vào cơ sở dữ liệu khi cache hết hạn (Cache Stampede).
- Truy cập tài nguyên đòi hỏi loại trừ tương hỗ: Điều khiển việc ghi file hoặc xử lý message queue để tránh xung đột dữ liệu.