Khóa phân tán (Distributed Lock)
1. Tổng quan
Định nghĩa: Khóa phân tán (Distributed Lock) là kỹ thuật điều khiển nhằm điều phối loại trừ tương hỗ và quyền sở hữu giữa các bên tham gia được kết nối qua mạng, để nhiều máy chủ, tiến trình hay container không đồng thời thay đổi tài nguyên dùng chung hoặc vùng găng (critical section).
Trong một tiến trình đơn, mutex hay semaphore bảo vệ vùng găng bằng các lệnh nguyên tử trên bộ nhớ dùng chung. Tuy nhiên, khi dịch vụ được mở rộng theo chiều ngang, sẽ có nhiều instance xử lý yêu cầu, và khóa đặt trong bộ nhớ của mỗi instance thì các instance khác không nhìn thấy. Khi đó, những công việc phải "chỉ một chủ thể thực hiện tại một thời điểm" như trừ tồn kho, chống chạy trùng batch, tạo cùng một tệp hay bầu leader cho scheduler đều cần sự điều phối qua mạng.
Khóa phân tán không chỉ đơn giản là chức năng lưu một khóa (key). Chủ thể đã giành được khóa có thể không kết thúc bình thường, mạng có thể bị phân tách, và phản hồi có thể chậm đến mức chủ thể không biết rằng khóa của mình đã hết hạn. Vì vậy, chỉ định nghĩa loại trừ tương hỗ (mutual exclusion) là chưa đủ, mà phải thiết kế đồng thời tính sống (liveness) — chủ thể đang chờ rồi cũng sẽ tiến hành được, khả năng tự phục hồi sau sự cố, và tính an toàn (safety) — dữ liệu không bị làm hỏng ngay cả khi một chủ sở hữu sai hoàn tất công việc muộn.
Ví dụ, khi dịch vụ đặt hàng chỉ còn 1 sản phẩm tồn kho mà hai instance cùng trừ, mỗi instance có thể đọc cùng một số tồn còn lại và đều xử lý thành công. Khóa phân tán tuần tự hóa sự cạnh tranh này, nhưng dùng khóa không có nghĩa là không còn cần cập nhật có điều kiện, giao dịch hay ràng buộc của cơ sở dữ liệu. Khóa là cơ chế điều phối để giảm tranh chấp, còn tính nhất quán cuối cùng phải được bảo đảm bởi phép kiểm tra nguyên tử trên dữ liệu gốc (sổ cái).
Trong bài làm của Kỹ sư chuyên nghiệp (Professional Engineer), điều quan trọng là trước khi nói "chọn kho lưu trữ nào làm dịch vụ khóa", phải nêu rõ đối tượng bảo vệ, phạm vi khóa, vòng đời quyền sở hữu, mô hình sự cố và yêu cầu nhất quán. Nếu công việc ngắn và được xử lý nguyên tử tại một cơ sở dữ liệu duy nhất, thì UPDATE có điều kiện đơn giản và mạnh hơn. Ngược lại, nếu phải điều phối nhiều tài nguyên hoặc hệ thống bên ngoài, cần thiết kế khóa phân tán bao gồm thuê (lease), fencing token, thử lại và khả năng quan sát.
- Loại trừ tương hỗ: Với cùng một tên khóa, tại một thời điểm chỉ được có một chủ sở hữu hợp lệ.
- Nhận diện quyền sở hữu: Nhận diện chủ thể giành khóa bằng một token duy nhất ngẫu nhiên để không giải phóng khóa của chủ thể khác.
- Tự phục hồi: Ngay cả khi chủ sở hữu gặp sự cố, tránh bế tắc vĩnh viễn nhờ TTL, phiên (session) hay hết hạn thuê.
- Liên kết với tính nhất quán nghiệp vụ: Không tuyên bố hoàn tất chỉ vì giành được khóa, mà chặn công việc đến muộn bằng cập nhật có điều kiện, fencing và giao dịch.
2. Cấu trúc tổng thể và các thành phần
Điểm xuất phát là chia hệ thống khóa phân tán thành client, kho điều phối khóa và tài nguyên được bảo vệ. Client giành khóa, thực thi vùng găng rồi giải phóng, nhưng thông tin sở hữu thực tế và hạn của khóa được đặt tại một kho nhất quán mà mọi client đều quan sát được. Kho này có thể được hiện thực bằng cơ sở dữ liệu quan hệ, bộ điều phối nhất quán mạnh, kho key-value trong bộ nhớ, v.v.
graph LR
C1["Client A"] -->|"Acquire(lock-name, token)"| S["Kho điều phối khóa\ntạo·cập nhật·hết hạn nguyên tử"]
C2["Client B"] -->|"Acquire(lock-name, token)"| S
S -->|"quyền sở hữu·lease·fencing token"| C1
C1 -->|"cập nhật có điều kiện + token"| R["Tài nguyên được bảo vệ"]
C2 -.->|"chờ·thử lại·thất bại"| R
S --> M["Metric·log kiểm toán"]
Tên khóa (lock key) nhận diện tài nguyên logic cần bảo vệ. Hãy thiết kế sao cho đơn vị nghiệp vụ hiện rõ như inventory:item:1234, job:daily-settlement:2026-09-16, file:report.csv, nhưng khóa toàn cục quá rộng sẽ gây tuần tự hóa không cần thiết. Ngược lại, nếu khóa bị chia quá nhỏ, nhiều bước của cùng một nghiệp vụ có thể dùng các khóa khác nhau và bỏ lọt phạm vi bảo vệ.
Token chủ sở hữu (owner token) được cấp bằng số ngẫu nhiên hoặc UUID khi giành khóa. Yêu cầu giải phóng gửi kèm token này và chỉ xóa khi khớp với token đã lưu. Nếu chỉ chạy DEL lock-key, sẽ xảy ra sự cố: trong lúc công việc của chủ sở hữu cũ bị chậm, chủ sở hữu mới giành được khóa, rồi chủ sở hữu cũ xóa luôn cả khóa của chủ sở hữu mới. Kiểm tra chủ sở hữu là cơ chế an toàn cơ bản của thao tác giải phóng.
Thuê (lease) và TTL biểu thị thời hạn hiệu lực của khóa. Ngay cả khi tiến trình chết hoặc mạng bị ngắt khiến không gửi được yêu cầu giải phóng, chủ thể khác vẫn có thể tiến hành sau khi hết hạn. Tuy nhiên, TTL không phải là bảo đảm công việc sẽ kết thúc mà là "giới hạn trên của thời gian được phép khẳng định quyền sở hữu". Nếu công việc kéo dài hơn TTL thì cần gia hạn (renewal), và nếu gia hạn thất bại thì phải dừng ngay vùng găng hoặc dùng fencing để từ chối việc ghi kết quả.
Fencing token là số tăng đơn điệu mỗi lần giành khóa. Tài nguyên được bảo vệ chỉ ghi nhận khi token kèm trong yêu cầu lớn hơn token đã xử lý gần nhất. Cơ chế này giải quyết vấn đề "client zombie" — chủ sở hữu cũ đến muộn do trễ mạng. Với khóa chỉ có TTL, client tại thời điểm hết hạn vẫn có thể tiếp tục chạy, nên nếu kho bên ngoài không kiểm tra token thì trực giác về loại trừ tương hỗ không chuyển thành an toàn dữ liệu thực sự.
3. Giao thức giành·duy trì·giải phóng
A. Giành khóa
Quá trình giành khóa bắt đầu từ việc định nghĩa mô hình xung đột. Client tạo một token chủ sở hữu duy nhất và yêu cầu kho điều phối thực hiện phép nguyên tử "chỉ tạo khi khóa chưa tồn tại". Nếu thành công, client nhận được thời điểm hết hạn thuê và fencing token nếu cần. Nếu thất bại, không lặp vô hạn mà áp dụng backoff và thời gian chờ tối đa.
sequenceDiagram
participant A as Client A
participant L as Kho khóa
participant D as Tài nguyên được bảo vệ
A->>L: SET lock key, owner token, NX, TTL
alt Khóa chưa tồn tại
L-->>A: Thành công + fencing token n
A->>D: Thao tác có điều kiện kèm token n
D-->>A: Kết quả ghi nhận
A->>L: Giải phóng khi owner token khớp
L-->>A: Đã giải phóng
else Đã có người nắm giữ
L-->>A: Thất bại
A->>A: Thử lại hoặc bỏ cuộc sau backoff có jitter
end
Tính nguyên tử có nghĩa là gộp "kiểm tra rồi lưu" thành một thao tác logic duy nhất. Nếu gọi EXISTS trước rồi mới SET, sẽ sinh điều kiện tranh đua (race condition) khi hai client đồng thời thấy khóa chưa có và cùng lưu. Ràng buộc UNIQUE cùng INSERT của cơ sở dữ liệu, compare-and-set (CAS) của bộ điều phối, SET NX của kho key-value cung cấp tính nguyên tử này.
Sau khi giành thành công, cần ghi lại kết quả trả về. Nếu ghi log token chủ sở hữu, thời điểm giành, thời điểm hết hạn, fencing token và ID yêu cầu/công việc, có thể truy vết tranh chấp khóa và nguyên nhân sự cố. Nếu chỉ ghi giá trị Boolean thành công hay không, sẽ khó phân tích "ai đã giữ lâu" và "có yêu cầu muộn sau khi hết hạn không".
Việc thử lại nên kết hợp backoff lũy thừa với jitter ngẫu nhiên thay vì khoảng cách cố định thì an toàn hơn. Nếu mọi bên chờ cùng gửi lại một lúc, kho sẽ gặp thundering herd, và khi khóa được nhả lại xảy ra một đợt bùng nổ tranh chấp khác. Hãy xác định giới hạn trên thời gian chờ và việc nghiệp vụ có chấp nhận trùng lặp hay không, đồng thời chuẩn bị phương án thay thế như đưa vào hàng đợi hoặc trả về yêu cầu xử lý lại cho người dùng khi không giành được khóa.
B. Duy trì và gia hạn khóa
Gia hạn thuê không phải là tăng TTL vô điều kiện. Phải kiểm tra nguyên tử rằng client vẫn còn sống và vẫn nắm giữ token đó, và chỉ gia hạn khi thời gian thuê còn lại ngắn hơn ngưỡng. Nếu phản hồi gia hạn bị trễ hoặc hết thời gian chờ, không nên coi là thành công; chính sách thận trọng là dừng mọi thao tác ghi thêm lên tài nguyên được bảo vệ.
Nếu thời gian dự kiến của công việc ngắn và ít biến động, có thể đặt TTL dài hơn đáng kể so với thời gian công việc. Nhưng TTL quá dài làm chậm phục hồi sau sự cố, còn TTL quá ngắn làm tăng khả năng biến công việc bình thường thành công việc zombie. Vì vậy, hãy tính ngân sách thuê không dựa trên thời gian trung bình mà bao gồm độ trễ p99·p999, GC pause, độ trễ lập lịch container và thời gian khứ hồi mạng.
Nếu luồng gia hạn tách biệt khỏi luồng công việc, có thể phát sinh lỗi vẫn gia hạn sau khi công việc đã dừng. Khi nhận tín hiệu hủy công việc, hết thời gian chờ hay kết thúc tiến trình, hãy dừng gia hạn trước, chặn I/O mới, rồi mới thực hiện thủ tục dọn dẹp. Ngay cả sau khi phát hiện gia hạn thất bại, vẫn có thể còn các tác vụ bất đồng bộ đã phát đi, nên cần đặt fencing hoặc khóa lũy đẳng (idempotency key) ở bước ghi kết quả vào hàng đợi, DB hay API bên ngoài.
C. Giải phóng khóa
Giải phóng bình thường thực hiện kiểm tra chủ sở hữu và xóa trong một thao tác nguyên tử duy nhất. Với Redis, có thể dùng Lua script so sánh token rồi xóa; với DB quan hệ, dùng DELETE với điều kiện WHERE lock_name = ? AND owner_token = ?. Nếu so sánh và xóa bị tách rời, việc hết hạn TTL và giành khóa mới có thể chen vào ngay sau khi kiểm tra.
Hết thời gian chờ của yêu cầu giải phóng khác với việc giải phóng có thực sự thành công hay không. Dù client nhận timeout và thử lại, nếu kiểm tra token là lũy đẳng thì có thể xác nhận lại an toàn thất bại hay thành công. Ngược lại, nếu vì không có phản hồi giải phóng mà cưỡng chế xóa khóa mới, có thể làm hỏng công việc của chủ sở hữu hiện tại. Phải chờ hết hạn hoặc kiểm soát riêng thủ tục giải phóng cưỡng chế dành cho quản trị viên.
Thứ tự giữa commit nghiệp vụ và giải phóng khóa cũng quan trọng. Nếu nhả khóa trước khi giao dịch cơ sở dữ liệu commit, chủ thể khác có thể đọc cùng tài nguyên và bắt đầu công việc trùng lặp. Thông thường, hoàn tất commit dữ liệu được bảo vệ và ghi lại trạng thái của các tác dụng phụ bên ngoài rồi mới giải phóng khóa, nhưng nếu có gọi API bên ngoài thì phải thiết kế thêm cả bù trừ (compensation), outbox và xử lý lũy đẳng.
4. Phương thức hiện thực và nguyên lý hoạt động
A. Khóa dựa trên cơ sở dữ liệu quan hệ
Cơ sở dữ liệu quan hệ vốn đã cung cấp giao dịch, log, phục hồi sự cố và ràng buộc UNIQUE, nên có thể xem xét đầu tiên khi vòng đời của dữ liệu nghiệp vụ và khóa trùng nhau. Nếu đặt lock_name làm khóa chính của bảng khóa và chỉ giao dịch nào INSERT thành công mới có quyền sở hữu, thì cơ sở dữ liệu sẽ phân xử loại trừ tương hỗ giữa các bên cạnh tranh.
CREATE TABLE distributed_lock (
lock_name VARCHAR(200) PRIMARY KEY,
owner_token VARCHAR(100) NOT NULL,
fencing_seq BIGINT NOT NULL,
lease_until TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
Đừng chỉ tin vào việc INSERT hay UPDATE có điều kiện thành công hay không, mà phải xét tới chênh lệch giữa đồng hồ cơ sở dữ liệu và đồng hồ ứng dụng. Nếu lưu thời điểm hết hạn do ứng dụng tính, một máy chủ lệch đồng hồ nhiều có thể coi khóa còn hiệu lực là đã hết hạn. Khi có thể, hãy dùng giá trị tăng đơn điệu, thời gian giao dịch và biểu thức điều kiện phía máy chủ của cơ sở dữ liệu, đồng thời nêu rõ sai số cho phép của các phán đoán dựa trên thời gian.
Khóa hàng (row lock) và advisory lock có tính chất khác nhau. Khóa hàng tự động được nhả khi giao dịch kết thúc và gắn trực tiếp với hàng bên trong DB, nhưng nếu đặt lời gọi bên ngoài kéo dài trong giao dịch thì kết nối và khóa bị chiếm dụng lâu. Advisory lock là khóa hợp tác trên khóa do ứng dụng quy định nên tiện lợi, nhưng mọi bên tham gia phải tuân thủ cùng quy tắc và cần xác nhận ý nghĩa của việc ngắt phiên DB hay tái sử dụng connection pool.
Ưu điểm của khóa dựa trên cơ sở dữ liệu là có thể gộp dữ liệu được bảo vệ và việc giành khóa vào cùng một giao dịch. Với công việc hoàn tất bằng một UPDATE có điều kiện trên một hàng như trừ tồn kho, thì UPDATE inventory SET quantity = quantity - 1 WHERE id = ? AND quantity > 0 là phương tiện bảo đảm nhất quán trực tiếp hơn một khóa phân tán riêng. Ngược lại, nếu tranh chấp giành khóa rất lớn, DB nghiệp vụ phải kiêm cả vai trò máy chủ khóa và có thể trở thành điểm nghẽn.
B. Khóa dựa trên kho key-value trong bộ nhớ
Các kho thuộc họ Redis cung cấp lệnh nguyên tử độ trễ thấp và TTL nên thường được dùng làm khóa cho vùng găng ngắn. Mẫu cơ bản là chạy SET resource token NX PX ttl kèm token ngẫu nhiên. NX chỉ lưu khi khóa chưa tồn tại, còn PX gắn thời gian tự hết hạn, giúp giảm tranh đua giữa kiểm tra·lưu riêng rẽ và chiếm dụng vô hạn.
Khi giải phóng nhất định phải so sánh token chủ sở hữu. Mã giả như sau.
if GET(resource) == my_token:
DEL(resource)
Tuy nhiên, nếu gửi hai dòng trên thành hai lệnh mạng riêng thì tranh đua giữa so sánh và xóa lại tái diễn. Vì vậy, dùng script nội bộ của kho hoặc CAS để biến phần sau thành một thao tác nguyên tử.
if value(resource) == my_token then
delete(resource)
return 1
else
return 0
end
Khóa dựa trên một instance Redis đơn lẻ hiện thực đơn giản, nhưng phải đưa sự cố instance, trễ sao chép và mất dữ liệu tại thời điểm failover vào mô hình sự cố. Với nghiệp vụ mà khóa thất bại dẫn tới thanh toán trùng hay hỏng dữ liệu, đừng khẳng định bảo đảm mạnh chỉ bằng TTL của Redis, mà hãy kết hợp ràng buộc DB, fencing token và tính lũy đẳng nghiệp vụ. Sharding của Redis Cluster có thể không gộp nguyên tử được các khóa khác nhau, nên cần kiểm tra phân bố khóa và phạm vi lệnh.
Các thuật toán sao chép khóa lên nhiều node Redis độc lập để tăng tính sẵn sàng đòi hỏi những tiền đề mạnh như độ chính xác đồng hồ, độ trễ mạng, sự cố chồng chéo và điều kiện giành được đa số. Đặc biệt, vấn đề client có thể tiếp tục chạy mà không dừng sau khi hết hạn thuê không được giải quyết chỉ bằng việc tăng số node. Với tài nguyên rủi ro cao, không coi kết quả khóa mà client nhận được là thẩm quyền cuối cùng, mà thiết kế để phía tài nguyên kiểm tra fencing token.
C. Bộ điều phối dựa trên đồng thuận
Các bộ điều phối cung cấp giao thức đồng thuận cùng session và watch như ZooKeeper, etcd, Consul mang lại chức năng điều phối mạnh thông qua node tạm thời (ephemeral node), hết hạn phiên, node tuần tự và compare-and-swap. Khi client mất phiên, node tạm thời biến mất và các bên chờ nhận thông báo thay đổi để thử lại. Điều này giúp biểu đạt tường minh vòng đời khóa và phát hiện sự cố dễ hơn so với lưu key-value đơn thuần.
Tuy nhiên, sự kiện watch không phải là thông báo xác định "giờ đến lượt tôi". Sau khi nhận sự kiện, phải thực sự truy vấn lại khóa và xác nhận điều kiện của mình vẫn còn đúng. Nếu mọi client cùng theo dõi một node, có thể xảy ra bão sự kiện, nên dùng mẫu giảm hiệu ứng bầy đàn (herd effect): trong các node tuần tự, mỗi client chỉ theo dõi node đứng ngay trước mình.
Kho dựa trên đồng thuận cũng không phải máy chủ khóa nhanh vô hạn. Log đồng thuận và giao tiếp quorum mang lại tính nhất quán nhưng đòi hỏi khứ hồi mạng và độ phức tạp vận hành. Phải quản lý số thành viên, quorum, độ trễ đĩa, lưu giữ snapshot·log, gia hạn chứng chỉ và chuyển đổi khi leader gặp sự cố; không được tiếp cận theo kiểu chỉ cài thêm một cơ sở dữ liệu hay Redis nữa.
5. Tính an toàn·tính sống·tính công bằng
Chất lượng của khóa phân tán được đánh giá theo ba trục sau. Tính an toàn là tính chất không có từ hai chủ sở hữu hợp lệ trở lên đồng thời thực hiện công việc được bảo vệ; tính sống là tính chất sau khi sự cố qua đi, rồi sẽ có chủ sở hữu mới tiến hành được. Tính công bằng là tính chất một client cụ thể không liên tục bị giành mất cơ hội và thứ tự chờ có thể dự đoán.
Tính an toàn không hoàn thiện chỉ nhờ việc giành khóa nguyên tử của kho. Trong các tình huống như client vẫn chạy sau khi TTL hết hạn, phân vùng mạng, gửi lại phản hồi hay tiến trình tạm dừng, chủ thể trước đó phải không ghi được vào tài nguyên. Do đó, cần xem xét đối tượng bảo vệ có so sánh số fencing hay không, hoặc bản thân công việc có lũy đẳng và có điều kiện hay không.
Tính sống được bảo đảm bằng hết hạn thuê, ngắt phiên và hủy của client. Tuy nhiên, nếu đặt thời gian hết hạn quá ngắn, khóa có thể bị cấp lại ngay cả khi trễ tạm thời, khiến hai chủ thể chồng lấn. Ngược lại, quá dài thì phục hồi sự cố bị chậm. Hãy nêu rõ lựa chọn giữa tính an toàn và tính sống theo SLA và ngân sách sự cố.
Tính công bằng được hiện thực bằng hàng đợi FIFO hoặc node tuần tự khi cần. Cách thử lại không chặn và không bảo đảm công bằng cho thông lượng cao nhưng một client cụ thể có thể liên tục thất bại. Với tác vụ batch, việc chờ công bằng có thể quan trọng, nhưng với vùng găng ngắn của yêu cầu người dùng, thất bại ngay rồi chuyển sang hàng đợi cấp trên có thể phù hợp hơn hàng chờ.
| Trục đánh giá | Câu hỏi | Phương tiện thiết kế tiêu biểu | Ảnh hưởng khi thất bại |
|---|---|---|---|
| Tính an toàn | Hai chủ thể có cùng ghi nhận không? | Giành nguyên tử, token chủ sở hữu, fencing | Xử lý trùng·hỏng dữ liệu |
| Tính sống | Khóa có được nhả sau sự cố không? | TTL, phiên, hủy·phục hồi | Bế tắc vĩnh viễn·trễ xử lý |
| Tính công bằng | Có chủ thể nào bị bỏ đói không? | FIFO, hàng đợi tuần tự, điều chỉnh jitter | starvation·trễ khó lường |
| Khả năng quan sát | Có truy vết được nguyên nhân không? | Chỉ số holder·age·wait·renew | Không thể phân tích sự cố |
6. So sánh và tình huống áp dụng
A. So sánh khóa phân tán với các kỹ thuật nhất quán thay thế
Khóa phân tán là cách tuần tự hóa tạm thời nhiều yêu cầu. Ngược lại, điều khiển đồng thời lạc quan phát hiện xung đột bằng số phiên bản hoặc cập nhật có điều kiện và thử lại yêu cầu thất bại. Khi tranh chấp thấp và phục hồi xung đột dễ, cách lạc quan cho thông lượng tốt hơn chờ khóa. Khi chi phí xung đột lớn và tác dụng phụ bên ngoài khó hoàn tác, khóa hoặc hàng đợi có thể phù hợp hơn.
Phân vùng đơn consumer của hàng đợi thông điệp bảo đảm thứ tự cho một khóa cụ thể, giảm nhu cầu khóa phân tán. Tuy nhiên, với sự cố consumer, cân bằng lại (rebalancing) và công việc trải trên nhiều phân vùng, cần thiết kế riêng tính lũy đẳng và trạng thái tiến độ. Khóa UNIQUE của cơ sở dữ liệu chặn việc tạo trùng, nhưng không thay thế được khóa tổng quát điều phối thứ tự thực thi của nhiều tài nguyên hay hệ thống bên ngoài.
| Kỹ thuật | Xử lý xung đột | Điểm mạnh | Giới hạn·tình huống phù hợp |
|---|---|---|---|
| Khóa phân tán | Chiếm trước khi thực thi | Tuần tự hóa trực quan, điều phối công việc bên ngoài | Phải thiết kế TTL·zombie·sự cố kho |
| Cập nhật có điều kiện | Kiểm tra khi commit | Gắn trực tiếp với tính nhất quán DB | Yếu với thử lại khi xung đột·tác dụng phụ bên ngoài |
| Phân vùng hàng đợi thông điệp | Sắp thứ tự theo khóa | Xử lý bất đồng bộ·xử lý lại | Cần thiết kế phân vùng và xử lý tiêu thụ trùng |
| Leader/scheduler đơn | Leader thực hiện công việc | Đơn giản cho batch | Cần chuyển đổi leader·chia công việc |
| Saga·bù trừ | Xác nhận·hủy theo bước | Nghiệp vụ phân tán dài hạn | Phức tạp logic bù trừ và trạng thái trung gian |
Lý do tạo ra khác biệt nằm ở "ai kiểm soát xung đột". Khóa chặn đối thủ trước khi thực thi, cách lạc quan phát hiện xung đột phiên bản sau khi thực thi, còn hàng đợi hấp thụ cạnh tranh bằng luồng xử lý có thứ tự. Vì vậy, trong bài làm không nên dùng khóa như giải pháp vạn năng mà cần trình bày rằng lựa chọn dựa trên đặc tính của tài nguyên được bảo vệ và tác dụng phụ.
B. Tình huống 1: Batch trùng lặp và bầu leader
Trong môi trường nhiều instance ứng dụng chạy batch quyết toán hằng ngày, scheduler có thể thức dậy ở mọi instance. Nếu chỉ instance nào giành được khóa job:settlement:date mới bắt đầu công việc, có thể giảm chạy trùng. Tuy nhiên, với công việc dài, cần dừng khi gia hạn thất bại và ghi dấu hoàn tất bằng khóa UNIQUE theo ngày để việc khởi động lại cũng lũy đẳng.
Bầu leader và khóa phân tán tương tự nhưng không đồng nhất. Leader mang trách nhiệm dài hạn và phải liên tục xử lý thay đổi thành viên cũng như chuyển đổi khi leader gặp sự cố. Batch chạy một lần có thể phù hợp với khóa thuê, nhưng khi leader chịu trách nhiệm cho mọi thay đổi trạng thái như control plane của cluster thì cần bầu chọn dựa trên đồng thuận và fencing.
C. Tình huống 2: Đặt giữ tồn kho và liên kết thanh toán
Khi nhiều đơn hàng đồng thời đặt giữ 1 sản phẩm tồn kho, khóa theo sản phẩm có thể thu hẹp vùng cạnh tranh. Nhưng nếu vừa giữ khóa vừa gọi phê duyệt thanh toán, API giao hàng và dịch vụ thông báo, độ trễ bên ngoài sẽ kéo dài thời gian chiếm khóa và khi có sự cố, thông lượng tổng thể giảm mạnh. Trong thực tế, trừ tồn kho có điều kiện trong giao dịch DB, còn thanh toán·thông báo tách ra bằng outbox và máy trạng thái thì vững chắc hơn.
Dù dùng khóa vẫn phải giữ điều kiện quantity > 0 và ràng buộc UNIQUE của mã đơn hàng. Cần bao gồm chuyển trạng thái đơn hàng và kiểm tra phiên bản để instance đến muộn sau khi khóa hết hạn không trừ tồn kho lần nữa. Tình huống này cho thấy khóa không thay thế ràng buộc nhất quán của cơ sở dữ liệu mà là tầng bổ trợ giúp thu hẹp mức độ tranh chấp.
D. Tình huống 3: Tạo tệp và lưu trữ dùng chung
Trong hệ thống nhiều worker tạo cùng một tệp báo cáo, có thể dùng khóa report:tenant:period để giảm tính toán trùng. Nhưng nếu khóa hết hạn giữa chừng khi đang ghi tệp, hai worker có thể ghi đè cùng một đường dẫn. Cách an toàn hơn là ghi vào tệp tạm, thực hiện fsync·kiểm tra rồi rename nguyên tử, và ghi phiên bản công việc cùng token người tạo vào kho siêu dữ liệu.
Hệ thống tệp dùng chung hay object storage có mô hình sự cố riêng, tách biệt với kho khóa. Chỉ việc giành được khóa không bảo đảm khả năng hiển thị, độ bền và việc tạo có điều kiện của hệ thống tệp, nên cần dùng kèm các điều kiện phía tài nguyên như If-None-Match của object, version ID, dấu hoàn tất. Với sản phẩm dung lượng lớn, hàng đợi công việc và khóa loại trùng có thể phù hợp hơn khóa.
7. Chuyên sâu: Quan hệ giữa hết hạn thuê và fencing token
Hiểu lầm nguy hiểm nhất trong khóa phân tán là giả định "TTL đã hết nên công việc trước cũng đã xong". Tiến trình có thể đã nhận tín hiệu dừng nhưng tạm thời không được bộ lập lịch kernel cho chạy, và phản hồi mạng có thể đến sau một khoảng trễ. Nếu client trước ghi vào tài nguyên được bảo vệ mà không biết mình đã mất quyền sở hữu ở kho khóa, tác dụng phụ sẽ phát sinh đồng thời với client mới.
Fencing token chặn vấn đề này ở phía tài nguyên. Mỗi lần giành khóa cấp một số tăng dần như n, n+1, và tài nguyên được bảo vệ lưu số đã chấp nhận gần nhất. Nếu token yêu cầu nhỏ hơn, coi đó là công việc cũ đến muộn và từ chối. Phép kiểm tra này không nên chỉ đặt trong câu lệnh if của mã ứng dụng mà nên đặt càng gần tài nguyên càng tốt, như WHERE version < :token của DB, ghi có điều kiện của storage, số thế hệ của hàng đợi công việc.
Tính tăng đơn điệu của token được phân biệt với gia hạn thuê. Khi cùng một chủ sở hữu chỉ kéo dài TTL thì không cần đổi token mỗi lần, nhưng khi quyền sở hữu chuyển sang client khác thì nhất định phải cấp thế hệ lớn hơn. Hãy xem xét lưu trữ bền và thủ tục phục hồi để bộ đếm không bị lùi lại do khôi phục kho hay phục hồi snapshot, và tài liệu hóa bất biến rằng token không bao giờ bị tái sử dụng.
Cũng có những API bên ngoài không thể áp dụng fencing token. Khi đó, kết hợp khóa lũy đẳng của lời gọi bên ngoài, thời điểm hết hạn yêu cầu, bảng phát hiện trùng và giao dịch bù trừ để giảm rủi ro. Ngay cả vậy, với các công việc không thể hủy nguyên tử như thanh toán·chuyển tiền, không hứa hẹn xử lý đúng một lần chỉ bằng khóa phân tán, mà lấy khóa lũy đẳng của nhà cung cấp thanh toán và quy trình đối soát hậu kiểm làm tuyến phòng thủ cuối cùng cho tính nhất quán.
8. Các điểm cần cân nhắc và hàm ý
- Phạm vi bảo vệ và thiết kế khóa: Phân tích khóa toàn cục, khóa theo tenant và khóa theo tài nguyên bằng đồ thị tranh chấp nghiệp vụ để chỉ khóa đúng phạm vi cần thiết. Đưa phiên bản, tenant, kỳ vào tên khóa, đồng thời quy định quy tắc đặt tên chung và nhóm sở hữu để các dịch vụ khác nhau không gọi cùng một tài nguyên logic bằng các tên khác nhau.
- Nêu rõ mô hình sự cố: Liệt kê sự cố kho, phân tách mạng, tiến trình dừng, GC pause, sai lệch thời gian, khởi động lại và phục hồi snapshot. Với mỗi sự cố, quyết định ưu tiên tính an toàn hay tính sẵn sàng, và ghi lại vào bài làm cũng như runbook vận hành rằng khi giả định thiết kế bị phá vỡ sẽ hành xử fail-closed hay fail-open.
- Ngân sách TTL·gia hạn: Tính TTL dựa trên thời gian công việc p99 và thời gian tạm dừng tối đa, và lan truyền ngay thất bại gia hạn thành thất bại nghiệp vụ. Thu thập metric về hết hạn, trễ gia hạn và nắm giữ kéo dài, đặt giới hạn trên cho thời gian giữ khóa để sự cố không lan rộng trong thời gian dài.
- Chống zombie và tính nhất quán: Áp dụng đồng thời giải phóng có kiểm tra token chủ sở hữu, fencing token, cập nhật có điều kiện của DB và khóa lũy đẳng. Không ghi nhận giành khóa thành công là nghiệp vụ thành công, mà quản lý riêng kết quả commit của tài nguyên được bảo vệ và trạng thái đối soát.
- Hiệu năng và chi phí: Phản ánh thời gian khứ hồi của máy chủ khóa, tỷ lệ tranh chấp, độ dài hàng chờ và độ trễ quorum của kho vào SLA. Với công việc tần suất cao, siêu ngắn, bản thân lời gọi máy chủ khóa có thể thành điểm nghẽn, nên hãy so sánh batch cục bộ, phân vùng khóa, tuần tự hóa bằng hàng đợi và kiểm tra lạc quan.
- Vận hành·bảo mật: Bảo vệ chính sách xác thực, mã hóa, cô lập mạng và sao lưu của kho khóa ở mức tương đương dữ liệu nghiệp vụ. Giải phóng cưỡng chế của quản trị viên phải yêu cầu phê duyệt, lý do, thời hạn và log kiểm toán, và không cho phép xóa khóa tùy tiện như một cách ứng phó sự cố tiêu chuẩn.
- Kiểm thử và kiểm chứng: Không chỉ tranh chấp bình thường mà còn tiêm các lỗi như dừng ngay trước TTL, phản hồi trễ, failover của kho, phân tách mạng, trùng lặp khi thử lại, sai lệch đồng hồ. Tự động kiểm chứng bất biến cốt lõi "fencing token thấp không được ghi nhận sau token cao", và ghi lại kết quả tiêm lỗi cùng thời gian phục hồi.
- Hàm ý cho lựa chọn công nghệ: Công việc kết thúc trong một giao dịch DB đơn thì ưu tiên cập nhật có điều kiện, còn nghiệp vụ đa hệ thống dài hạn thì kết hợp với saga, outbox, hàng đợi. Với tài nguyên dùng chung rủi ro cao, hãy xem xét bộ điều phối dựa trên đồng thuận và fencing, nhưng đánh giá cả năng lực vận hành và chi phí tương ứng với độ phức tạp tăng thêm.
Tài liệu tham khảo
- Redis Documentation, "Distributed Locks with Redis": https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/
- Martin Kleppmann, "How to do distributed locking": https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
- PostgreSQL Documentation, "Explicit Locking": https://www.postgresql.org/docs/current/explicit-locking.html
- Apache ZooKeeper Documentation, "Locks": https://zookeeper.apache.org/doc/current/recipes.html#Locks
- etcd Documentation, "Concurrency API": https://etcd.io/docs/v3.6/dev-guide/api_concurrency_reference_v3/
Tóm tắt một câu: Khóa phân tán là phương tiện tuần tự hóa vùng găng vượt qua ranh giới mạng, nhưng tính an toàn không hoàn thiện chỉ bằng TTL, nên phải thiết kế đồng thời quyền sở hữu nguyên tử, phục hồi sự cố, fencing token, cập nhật có điều kiện và tính lũy đẳng.