Distributed Locking - Complete Deep Dive
Prerequisites: Database Concepts, Caching Used in: Digital Wallet, BookMyShow, Job Scheduler, any system with shared mutable state
What is Distributed Locking?
A distributed lock ensures that only ONE process/server can access a shared resource at a time, even when multiple servers are running.
Real-world analogy: A bathroom with a lock. One person enters, locks it. Others wait. When done, they unlock, and the next person enters. In distributed systems, the “bathroom” is a shared resource (database row, file, external API) and the “people” are different servers.
Why it’s needed:
Without lock:
Server A: reads balance=100, deducts 80 → writes 20
Server B: reads balance=100 (same time!), deducts 80 → writes 20
Expected final: 100 - 80 - 80 = -60 (impossible!) or 20 (lost update)
With lock:
Server A: acquires lock → reads 100 → deducts 80 → writes 20 → releases lock
Server B: tries lock → WAITS → acquires lock → reads 20 → deducts 80 → FAILS (insufficient)
Correct!
When Do You Need Distributed Locks?
| Scenario | Why |
|---|---|
| Prevent double-booking | Two users try to book the last seat at the same time |
| Prevent double-spending | Two transfers from the same wallet concurrently |
| Exactly-once job execution | Cron job running on 5 servers — only one should execute |
| Rate limit writes to external API | Only one server should call the API at a time |
| Resource ownership | Only one worker processes a specific task |
Approaches
1. Redis SETNX (Simplest, Most Common)
Use Redis SET key value NX EX ttl — sets the key only if it doesn’t exist (NX), with expiry (EX).
ACQUIRE LOCK:
SET "lock:order_123" "server_A_uuid" NX EX 30
→ returns OK if acquired (key didn't exist)
→ returns nil if someone else holds it
RELEASE LOCK (only if you own it):
if GET "lock:order_123" == "server_A_uuid":
DEL "lock:order_123"
Why UUID as value? To ensure only the lock OWNER can release it. Without it, Server B could accidentally delete Server A’s lock.
Why TTL (EX 30)? If the lock holder crashes, the lock auto-expires after 30s. Without TTL, a crashed holder keeps the lock forever (deadlock).
// Java implementation
public boolean acquireLock(String resource, String owner, int ttlSeconds) {
String key = "lock:" + resource;
String result = redis.set(key, owner, SetParams.setParams().nx().ex(ttlSeconds));
return "OK".equals(result);
}
public boolean releaseLock(String resource, String owner) {
String key = "lock:" + resource;
// Lua script for atomic check-and-delete
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
return redis.eval(lua, List.of(key), List.of(owner)) == 1;
}
Why Lua for release? GET and DEL must be atomic. Without Lua:
Server A: GET lock → "server_A" (correct owner)
--- A's TTL expires here ---
Server B: SET lock → "server_B" (acquires lock)
Server A: DEL lock → deletes B's lock! ← BUG
Pros: Simple, fast (~1ms), widely used. Cons: Single Redis instance is a SPOF. If Redis crashes, lock is gone.
2. Redlock (Multi-Node Redis)
For higher reliability, acquire locks on a MAJORITY of Redis nodes (e.g., 3 out of 5).
5 independent Redis instances.
ACQUIRE:
Try SET NX EX on all 5 instances
If successful on ≥ 3 (majority) → lock acquired
If < 3 → release all, retry after random delay
RELEASE:
DEL key on ALL 5 instances
Pros: Tolerates up to 2 Redis nodes dying. Cons: Complex. Debated in the community (Martin Kleppmann’s critique). Clock skew issues.
For interviews: Mention Redlock exists but say “for most applications, single Redis SETNX with TTL is sufficient. Redlock adds complexity.”
3. DynamoDB Conditional Write
Use PutItem with a condition expression.
Put lock item:
PK: "lock:order_123"
owner: "server_A"
expiresAt: now + 30s
ConditionExpression: "attribute_not_exists(PK) OR expiresAt < :now"
If condition passes → lock acquired
If condition fails → someone else holds it → retry or fail
Pros: No extra infrastructure (already using DynamoDB). Serverless-friendly. TTL via DynamoDB TTL feature. Cons: ~10ms latency (vs ~1ms Redis). Eventually consistent reads might miss a lock (use consistent reads).
4. Database Row Locking (SELECT FOR UPDATE)
Use your existing relational database.
BEGIN;
SELECT * FROM locks WHERE resource = 'order_123' FOR UPDATE;
-- If row exists and not expired → someone holds it → rollback
-- If row doesn't exist or expired → insert/update with our owner
INSERT INTO locks (resource, owner, expires_at)
VALUES ('order_123', 'server_A', NOW() + INTERVAL '30 seconds')
ON CONFLICT (resource) DO UPDATE SET owner = 'server_A', expires_at = ...
WHERE locks.expires_at < NOW();
COMMIT;
Pros: ACID guaranteed. No extra infrastructure. Cons: Database becomes the bottleneck. Locks are slow (disk I/O). Connection pool pressure.
5. ZooKeeper / etcd (Heavyweight)
Create an ephemeral node. When the holder disconnects, the node auto-deletes (lock released).
ACQUIRE: create("/locks/order_123", ephemeral=true)
→ success: you have the lock
→ already exists: set a watch, wait for deletion
RELEASE: node auto-deletes when session ends (client disconnects/crashes)
Pros: Battle-tested. Auto-cleanup on crash. No TTL guessing. Cons: Heavy infrastructure (ZooKeeper cluster). Higher latency. Overkill for most use cases.
Comparison
| Method | Latency | Reliability | Complexity | Best For |
|---|---|---|---|---|
| Redis SETNX | ~1ms | Single Redis SPOF | Low | Most applications |
| Redlock | ~5ms | Tolerates minority failure | High | Critical locks needing HA |
| DynamoDB | ~10ms | Highly available (managed) | Medium | AWS/serverless apps |
| DB Row Lock | ~20-50ms | As reliable as your DB | Low | Small scale, already have DB |
| ZooKeeper | ~10-20ms | Very high (consensus) | High | Distributed coordination |
Common Problems and Solutions
Problem 1: Lock Holder Crashes (Deadlock)
Lock is acquired, holder crashes, lock never released.
Solution: TTL/expiry on every lock. After TTL, lock auto-releases.
Trade-off: TTL too short → lock expires while holder is still working (split-brain). TTL too long → long wait if holder crashes.
Best practice: TTL = 3x expected operation time. If operation takes 5s, set TTL = 15s.
Problem 2: Lock Expires While Still Working
Server A acquires lock (TTL=30s)
Server A's operation takes 35s (slow network/GC pause)
Lock expires at 30s → Server B acquires lock
Both A and B are now operating on the same resource! ← DANGEROUS
Solution: Fencing Tokens
Every lock acquisition returns an incrementing token number.
Server A gets lock with token=33
Server B gets lock with token=34 (after A's expires)
Storage checks: "reject writes with token < current max"
Server A tries to write with token=33 → REJECTED (current max is 34)
Server B writes with token=34 → ACCEPTED
Problem 3: Lock Ordering (Deadlock Between Two Locks)
Server A: acquires lock on Wallet_1, then tries lock on Wallet_2
Server B: acquires lock on Wallet_2, then tries lock on Wallet_1
→ Both wait forever (deadlock)
Solution: Always acquire locks in the same order (sorted by ID).
// Transfer from wallet A to wallet B
Wallet first = walletA.id < walletB.id ? walletA : walletB;
Wallet second = walletA.id < walletB.id ? walletB : walletA;
acquireLock(first.id); // Both threads try this first
acquireLock(second.id); // Then this
// No circular wait → no deadlock
Common Interview Questions
Q: “How do you prevent double-booking?” A: “Use a distributed lock on the resource (seat_id). Before booking, acquire lock with Redis SETNX + TTL. If acquired, proceed with booking. If not, return ‘already booked’. Release lock after commit.”
Q: “What if the lock holder crashes?” A: “TTL auto-releases the lock. The next server to try will acquire it. The crashed server’s partial work should be rolled back (use DB transactions) or designed to be idempotent.”
Q: “Redis SETNX vs database locking?” A: “Redis is faster (~1ms vs ~20ms) and doesn’t pressure your DB connection pool. Use Redis if you have it. Use DB locking only if you don’t want to add Redis infrastructure.”
Q: “How do you handle lock contention (too many waiting)?” A: “1) Exponential backoff on retry. 2) Timeout — if can’t acquire within 5s, fail fast. 3) Redesign to reduce lock scope (lock individual items, not entire collections).”
| ← Back to Fundamentals | ← Database Sharding | Next: Event Sourcing & CQRS → |
💬 Comments