Distributed Locking Trong .NET: Điều Phối Công Việc Trên Nhiều Instance
Trong hệ thống phân tán, nhiều instance có thể cùng truy cập một tài nguyên, dễ gây lỗi race condition hoặc trùng dữ liệu. Distributed Locking (Khóa Phân Tán) giúp đảm bảo chỉ một tiến trình được thao tác tại một thời điểm. Bài viết giới thiệu lý do cần cơ chế này và cách triển khai trong .NET bằng PostgreSQL advisory locks và thư viện DistributedLock.
Vấn Đề
Trong các hệ thống phân tán nơi ứng dụng chạy trên nhiều máy chủ hoặc quy trình, việc truy cập đồng thời vào tài nguyên chia sẻ có thể dẫn đến các điều kiện đua (race conditions), dữ liệu bị hỏng hoặc công việc trùng lặp. Mặc dù .NET cung cấp các nguyên thủy kiểm soát đồng thời như lock, SemaphoreSlim và Mutex cho các kịch bản quy trình đơn, nhưng chúng không đủ cho môi trường phân tán nơi mỗi instance hoạt động trong không gian bộ nhớ riêng.
Khóa phân tán giải quyết thách thức này bằng cách đảm bảo chỉ một nút (instance ứng dụng) có thể truy cập vào một phần quan trọng tại một thời điểm, từ đó duy trì tính nhất quán dữ liệu và ngăn ngừa xung đột trên toàn hệ thống.
Khi nào cần sử dụng Distributed Locking
- Xử Lý Công Việc Nền (Background Jobs): Đảm bảo chỉ một worker xử lý một công việc hoặc tài nguyên cụ thể tại một thời điểm.
- Bầu Chọn Lãnh Đạo: Chọn một quy trình duy nhất để thực hiện công việc định kỳ.
- Ngăn Chặn Thực Thi Trùng Lặp: Tránh nhiều instance chạy cùng một tác vụ đã lên lịch đồng thời.
- Điều Phối Tài Nguyên Chung: Cho phép chỉ một instance dịch vụ thực hiện di chuyển (migration) hoặc dọn dẹp (cleanup) tại một thời điểm.
- Ngăn Chặn Đám Đông Bộ Nhớ Đệm (cache stampede): Đảm bảo chỉ một instance làm mới mục bộ nhớ đệm khi hết hạn.
Nếu không có distributed lock, bạn sẽ đối mặt với dữ liệu không nhất quán, tác vụ bị nhân đôi và tải hệ thống tăng cao.
Các Phương Pháp Triển Khai Khóa Phân Tán
Triển khai Distributed Locking với PostgreSQL Advisory Locks
PostgreSQL cung cấp tính năng Advisory Locks, cho phép đồng bộ hoá mà không ảnh hưởng trực tiếp đến dữ liệu. Các khóa này sử dụng khóa số để xác định.
Ví dụ triển khai:
public class NightlyReportService(NpgsqlDataSource dataSource)
{
public async Task ProcessNightlyReport()
{
await using var connection = dataSource.OpenConnection();
var key = HashKey("nightly-report");
var acquired = await connection.ExecuteScalarAsync<bool>(
"SELECT pg_try_advisory_lock(@key)",
new { key });
if (!acquired)
{
throw new ConflictException("Một instance khác đang xử lý báo cáo hàng đêm");
}
try
{
await DoWork();
}
finally
{
await connection.ExecuteAsync(
"SELECT pg_advisory_unlock(@key)",
new { key });
}
}
private static long HashKey(string key) =>
BitConverter.ToInt64(SHA256.HashData(Encoding.UTF8.GetBytes(key)), 0);
private static Task DoWork() => Task.Delay(5000); // Thay thế cho công việc thực tế
}
Cách hoạt động:
- Khóa được băm thành số nguyên 64-bit để tương thích với PostgreSQL.
pg_try_advisory_lock()sẽ thử lấy khóa và trả về ngay lập tức.- Nếu khóa đã được giữ, tiến trình hiện tại sẽ dừng.
finallyluôn đảm bảo giải phóng khóa.
SQL Server cung cấp cơ chế tương tự qua sp_getapplock.
Sử Dụng Thư Viện DistributedLock
Thư viện DistributedLock là giải pháp sản xuất sẵn, hỗ trợ nhiều backend như Postgres, Redis và SQL Server.
Cài đặt:
Install-Package DistributedLock
Ví dụ cấu hình:
Đối với PostgreSQL:
builder.Services.AddSingleton<IDistributedLockProvider>(
(_) =>
{
return new PostgresDistributedSynchronizationProvider(
builder.Configuration.GetConnectionString("distributed-locking")!);
});
Đối với Redis (sử dụng thuật toán Redlock):
builder.Services.AddSingleton<IConnectionMultiplexer>(
(_) =>
{
return ConnectionMultiplexer.Connect(
builder.Configuration.GetConnectionString("redis")!);
});
builder.Services.AddSingleton<IDistributedLockProvider>(
(sp) =>
{
var connectionMultiplexer = sp.GetRequiredService<IConnectionMultiplexer>();
return new RedisDistributedSynchronizationProvider(connectionMultiplexer.GetDatabase());
});
Sử dụng:
IDistributedSynchronizationHandle? distributedLock = distributedLockProvider
.TryAcquireLock("nightly-report");
if (distributedLock is null)
{
return Results.Conflict();
}
using (distributedLock)
{
await DoWork();
}
Kết luận
Distributed Locking là một kỹ thuật quan trọng trong việc đảm bảo tính toàn vẹn dữ liệu và điều phối truy cập tài nguyên trong môi trường phân tán.
All rights reserved