Triển khai DistributedLock để quản lý khóa phân tán trong ứng dụng .NET

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.

Thẻ: .NET DistributedLock Redis SQL Server Concurrency

Đăng vào ngày 17 tháng 05 lúc 00:12