Khi xây dựng hệ thống phần mềm hiện đại, đặc biệt là các ứng dụng có kiến trúc phân tán, việc đảm bảo tính toàn vẹn dữ liệu trong môi trường đa tiến trình và đa nút là một thách thức không thể bỏ qua. Khóa phân tán chính là cơ chế then chốt giúp giải quyết bài toán đồng bộ hóa truy cập tài nguyên chung giữa các dịch vụ chạy trên các máy chủ khác nhau.
1. Khóa phân tán là gì?
Khóa phân tán là một cơ chế đồng bộ cho phép nhiều tiến trình — có thể nằm trên các máy vật lý hoặc container riêng biệt — phối hợp để độc chiếm quyền truy cập vào một tài nguyên chung (ví dụ: bản ghi cơ sở dữ liệu, cấu hình hệ thống, trạng thái thanh toán). Khác với khóa cục bộ (như synchronized trong Java hay threading.Lock trong Python), khóa phân tán phải tồn tại bên ngoài phạm vi của một tiến trình duy nhất và chịu được lỗi mạng, gián đoạn hoặc khởi động lại.
2. Ba cách triển khai tiêu biểu
a) Dựa trên cơ sở dữ liệu quan hệ
Có hai chiến lược chính: khóa bi quan (pessimistic locking) và khóa lạc quan (optimistic locking).
- Khóa lạc quan với cột phiên bản: Mỗi bảng chứa một cột
revision(hoặcversion). Khi cập nhật, câu lệnhUPDATEkiểm tra giá trị hiện tại củarevisiontrước khi ghi đè:
Nếu dòng bị ảnh hưởng bằng 0, nghĩa là đã có tiến trình khác cập nhật thành công trước đó — yêu cầu phải tải lại dữ liệu và thử lại.UPDATE accounts SET balance = balance - 2000, revision = revision + 1 WHERE id = 123 AND revision = 1; - Khóa bi quan với
SELECT ... FOR UPDATE: Thực thi truy vấn khóa hàng ngay từ đầu giao dịch:
Phương pháp này hiệu quả với thời gian xử lý ngắn, nhưng dễ gây nghẽn nếu giao dịch kéo dài.BEGIN TRANSACTION; SELECT balance FROM accounts WHERE id = 123 FOR UPDATE; -- Xử lý logic nghiệp vụ UPDATE accounts SET balance = balance - 2000 WHERE id = 123; COMMIT;
b) Dựa trên Redis với nguyên tắc atomicity
Redis hỗ trợ các thao tác nguyên tử như SETNX (set if not exists) và khả năng đặt thời gian sống (expiration) cho khóa. Một triển khai an toàn cần đáp ứng ba điều kiện: độc nhất, tự động hết hạn, và giải phóng an toàn.
Dưới đây là ví dụ minh họa sử dụng Lua script để đảm bảo tính nguyên tử khi xóa khóa:
-- Đặt khóa với TTL = 30s và giá trị ngẫu nhiên (để tránh xóa nhầm khóa của tiến trình khác)
SET lock:order:789 "a1b2c3" NX PX 30000
-- Script Lua để giải phóng khóa chỉ khi giá trị khớp
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Giá trị ngẫu nhiên (ví dụ "a1b2c3") đóng vai trò như "token sở hữu", ngăn chặn tình huống tiến trình A hết thời gian chờ nhưng vẫn vô tình xóa khóa do tiến trình B tạo ra.
c) Dựa trên ZooKeeper với mô hình node tạm thời tuần tự
ZooKeeper cung cấp hệ thống phân cấp node theo dạng cây, trong đó các node có thể được tạo dưới dạng ephemeral (tạm thời) và sequential (có thứ tự tăng dần). Quy trình giành khóa gồm các bước sau:
- Tạo node con mang tiền tố
/locks/order-req-dưới một node cha cố định (ví dụ/locks). ZooKeeper tự động gắn số thứ tự vào tên node (ví dụ:/locks/order-req-0000000001). - Lấy danh sách tất cả node con và sắp xếp theo tên. Nếu node vừa tạo là nhỏ nhất → đã giành được khóa.
- Nếu không phải nhỏ nhất, đăng ký watcher lên node liền trước nó. Khi node đó bị xóa (do tiến trình sở hữu nó chết hoặc chủ động giải phóng), watcher sẽ đánh thức tiến trình hiện tại để kiểm tra lại.
Cơ chế này đảm bảo tính công bằng (fairness) và tự phục hồi khi tiến trình crash — vì node tạm thời sẽ biến mất khi session kết thúc.