Distributed lock với Redis: lock hết hạn khi chủ còn chạy thì sao? Nâng cao
Thời gian đọc ước tính: 8 phút
Vấn đề
Phần tiêu đề “Vấn đề”Job đồng bộ tồn kho với nhà cung cấp chạy trên 3 pod, đọc số liệu nhà cung cấp rồi ghi qty vào bảng inventory. Chỉ một pod được chạy mỗi lúc; hai pod cùng ghi thì số cũ có thể đè số mới.
Cách hay gặp là một key lock Redis có TTL. Nhưng job đôi khi bị GC pause hoặc treo lâu hơn TTL.
Lock Redis bảo đảm được gì khi chủ lock bị dừng quá hạn, và cần thêm gì để dữ liệu vẫn đúng?
Chọn gì? (tóm tắt trước)
Phần tiêu đề “Chọn gì? (tóm tắt trước)”| Tình huống | Chọn | Điều kiện |
|---|---|---|
| Tránh làm trùng việc, hiếm khi trùng vẫn chấp nhận | SET key token NX PX trên một Redis, nhả bằng Lua |
Job idempotent |
| Cần đúng tuyệt đối | Fencing token kiểm tra ở tài nguyên đích, hệ đồng thuận (etcd, ZooKeeper), hoặc ràng buộc database | Đích từ chối token cũ |
| Đích là PostgreSQL | Advisory lock hoặc row lock | Kèm ghi có điều kiện |
Kleppmann gọi hai nhu cầu đầu là lock cho hiệu quả và cho tính đúng; nhầm hai loại là lỗi chính.
Khái niệm cốt lõi
Phần tiêu đề “Khái niệm cốt lõi”Lock Redis thực chất là một lease: tự hết khi TTL về 0 dù chủ còn chạy. Advisory lock thì ngược lại: không có hạn, mất khi connection đứt.
Redis SET NX PX |
PostgreSQL advisory lock | |
|---|---|---|
| Mất lock khi | TTL hết, failover | Connection đứt |
| Chủ biết mình mất lock? | Không | Khi query kế tiếp báo lỗi connection |
| Fencing token | Không | Không |
Cách hoạt động
Phần tiêu đề “Cách hoạt động”Lấy lock: vì sao không dùng SETNX rồi EXPIRE
Phần tiêu đề “Lấy lock: vì sao không dùng SETNX rồi EXPIRE”Value là token ngẫu nhiên mới cho mỗi lần lấy. SETNX rồi EXPIRE là hai lệnh: chết ở giữa thì key không có hạn (TTL trả -1), lock kẹt mãi. SETNX deprecated từ 2.6.12; dùng một lệnh SET key token NX PX 30000 (OK, hoặc nil nếu key đã có).
Nhả lock: so token trong Lua
Phần tiêu đề “Nhả lock: so token trong Lua”DEL thẳng xoá bất kỳ lock nào ở key, kể cả lock pod khác vừa lấy sau khi lock của mình hết hạn. Script chạy nguyên tử nên không có khe giữa GET và DEL (xem Lua script):
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0endRedis 8.4 thêm DELEX key IFEQ value làm đúng việc này bằng một lệnh; Redis 7.x vẫn dùng script.
Lock hết hạn khi chủ còn chạy
Phần tiêu đề “Lock hết hạn khi chủ còn chạy”Không cần Redis lỗi: chỉ cần Pod A dừng lâu hơn TTL. Hình đi theo lần chạy demo:
Pod A lấy lock TTL 300 ms và INCR được fencing token 33.
| Bước | Redis giữ lock của | Pod tin mình là chủ | inventory |
|---|---|---|---|
| 1–3 | A, rồi không ai | A | qty = 100, fence_token = 0 |
| 4 | B | A và B | không đổi |
| 5 | B | A và B | qty = 95, fence_token = 34 |
| 6 | B | A và B | A bị từ chối |
Bỏ điều kiện fence_token < $token thì A ghi qty = 120 đè số mới của B. Lock không ngăn được; chỉ tài nguyên đích ngăn được.
Watchdog gia hạn không cứu được GC pause
Phần tiêu đề “Watchdog gia hạn không cứu được GC pause”Watchdog (có trong vài thư viện như Redisson) là timer gia hạn TTL định kỳ, chạy trong cùng process với job: GC pause làm timer đứng theo. Demo: watchdog trả 0 khi A tỉnh, đã muộn.
Kiểm tra lock ngay trước khi ghi cũng không đủ: pause có thể xảy ra ngay sau đó.
Failover: replica chưa nhận key lock
Phần tiêu đề “Failover: replica chưa nhận key lock”Replication của Redis là bất đồng bộ; Sentinel hay Cluster đều failover sang replica:
Đang tải diagram…
Redlock: N master độc lập và quorum
Phần tiêu đề “Redlock: N master độc lập và quorum”Redlock (redis.io) dùng N master không replication, ví dụ N = 5 trên 5 máy:
- Ghi thời điểm bắt đầu, gửi
SET key token NX PX ttltới cả 5 node song song, timeout mỗi node nhỏ so với TTL. - Thành công khi lấy được quá bán (≥ 3) và thời gian đã trôi nhỏ hơn TTL.
- Thời gian hiệu lực = TTL − thời gian đã trôi − bù clock drift.
- Thất bại thì nhả trên mọi node, thử lại sau khoảng ngẫu nhiên.
Node restart mất key (xem RDB và AOF) có thể cho hai client cùng quá bán; tài liệu đề xuất trì hoãn restart quá TTL lớn nhất.
Chi tiết vận hành
Phần tiêu đề “Chi tiết vận hành”Tranh luận năm 2016 quanh Redlock, redis.io dẫn cả hai phía:
| Điểm | Martin Kleppmann | antirez (tác giả Redis) |
|---|---|---|
| Fencing | Redlock không sinh được số tăng dần | Đích kiểm tra được token thì đã tự loại trừ được; có thể check-and-set bằng token ngẫu nhiên |
| Thời gian | Dựa vào giả định về trễ mạng, pause, đồng hồ | Chỉ cần đếm thời gian trôi với sai số giới hạn |
| Pause sau khi lấy lock | Gây hai chủ | Ảnh hưởng mọi lock tự hết hạn, không riêng Redlock |
| Khuyến nghị | Hiệu quả: một Redis. Đúng: hệ đồng thuận + fencing | Redlock dùng được khi cần loại trừ |
redis.io hiện khuyên triển khai fencing token và ghi rõ TTL không dùng monotonic clock: chỉnh giờ hệ thống có thể cấp lock cho nhiều process.
Fencing token cần nguồn tăng dần bền. INCR trên Redis có replica có thể lặp token sau failover. Nguồn chắc hơn: database đích (version, sequence) hoặc hệ đồng thuận (revision của etcd, zxid của ZooKeeper).
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Demo examples/src/redis-distributed-lock/demo.ts (Redis 7.4.9, PostgreSQL 17): Pod A chạy trong worker thread; GC pause giả lập bằng Atomics.wait 900 ms, chặn cả event loop của thread. TTL 300 ms, watchdog mỗi 100 ms.
Output thật, rút gọn (mốc ms đổi theo lần chạy)
2. A nhả lock bằng DEL thẳng sau khi lock đã hết hạn A: SET NX PX 300 -> OK B: SET NX PX 5000 -> OK A: DEL -> 1 (xoá lock của B) C: SET NX PX 5000 -> OK => B và C cùng tin mình giữ lock3. Như bước 2, nhả bằng Lua A: EVAL release(tokenA) -> 0 (token không khớp, không xoá)4. Watchdog + GC pause: lock hết hạn khi A còn chạy, không fencing [ 47 ms] A: SET NX PX 300 -> OK, fencing token = 33 [ 249 ms] A: watchdog PEXPIRE 300 -> 1 (còn gia hạn được) [ 298 ms] A: bắt đầu GC pause 900 ms [ 555 ms] B: SET NX PX 5000 -> OK, fencing token = 34 [ 560 ms] B: ghi qty = 95 với token 34 -> UPDATE 1 row [1198 ms] A: tỉnh dậy, ghi qty = 120 với token 33 [1200 ms] A: watchdog PEXPIRE 300 -> 0 [1200 ms] A: UPDATE -> 1 row5. Như bước 4, có fencing: UPDATE ... WHERE fence_token < $token [1187 ms] A: UPDATE -> 0 row (bị từ chối: token cũ) Kết quả: { qty: 95, fence_token: '34', updated_by: 'pod-b' }Bước 4: số cũ 120 đè số mới 95. Bước 5: PostgreSQL giữ 95. Bộ đếm đặt sẵn 32 để token khớp hình.
Trade-offs
Phần tiêu đề “Trade-offs”Ưu điểm
-
Một lệnh, độ trễ thấp.
-
TTL tự giải phóng khi chủ chết.
-
Đủ cho lock hiệu quả.
< slot="cons"> >
-
Hết hạn khi chủ còn chạy; failover có thể mất lock.
-
Không có fencing token; tài nguyên đích phải tự kiểm tra.
-
Redlock tốn 5 node, vẫn dựa vào giả định thời gian.
Nhược điểm
Checklist production
Phần tiêu đề “Checklist production”- Ghi rõ lock cho hiệu quả hay tính đúng; loại sau bắt buộc có fencing.
- Nhả bằng script so token (Redis 8.4+:
DELEX ... IFEQ); cấmDELthẳng. - TTL lớn hơn p99 thời gian job; job có deadline riêng, quá hạn thì dừng.
- Mọi lần ghi mang fencing token từ nguồn bền; đích kiểm tra
fence_token < $token. - Job vẫn idempotent: fencing không chặn side effect ngoài như gọi API hai lần.
- Cảnh báo khi script nhả trả
0: lock đã hết hạn giữa chừng.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Lỗi phổ biến nhất nằm ở bước nhả lock trong finally:
Nhả lock: DEL thẳng xoá cả lock của pod khác
Hay gặp
await redis.set(key, '1', 'PX', 30000, 'NX');try { await syncInventory(); } finally { await redis.del(key); }Nên viết
const token = randomBytes(16).toString('hex');if ((await redis.set(key, token, 'PX', 30000, 'NX')) !== 'OK') return;try { await syncInventory(); } finally { await redis.eval(RELEASE, 1, key, token); }Vì sao
Job quá TTL thì DEL xoá lock của pod khác (demo bước 2).
Sai lầm thứ hai là tin gia hạn là đủ:
| Cách hiểu sai | Hệ quả | Cần làm |
|---|---|---|
| Kiểm tra lock trước khi ghi là đủ | Pause ngay sau kiểm tra | Fencing ở tài nguyên đích |
INCR trong Redis là token bền |
Failover làm token lặp | Lấy token từ database hoặc hệ đồng thuận |
Tự kiểm tra
Phần tiêu đề “Tự kiểm tra”- Ở demo bước 2, vì sao
DELcủa A trả1dù lock của A đã hết hạn? - Watchdog gia hạn mỗi 100 ms, TTL 300 ms. Vì sao B vẫn lấy được lock khi A pause 900 ms?
- Kể một chuỗi sự kiện cho hai client cùng giữ quá bán 3/5 node Redlock.
Tóm tắt
Phần tiêu đề “Tóm tắt”Tham khảo
Phần tiêu đề “Tham khảo”- Redis Docs — Distributed Locks: công thức một instance, Redlock, failover, fencing, clock.
- Redis Docs — SET và SETNX:
NX,PX, deprecation. - Redis Docs — Replication: replication bất đồng bộ.
- Martin Kleppmann — How to do distributed locking: efficiency và correctness, fencing token.
- antirez — Is Redlock safe?: phản biện về fencing, clock, pause.