Redis Phân tán: Khóa phân tán và Kiến trúc cụm

Trong phần trước, chúng ta đã khái quát các khía cạnh nền tảng của Redis — từ bối cảnh ứng dụng, cơ chế I/O đa路 (multiplexing), cấu trúc dữ liệu cốt lõi đến các chiến lược lưu trữ bền vững. Phần này sẽ đi sâu vào hai chủ đề thực tiễn quan trọng trong kiến trúc hệ thống hiện đại: khóa phân táncụm Redis (Redis Cluster), thông qua một tình huống nghiệp vụ minh họa — hệ thống phát hành & nhận phong bao lì xì trực tuyến.

1. Vấn đề khóa phân tán trong môi trường đa dịch vụ

Khi một ứng dụng mở rộng ra nhiều bản sao (instance) chạy song song — ví dụ như 3 máy chủ web cân bằng tải qua Nginx — thì việc đồng bộ truy cập tài nguyên chung (như số lượng phong bao còn lại) trở nên cực kỳ nhạy cảm. Trong môi trường đơn instance, ta có thể dùng lock cấp thread; nhưng ở mức phân tán, cần một cơ chế đồng bộ xuyên suốt toàn bộ cụm — đó chính là khóa phân tán. Giả sử hệ thống quản lý 100 phong bao lì xì, lưu trong cơ sở dữ liệu. Mỗi lần người dùng nhận, hệ thống phải:
  1. Kiểm tra số dư còn > 0
  2. Nếu đủ, giảm giá trị đi 1 và trả kết quả thành công
Nếu không có cơ chế bảo vệ, hàng chục yêu cầu đồng thời có thể cùng đọc giá trị 100, rồi cùng ghi đè thành 99 → dẫn đến "nhận ảo" (over-allocation) hoặc "mất giao dịch" (lost update).

2. Triển khai khóa phân tán bằng Redis

Redis — với đặc tính đơn luồng, thao tác trên bộ nhớ và hỗ trợ atomic operations — là lựa chọn tối ưu để xây dựng khóa phân tán. Dưới đây là cách tiếp cận từng bước:

a) Phiên bản cơ bản (có rủi ro)

// Đặt khóa với TTL = 60s để tránh khóa treo
bool acquired = db.StringSet("lock:envelope", "active", TimeSpan.FromSeconds(60), When.NotExists);

if (!acquired) {
    return BadRequest("Hệ thống đang bận, vui lòng thử lại");
}

try {
    var remaining = GetEnvelopeCount(); // Truy vấn DB hoặc cache
    if (remaining <= 0) return Ok("Phong bao đã hết!");

    ClaimEnvelope(); // Cập nhật DB/cache
} finally {
    db.KeyDelete("lock:envelope"); // Giải phóng khóa — NGUY HIỂM nếu exception xảy ra trước dòng này!
}
→ Nhược điểm rõ ràng: Nếu xử lý nghiệp vụ mất hơn 60s, khóa tự hết hạn trong khi logic chưa hoàn tất → nguy cơ xung đột.

b) Cải tiến an toàn: Xác thực chủ sở hữu khóa

string lockToken = Guid.NewGuid().ToString("N");

// Đặt khóa kèm token duy nhất cho request hiện tại
bool success = db.StringSet("lock:envelope", lockToken, TimeSpan.FromSeconds(60), When.NotExists);
if (!success) return StatusCode(423, "Đang xử lý, vui lòng chờ");

try {
    var count = GetEnvelopeCount();
    if (count <= 0) return Ok("Phong bao đã hết!");
    
    ClaimEnvelope();
}
finally {
    // Chỉ giải phóng nếu token khớp — đảm bảo không xóa khóa của request khác
    if (db.StringGet("lock:envelope") == lockToken) {
        db.KeyDelete("lock:envelope");
    }
}

c) Sử dụng StackExchange.Redis với LockTake/LockRelease

public class RedisDistributedLock : IDisposable
{
    private readonly IDatabase _db;
    private readonly string _lockKey;
    private readonly string _lockValue;

    public RedisDistributedLock(IDatabase db, string key)
    {
        _db = db;
        _lockKey = $"lock:{key}";
        _lockValue = Guid.NewGuid().ToString("N");
    }

    public bool Acquire(int timeoutMs = 5000)
    {
        var startTime = DateTimeOffset.Now;
        while ((DateTimeOffset.Now - startTime).TotalMilliseconds < timeoutMs)
        {
            if (_db.LockTake(_lockKey, _lockValue, TimeSpan.FromSeconds(30)))
                return true;
            Thread.Sleep(50); // Giảm tải polling
        }
        return false;
    }

    public void Release() => _db.LockRelease(_lockKey, _lockValue);

    public void Dispose() => Release();
}

// Cách dùng:
using (var locker = new RedisDistributedLock(db, "envelope"))
{
    if (locker.Acquire())
    {
        // Thực thi logic nghiệp vụ an toàn
    }
}

3. Giới hạn và lưu ý khi dùng khóa phân tán trên Redis

  • Vấn đề mất khóa trong mô hình Master-Slave: Nếu khóa được đặt trên master, sau đó master sập trước khi replicate sang slave, và sentinel nâng slave lên làm master mới → khóa biến mất → vi phạm tính toàn vẹn.
  • So sánh với ZooKeeper: ZooKeeper ưu tiên tính nhất quán (C) theo CAP, phù hợp cho khóa yêu cầu độ tin cậy cao; Redis thiên về tính sẵn sàng (A), tốc độ nhanh hơn nhưng chấp nhận khả năng mất khóa tạm thời.

4. Các mô hình Redis đảm bảo tính sẵn sàng cao

Redis cung cấp ba phương án tăng cường độ tin cậy và khả năng mở rộng:

a) Mô hình Master-Slave (Nhân bản)

  • Master xử lý write, slave replica dữ liệu và phục vụ read.
  • Hạn chế: Không tự động failover, dễ mất dữ liệu nếu master sập trước khi sync, khó mở rộng dung lượng.

b) Mô hình Sentinel (Giám sát)

  • Thêm layer giám sát độc lập: Phát hiện sự cố master, tự động bầu chọn slave mới làm master, cập nhật cấu hình cho client.
  • Vẫn phụ thuộc vào nhân bản → vẫn tồn tại độ trễ sync và giới hạn về dung lượng.

c) Mô hình Cluster (Cụm phân mảnh)

Đây là giải pháp mạnh nhất cho hệ thống quy mô lớn:
  • Phân vùng dữ liệu: 16384 slot, mỗi key được băm (CRC16) rồi lấy mod 16384 để xác định slot → slot được phân bổ giữa các node.
  • Tính phi tập trung: Không có node điều khiển trung tâm; các node giao tiếp qua giao thức Gossip để đồng bộ trạng thái.
  • Mỗi node có thể là master hoặc slave: Slave sao chép dữ liệu từ master tương ứng và sẵn sàng thay thế khi cần.

d) Triển khai cụm Redis trên Windows (tóm tắt)

  1. Cài Ruby và gem redis-trib.rb
  2. Chuẩn bị 6 instance Redis (3 master + 3 slave), cấu hình bật cluster-enabled yes, chỉ định cluster-config-filecluster-node-timeout.
  3. Khởi tạo cụm:
    ruby redis-trib.rb create --replicas 1 127.0.0.1:6380 127.0.0.1:6381 ...
  4. Kiểm tra:
    ruby redis-trib.rb check 127.0.0.1:6380

Thẻ: Redis distributed-lock redis-cluster sentinel master-slave

Đăng vào ngày 9 tháng 6 lúc 22:44