Progress · 0/8 sections
- 01 — Load Balancing at Scale (50M Users)
- 02 — Caching Strategies (50M Users, Multi-Layer)
- 03 — Service Discovery & Service Mesh (50M Users
- 04 — Observability in Production (50M Users)
- 05 — Failure Handling & Resilience (50M Users)
- 06 — Distributed Locking & Coordination (50M Use
- 07 — Data Pipelines & Stream Processing (50M Use
- 08 — Deployment & Infrastructure at Scale (50M U
06 — Distributed Locking & Coordination (50M Users)
8 min read
06 — Distributed Locking & Coordination (50M Users)
Scenario: Multiple instances of same service chal rahi hain. Ek shared resource pe sirf EK instance kaam kare — yeh kaise ensure karein? Payment double charge na ho, cron job duplicate na chale.
Why Distributed Locks?
Single server: sync.Mutex → done
Multiple servers: sync.Mutex sirf local process mein kaam karta hai
Problem examples:
1. Payment: 2 servers simultaneously charge same order → double payment
2. Inventory: 2 servers read "5 items left", both decrement → oversold
3. Cron: scheduled job runs on ALL 10 instances → 10x emails sent
4. Migration: data migration should run on exactly 1 instance
Redis Distributed Lock (Single Node)
Simple SETNX
func AcquireLock(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) (string, error) {
// Unique value = fencing token (proves ownership)
lockValue := uuid.New().String()
// SET key value NX EX ttl
// NX = only set if not exists (atomic)
// EX = auto-expire after ttl (prevents deadlock if holder crashes)
ok, err := rdb.SetNX(ctx, "lock:"+key, lockValue, ttl).Result()
if err != nil {
return "", err
}
if !ok {
return "", ErrLockNotAcquired
}
return lockValue, nil
}
func ReleaseLock(ctx context.Context, rdb *redis.Client, key, lockValue string) error {
// Lua script: only delete if value matches (prevent releasing someone else's lock)
script := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`
result, err := rdb.Eval(ctx, script, []string{"lock:" + key}, lockValue).Int64()
if err != nil {
return err
}
if result == 0 {
return ErrLockNotOwned
}
return nil
}Why Lua Script for Release?
Without Lua (RACE CONDITION):
Server A: GET lock → value matches mine ✅
--- Network delay / GC pause ---
Lock expires. Server B acquires lock.
Server A: DEL lock → DELETES SERVER B's LOCK! ❌
With Lua: GET + compare + DEL is atomic on Redis server.
Redlock — Multi-Node Redis Lock
Problem with Single Redis
Redis master dies after accepting SET (lock acquired).
Replica promotes to master but didn't replicate the SET yet.
Another client acquires SAME lock on new master.
Two clients think they hold the lock → DATA CORRUPTION.
Redlock Algorithm (Martin Kleppmann vs Antirez debate)
5 independent Redis masters (no replication between them).
To acquire lock:
1. Get current time (T1)
2. Try SET NX on ALL 5 Redis instances with same key+value+TTL
3. Lock acquired if:
- Majority (≥3) instances accepted the SET
- Total time to acquire < TTL
4. Effective TTL = original TTL - time spent acquiring
5. If failed → release lock on ALL instances
To release: DEL on ALL 5 instances
⚠️ Redlock Controversy
Martin Kleppmann's argument:
"Redlock is fundamentally broken for correctness."
Problem: Process pause (GC, page fault) AFTER acquiring lock
Client A: Acquires Redlock (TTL=10s)
Client A: Long GC pause (15 seconds)
Lock expires on all Redis nodes
Client B: Acquires same lock
Client A: GC finishes, thinks it still has lock
Client A: Writes to DB → CONFLICT with Client B
Solution needed: Fencing tokens
Fencing Tokens
Every lock acquisition returns an incrementing token.
Lock acquire #1 → fencing_token = 33
Lock acquire #2 → fencing_token = 34
Client sends fencing token WITH every write to storage.
Storage rejects writes with OLD tokens.
Client A: lock acquired, token=33
Client A: GC pause... lock expires
Client B: lock acquired, token=34
Client B: writes to DB with token=34 ✅ (DB records: last_token=34)
Client A: resumes, writes to DB with token=33
DB: "33 < 34, reject!" ✅ SAFE
// Fencing token with lock
type FencedLock struct {
Token int64
Owner string
}
// Storage checks token before write
func (s *Storage) Write(ctx context.Context, key string, value []byte, fencingToken int64) error {
// Atomic compare-and-write
result := s.db.ExecContext(ctx,
`UPDATE resources SET value = $1, fencing_token = $2
WHERE key = $3 AND fencing_token < $2`,
value, fencingToken, key,
)
if result.RowsAffected() == 0 {
return ErrStaleFencingToken
}
return nil
}ZooKeeper — Purpose-Built Coordination
ZooKeeper Basics
ZooKeeper = distributed coordination service
- Consistent (linearizable reads/writes)
- Uses ZAB consensus (similar to Raft)
- Data model: hierarchical znodes (like filesystem)
- Ephemeral znodes: auto-delete when session ends
- Sequential znodes: auto-incrementing suffix
- Watches: get notified when znode changes
Used by: Kafka (older versions), HBase, Hadoop, Solr
ZooKeeper Lock Recipe
To acquire lock on resource "orders":
1. Create ephemeral sequential znode:
/locks/orders/lock-0000000001 (my node)
2. Get all children of /locks/orders/:
lock-0000000001 ← mine
lock-0000000002
lock-0000000003
3. Am I the LOWEST numbered?
YES → I have the lock ✅
NO → Set watch on the node JUST before me
Wait for notification
4. Node before me deleted → re-check if I'm lowest now
Release: Delete my ephemeral znode (or session timeout auto-deletes)
Why sequential + ephemeral?
Sequential: Fair ordering (FIFO lock)
Ephemeral: If holder crashes, session expires → znode auto-deleted → next waiter gets lock
No deadlocks! No manual cleanup! Built-in fencing via znode version.
ZooKeeper vs Redis for Locks
ZooKeeper:
✅ Consensus-based (CP system) — correct by design
✅ Ephemeral znodes = automatic deadlock recovery
✅ Sequential = fair lock ordering
✅ Watches = efficient waiting (no polling)
❌ Slower (consensus overhead)
❌ More complex to operate
❌ Heavier (JVM-based, needs dedicated cluster)
Redis (single/Redlock):
✅ Fast (~1ms acquire)
✅ Simple to operate
✅ Already in your stack (caching)
❌ Not consensus-based (correctness caveats)
❌ TTL-based expiry = clock dependency
❌ Redlock debate (may not be safe for critical paths)
Recommendation:
Payment/financial: ZooKeeper or etcd (correctness matters)
Idempotency/dedup: Redis lock (good enough, fast)
Cron dedup: Redis lock with TTL (simple, effective)
etcd — Modern Alternative to ZooKeeper
etcd = distributed key-value store using Raft consensus
Used by Kubernetes for all cluster state.
Lock via etcd lease:
1. Create lease (TTL)
2. PUT key with lease attached
3. If key created → lock acquired
4. Keep-alive to extend lease
5. Stop keep-alive or crash → lease expires → lock released
Simpler than ZooKeeper, Go-native, Raft-based.
Kubernetes clusters already have etcd running.
Leader Election
Problem
Scheduler service: 3 replicas for HA.
But only ONE should execute scheduled jobs.
Others = standby (take over if leader dies).
Leader Election with etcd/K8s
import "k8s.io/client-go/tools/leaderelection"
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
Lock: &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: "scheduler-leader",
Namespace: "default",
},
Client: kubeClient.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: hostname, // unique per instance
},
},
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
// I am the leader — start doing work
runScheduler(ctx)
},
OnStoppedLeading: func() {
// Lost leadership — stop work, become standby
logger.Info("lost leader election")
},
OnNewLeader: func(identity string) {
logger.Info("new leader elected", zap.String("leader", identity))
},
},
})Distributed Cron / Scheduled Jobs
Problem
"Every day 2am pe stale sessions cleanup karo"
10 instances of the service → 10 cleanup jobs run simultaneously?
Solutions:
1. Leader election: sirf leader runs cron
2. Distributed lock: lock acquire karo, first one wins, others skip
3. Dedicated scheduler service (1 instance with HA failover)
Simple Distributed Cron with Redis Lock
func RunDistributedCron(ctx context.Context, rdb *redis.Client) {
ticker := time.NewTicker(1 * time.Hour)
for {
select {
case <-ticker.C:
lockValue, err := AcquireLock(ctx, rdb, "cron:cleanup-sessions", 30*time.Minute)
if err != nil {
// Another instance got the lock, skip
continue
}
// I got the lock — do the work
cleanupStaleSessions(ctx)
ReleaseLock(ctx, rdb, "cron:cleanup-sessions", lockValue)
case <-ctx.Done():
return
}
}
}Production Recommendations — 50M Users
Use case → Solution:
Idempotency (prevent double processing):
→ Redis SETNX with request ID, TTL = processing window
Inventory/stock management:
→ Database row-level locks (SELECT FOR UPDATE)
→ OR optimistic locking (version column)
Payment processing:
→ etcd/ZooKeeper locks with fencing tokens
→ Idempotency key stored in DB (permanent record)
Cron job deduplication:
→ Redis lock with TTL slightly > job duration
Leader election:
→ Kubernetes Lease API (if on K8s)
→ etcd lease (if not on K8s)
Rate limiting counters:
→ Redis INCR with EXPIRE (atomic counter)
General rules:
✅ Always set TTL on locks (prevent deadlocks)
✅ Use fencing tokens for critical paths
✅ Prefer database-level guarantees over distributed locks when possible
✅ Make operations idempotent — then locks become optimization, not correctness
❌ Don't hold locks for long durations (> 30s suspicious)
❌ Don't nest distributed locks (deadlock risk)