- Khi hai user cùng bấm đặt vé một lúc, ai sẽ thắng?
- Không có distributed lock, cả hai đều thắng — và hệ thống của bạn sẽ bán gấp đôi số vé còn lại.
- Bài viết đi sâu vào cơ chế, so sánh Redis vs ZooKeeper vs etcd, và những bẫy chết người mà hầu hết engineer bỏ qua.
TL;DR
Distributed lock là cơ chế đảm bảo chỉ một process tại một thời điểm được phép đọc/ghi vào một tài nguyên dùng chung trên nhiều server. Không có nó, bạn sẽ có race condition, dữ liệu hỏng, double charge, oversell inventory — đủ mọi thứ tệ nhất của hệ thống phân tán. Nhưng implement sai còn tệ hơn không có.
Bài toán gốc: hai người, một chiếc ghế
Tưởng tượng hệ thống đặt vé concert. Còn đúng 1 ghế. User A và User B bấm "Đặt" cùng lúc từ hai server khác nhau.
Không có điều phối:
- Server A đọc DB: còn 1 ghế → xử lý đặt
- Server B đọc DB: còn 1 ghế → xử lý đặt
- Cả hai ghi thành công → ghế được bán hai lần
Đây không phải edge case — đây là điều xảy ra mặc định trong mọi hệ thống phân tán không có điều phối. Distributed lock giải quyết bằng cách: process đầu tiên acquire lock, xử lý, release. Process thứ hai phải chờ hoặc fail. Chỉ một booking thành công.
Cơ chế hoạt động
Flow cơ bản:
- Process yêu cầu lock cho resource
- Nếu lock available → được cấp
- Nếu đang bị lock → wait / retry / fail tùy config
- Sau khi xong công việc → release lock
Ba tính chất bắt buộc (theo Redis official docs):
- Mutual exclusion: tại bất kỳ thời điểm nào, chỉ 1 client giữ lock
- Deadlock-free: lock phải tự expire nếu holder crash — không được block mãi mãi
- Fault tolerance: cơ chế lock phải survive khi một số node chết
Điểm mấu chốt: lock phải có TTL (Time-To-Live). Nếu process giữ lock rồi crash mà không có TTL, resource bị block vĩnh viễn — hệ thống chết.
Redis, ZooKeeper, etcd — chọn cái nào?
Đây là câu hỏi quan trọng nhất, và câu trả lời phụ thuộc vào bạn cần efficiency hay correctness.
| Redis | ZooKeeper | etcd | Database lock | |
|---|---|---|---|---|
| Consistency | AP (eventual) | CP (strong) | CP (strong) | Phụ thuộc DB |
| Performance | Rất cao | Trung bình | Trung bình | Thấp |
| Blocking support | Không native | Có (watch) | Có (watch) | Có (SELECT FOR UPDATE) |
| Fencing tokens | Không | Có (zxid) | Có | Via version field |
| Dùng khi | High concurrency, efficiency | Critical correctness | Kubernetes/cloud | Scale thấp, đơn giản |
Redis dùng lệnh SET key value NX PX 10000: NX = chỉ set nếu chưa tồn tại (atomic), PX 10000 = TTL 10 giây. Release phải qua Lua script để atomic check-and-delete — dùng bare DEL là sai vì có thể xóa nhầm lock của người khác. Redis 8.4 có lệnh DELEX cho việc này.
ZooKeeper dùng ephemeral sequential nodes: mỗi client tạo node /locks/lock-XXXXX, node có sequence number nhỏ nhất thắng. Khi holder disconnect, node tự xóa → lock tự release. Là hệ thống CP — strong consistency. Hỗ trợ fencing tokens qua zxid hoặc znode version number.
etcd dùng leases + CAS transactions, với keep-alive giữ lease sống. Client crash → lease expire → lock release tự động. Default coordination backend trong Kubernetes.
Database locks: đơn giản nhưng không scale — áp lực lên DB, nguy cơ lock toàn bảng.
Redlock và những gì bị giấu
Redis đề xuất thuật toán Redlock cho multi-node setup: acquire lock trên đa số (3/5) trong 5 Redis master độc lập. Nghe có vẻ robust.
Năm 2016, Martin Kleppmann — tác giả của Designing Data-Intensive Applications — viết một bài phân tích và kết luận Redlock là "neither fish nor fowl": quá nặng cho efficiency locks, không đủ an toàn cho correctness locks.
Ba vấn đề cốt lõi:
- Clock drift: Redis dùng
gettimeofday(wall clock, không phải monotonic clock). Nếu NTP correction hoặc admin chỉnh thủ công làm clock nhảy lên trên node C, lock expire sớm → client khác acquire cùng lock → hai client cùng nghĩ mình đang giữ lock - GC pauses: Process Java đang giữ lock gặp stop-the-world GC 30 giây → lock expire → client khác acquire → khi GC xong, client cũ tiếp tục ghi dữ liệu dù không còn lock hợp lệ → data corruption
- Không có fencing tokens: Redlock không generate monotonically increasing number. ZooKeeper có
zxid— storage system có thể reject write request với token thấp hơn token đã thấy. Redlock không có khả năng này → không bảo vệ được khi network delay làm packet đến sau khi lease hết hạn
GitHub từng ghi nhận packet bị delay ~90 giây trong network — write request gửi đi khi còn giữ lock, đến storage khi lease đã hết hạn từ lâu.
5 bẫy chết người khi tự implement
Alibaba đã phân tích chi tiết các lỗi phổ biến:
- Không có TTL: process crash → lock block vĩnh viễn → service down
- Dùng bare DEL để release: execution time vượt TTL → lock expire → client B acquire → client A DEL xóa lock của B → client C acquire cùng resource với B → chaos
- Clock jump: node C trong Redlock cluster clock nhảy → lock expire sớm → lock bị acquire 2 lần
- Persistence gap: Redis AOF mặc định sync mỗi 1 giây. Node crash trong khoảng đó → restart không có lock state → lock được acquire lại bởi client khác dù client cũ vẫn nghĩ mình đang giữ
- Thread ID làm lock identifier: thread ID có thể giống nhau trên các instance khác nhau → sai lock owner. Phải dùng UUID hoặc
node_id + thread_id
Trong thực tế: ai dùng gì
Dựa trên các use case được document:
- E-commerce flash sales: Alibaba dùng segmented locking — 100 inventory items chia thành 5 Redis key × 20 item mỗi key → throughput tăng 5× so với 1 lock duy nhất
- Fintech / payment: single transaction execution, balance update — yêu cầu correctness → ZooKeeper hoặc DB transactions + fencing tokens
- Distributed schedulers: leader election để chỉ 1 server chạy cron job → tránh duplicate expensive computation
- Kubernetes: etcd là lock backend mặc định cho controller election
Redisson implement watchdog pattern: background thread renew TTL mỗi 10 giây — bảo vệ khỏi lock expire khi business logic đang chạy dài.
Khi nào KHÔNG nên dùng distributed lock
Câu trả lời là hầu hết thời gian. Kleppmann và nhiều expert đồng ý: ưu tiên alternatives trước.
- Idempotency: thiết kế operation sao cho chạy 2 lần vẫn cho kết quả đúng — nếu lock fail và client khác re-run, không có hậu quả xấu
- Database constraints:
SELECT FOR UPDATE, unique index, version field + CAS — dùng locking mechanism có sẵn của DB, tránh clock drift và network delay của external lock service - Queue-based: route tất cả request cho một resource qua single message queue partition → sequential processing không cần lock
Quyết định đơn giản: nếu lock chỉ để tránh làm việc thừa (efficiency) → single Redis node, SETNX. Nếu lock để tránh data corruption (correctness) → ZooKeeper/etcd + fencing tokens, hoặc DB transactions.
Takeaway
Distributed locks giải quyết vấn đề thực nhưng mang theo complexity lớn. Ba nguyên tắc không đàm phán được:
- Luôn có TTL. Không ngoại lệ.
- Release phải atomic — verify owner trước khi delete.
- Cần correctness → ZooKeeper/etcd + fencing tokens, không phải Redlock.
Nguồn: Redis Distributed Locks Docs, Martin Kleppmann — How to do distributed locking, Alibaba Cloud — Distributed Lock Best Practices.