Distributed Systems: Locks, Logical Clocks và Ordering
8. Distributed Locks — Chi tiết
(Note: Section này bổ sung và đi sâu hơn System Design Patterns file)
8.1 Requirements cho Distributed Lock
Safety: Mutual exclusion — tại một thời điểm, chỉ 1 client hold lock
Liveness: Deadlock-free — client cuối cùng acquire được lock
(kể cả client holding lock crash)
Fault-tolerance: Nếu N/2 nodes up → lock vẫn work
8.2 Redis Single-Instance Lock — Deep Dive
Acquire:
SET lock:{resource} {unique_value} NX PX {ttl_ms}
unique_value = UUID + client_id (để chỉ owner mới release được)
NX = only if not exists
PX = TTL in milliseconds
Tại sao cần TTL?
→ Client crash → lock tự expire → không bị stuck forever
Tại sao cần unique_value?
→ Phân biệt: lock của tôi hay của người khác?
Release (PHẢI atomic bằng Lua script):
-- Chỉ xóa nếu value match (tôi là owner)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
Tại sao Lua script?
Without Lua:
GET lock → value matches (still my lock)
[GC pause, TTL expires, another client acquires lock]
DEL lock → XÓA LOCK CỦA NGƯỜI KHÁC! ← race condition
Lua script = atomic ở Redis → không có race condition
8.3 Redlock — Deep Dive
N = 5 independent Redis instances (không phải cluster/replica)
Acquire Redlock:
1. t1 = current timestamp (milliseconds)
2. Với mỗi Redis instance i (theo thứ tự):
SET lock:{resource} {uuid} NX PX {lock_ttl}
(dùng short timeout để không block quá lâu ở instance down)
count++ nếu thành công
3. t2 = current timestamp
elapsed = t2 - t1
validity_time = lock_ttl - elapsed - clock_drift_factor
4. Nếu count >= 3 (majority) AND validity_time > 0:
→ Lock acquired! Hữu hiệu trong validity_time ms
5. Nếu không:
→ Release tất cả instances đã acquired (cleanup)
→ Retry sau random delay
Release Redlock:
Gửi Lua delete script đến TẤT CẢ 5 instances
(kể cả instance failed — có thể recover sau, phải clean up)
Tại sao N=5 independent instances?
Tolerate 2 failures (N/2 - 1 = 2)
Không dùng master-replica: replica lag có thể leak lock
Không dùng cluster: hash slots → lock có thể chỉ ở 1 node
8.4 Redlock Controversy — Martin Kleppmann vs Salvatore Sanfilippo
Kleppmann's Critique (2016):
Scenario 1 — GC Pause:
Client 1 acquires Redlock (validity = 10s)
Client 1 bị GC pause 15 seconds
Lock expires!
Client 2 acquires lock (valid)
Client 1 resumes → thinks it still holds lock
→ C1 và C2 CÙNG access resource!
Scenario 2 — Clock Jump:
Client acquires lock trên Redis-A, B, C, D, E
Redis-A admin thay đổi clock (NTP sync nhảy vọt)
→ Lock on Redis-A expires sớm hơn expected
→ Another client acquires lock on Redis-A, B, C
→ 6 clients cùng "hold" lock!
Sanfilippo's Defense:
GC pause scenario: bounded GC pause + conservative TTL
Clock jump: disable NTP large jumps, gradual clock adjustment
Redlock cung cấp "reasonable" safety cho nhiều use cases
Kleppmann's conclusion:
Redlock for "efficiency": OK (avoid duplicate work, occasional failures OK)
Redlock for "correctness": NOT OK (use ZooKeeper + fencing token)
Fencing Token là giải pháp đúng đắn:
Lock service trả về monotonic token khi grant lock
Resource server: reject requests với token < last_seen_token
→ Dù lock expire và reassigned → old client's request rejected
→ TRUE mutual exclusion
8.5 etcd-based Locks
etcd = strongly consistent (Raft-based) → phù hợp cho distributed locking
etcd Lock với Leases:
// Tạo lease với TTL
lease = etcd.grant_lease(ttl=30)
// Acquire lock
etcd.put(key="lock/resource", value=client_id, lease=lease.id,
prev_kv=None) // only if not exists
// Keep-alive: renew lease để giữ lock
goroutine {
etcd.keep_alive(lease.id) // gửi keep-alive mỗi TTL/3
}
// Release lock
etcd.delete(key="lock/resource")
etcd.revoke_lease(lease.id)
Ưu điểm vs Redlock:
Raft consensus → strong safety guarantees
Lease cấp fencing token (revision number)
etcd revision là monotonic counter → dùng làm fencing token!
etcd Watch:
Clients watch lock key
Khi key bị delete → notification → retry acquire
Sequential watches = fair lock (FIFO)
9. Logical Clocks — Lamport
9.1 Vấn đề Physical Clocks
Tại sao không dùng wall clock?
Clock Drift:
Hardware oscillator không chính xác tuyệt đối
Drift ~1ms/second → ~86 seconds/day
NTP sync: giảm drift nhưng không eliminate
Clock Skew:
Hai machines có thể differ vài milliseconds đến vài seconds
AWS machines: drift ~1ms typical, có thể lên 200ms+
Machine A: event at t=1000ms
Machine B: event at t=999ms (B's clock slightly behind)
→ Không thể biết event nào xảy ra trước!
Network Latency:
Gửi timestamp: t=1000ms
Message takes 100ms
Receiver nhận: t=1100ms (nhưng local clock đã là 1050ms!)
→ Ai đúng?
Kết luận: Physical clocks KHÔNG reliable để order distributed events
9.2 Happens-Before Relation (Leslie Lamport, 1978)
"Time, Clocks, and the Ordering of Events in a Distributed System"
Bài báo được cite nhiều nhất trong distributed systems
Happens-Before (→) được định nghĩa:
Rule 1: Trong cùng process: nếu a xảy ra trước b → a → b
Rule 2: Message passing:
Nếu a = "send message" và b = "receive message" → a → b
(gửi phải xảy ra trước nhận)
Rule 3: Transitivity:
Nếu a → b và b → c → a → c
Concurrent: nếu không có a → b NOR b → a
→ a và b là CONCURRENT (không liên quan nhân-quả)
Ví dụ:
Process P: a → b → c
Process Q: d → e → f
Message: b → e (P sends, Q receives)
Happens-before:
a→b→c (P's local order)
d→e→f (Q's local order)
b→e (message)
b→f (transitivity: b→e→f)
a→e, a→f (transitivity: a→b→e→f)
Concurrent pairs:
a và d (no path between them)
c và d, c và e, c và f
9.3 Lamport Clock Algorithm
Mỗi process có một counter C (integer)
Rules:
Rule 1: Trước mỗi event, increment counter:
C = C + 1
Rule 2: Khi gửi message m:
C = C + 1
gửi m với timestamp = C
Rule 3: Khi nhận message m với timestamp T:
C = max(C, T) + 1
Ví dụ:
Process A (C_A) Process B (C_B) Process C (C_C)
C_A=1: event a1 C_B=1: event b1 C_C=1: event c1
C_A=2: send→B C_B=2: event b2 C_C=2: send→B
C_A=3: event a3 C_B=max(2,2)+1=3 C_C=3: event c3
receive from A
C_B=max(3,3)+1=4
receive from C
C_B=5: send→A
A receive from B: C_A=max(3,5)+1=6
Lamport Clock Property:
Nếu a → b thì C(a) < C(b) [TRUE]
Nếu C(a) < C(b) thì a → b [FALSE!] ← KHÔNG đảm bảo
Lamport clock chỉ capture happens-before in one direction
Không phân biệt được concurrent events!
Total Order:
Để total order: break ties bằng process ID
(a, pid_A) < (b, pid_B) nếu C(a) < C(b) hoặc (C(a)==C(b) AND pid_A < pid_B)
→ Deterministic total order (nhưng arbitrary với concurrent events)
10. Vector Clocks
10.1 Motivation
Lamport Clock:
a → b ⟹ L(a) < L(b) ✅
L(a) < L(b) ⟹ a → b ❌
Muốn: phát hiện CONCURRENT events!
Vector Clock cho phép: V(a) < V(b) ⟺ a → b
Nếu V(a) không < V(b) AND V(b) không < V(a)
→ a và b là CONCURRENT ✅
10.2 Vector Clock Algorithm
N processes → mỗi process i có vector V_i = [0, 0, ..., 0] (N phần tử)
Rules:
Rule 1: Trước event, increment own component:
V_i[i] = V_i[i] + 1
Rule 2: Khi gửi message:
V_i[i] = V_i[i] + 1
gửi message kèm V_i
Rule 3: Khi nhận message với vector W:
V_i[i] = V_i[i] + 1
V_i[j] = max(V_i[j], W[j]) for all j ≠ i
Ví dụ (3 processes: A=0, B=1, C=2):
A: [1,0,0] a1
A: [2,0,0] send→B
B: [0,1,0] b1
B: [2,2,0] receive from A (max([2,0,0],[0,1,0])+1 at B)
C: [0,0,1] c1
C: [0,0,2] send→B
B: [2,3,2] receive from C (max([2,2,0],[0,0,2])+1 at B)
B: [2,4,2] send→A
A: [3,4,2] receive from B
Comparison:
V(a) < V(b) nếu V(a)[i] ≤ V(b)[i] for all i, và < cho ít nhất 1 i
[2,2,0] vs [0,0,2]:
[2,2,0][A]=2 > [0,0,2][A]=0 → không thể < theo A
[0,0,2][C]=2 > [2,2,0][C]=0 → không thể < theo C
→ CONCURRENT! ✅ (B nhận từ A và C độc lập)
[2,0,0] vs [2,2,0]:
[2,0,0][A]=2 ≤ [2,2,0][A]=2 ✓
[2,0,0][B]=0 ≤ [2,2,0][B]=2 ✓
[2,0,0][C]=0 ≤ [2,2,0][C]=0 ✓
→ [2,0,0] < [2,2,0] → "send from A" → "receive at B" ✅
10.3 Vector Clocks trong thực tế
Amazon DynamoDB (original design, Dynamo paper 2007):
Mỗi version của object có vector clock
Conflict = concurrent writes → trả về cả hai → client reconcile
get(key="cart") → [
({items:[A,B]}, VC=[{node1:2}]),
({items:[A,C]}, VC=[{node2:1}]) ← concurrent write!
]
client: merge = {items:[A,B,C]} → put
Vấn đề với Vector Clocks trong practice:
Size: O(N) với N = number của nodes
Large clusters → vector clock lớn
Giải pháp: Version Vectors (chỉ track nodes có writes)
Riak:
Dùng Dotted Version Vectors (DVV) — improved version
Giải quyết false conflicts từ Vector Clocks
Git:
Tương tự vector clocks
Merge conflicts khi concurrent edits → manual resolve
10.4 Version Vectors vs Vector Clocks
Vector Clock: track causality của events (operations)
Version Vector: track causality của values (data versions)
Subtle difference:
VC: gắn với operations/messages
VV: gắn với data replicas/values
Practical difference:
VC có thể có false conflicts nếu không cẩn thận
VV được thiết kế đặc biệt cho tracking data versions
Dotted Version Vectors (DVV):
Giải quyết "sibling explosion" trong Riak
Mỗi version có: base (VV) + dot (specific event)
→ Phân biệt chính xác concurrent vs sequential writes
11. Hybrid Logical Clocks (HLC)
11.1 Motivation
Lamport / Vector Clocks:
Không correlate với physical time
Không thể hỏi "events trong khoảng 12:00-13:00"
Debugging khó (timestamps vô nghĩa)
Physical Clocks:
Không đảm bảo causality
Clock skew → wrong ordering
HLC: kết hợp tốt nhất của cả hai!
Correlate với physical time (NTP-based)
Đảm bảo: nếu a → b thì HLC(a) < HLC(b)
HLC values gần với physical time
11.2 HLC Algorithm
Mỗi node có:
l: "logical" part (max physical time seen)
c: counter (tie-breaker khi l bằng nhau)
On local event:
l' = max(l, pt) (pt = physical time now)
if l' == l: c = c + 1
else: l = l', c = 0
On send:
same as local event, gửi (l, c)
On receive(l_m, c_m):
l' = max(l, l_m, pt)
if l' == l == l_m: c = max(c, c_m) + 1
elif l' == l: c = c + 1
elif l' == l_m: c = c_m + 1
else: c = 0
l = l'
HLC timestamp = (l, c)
Comparison: (l1, c1) < (l2, c2) nếu l1 < l2 hoặc (l1 == l2 AND c1 < c2)
Guarantee:
Nếu a → b: HLC(a) < HLC(b) ✅
HLC(a) ≈ physical_time(a) (bounded skew) ✅
Used in: CockroachDB, YugabyteDB