Distributed Locking
Coordinating exclusive access across processes
Key Takeaways
- ✓SET NX (set-if-not-exists) is the atomic primitive for lock acquisition — it guarantees only one client wins
- ✓Always set a TTL on locks so crashed clients don't hold locks forever (deadlock prevention)
- ✓Use a Lua script for release to make the ownership-check-and-delete atomic — two separate commands have a race condition
- ✓Store a unique owner ID as the lock value so only the holder can release it (safety property)
What is a Distributed Lock?
A distributed lock provides mutual exclusion across multiple processes or machines. When two instances of your service both try to process the same payment, send the same notification, or write to the same resource, a distributed lock ensures only one proceeds while the other waits or backs off.
Why It Matters
Without coordination, concurrent access to shared resources causes:
- Double processing: two workers handle the same job
- Data corruption: concurrent writes overwrite each other
- Duplicate side effects: sending the same email twice, charging a card twice
In a single-process application, you'd use a mutex or synchronized block. In a distributed system with multiple instances, you need a lock backed by shared state — typically Redis, ZooKeeper, or a database.
How Redis Locks Work
The simplest Redis lock uses three properties:
- Mutual exclusion:
SET key value NX— only succeeds if the key doesn't exist - Liveness (deadlock prevention):
SET key value PX ttl NX— key auto-expires after the TTL - Safety (ownership): Store a unique owner ID as the value, verify it before releasing
Acquire
SET lock:resource-123 owner-abc PX 5000 NXReturns OK if acquired, nil if already held.
Release (Lua script for atomicity)
if redis.call(class="text-pass">"get", KEYS[1]) == ARGV[1] then
return redis.call(class="text-pass">"del", KEYS[1])
else
return 0
endThe Lua script is critical — without it, there's a race condition between checking the owner and deleting the key.
Common Mistakes
- Using DEL without checking ownership: Another client might have acquired the lock between your GET and DEL, and you'd release *their* lock
- Forgetting the TTL: If a client crashes while holding the lock, no other client can ever acquire it — a permanent deadlock
- Using SETNX + EXPIRE as two commands: There's a gap between SETNX and EXPIRE where the client could crash, leaving a lock with no expiry. Use
SET key value PX ttl NXas a single atomic command - Assuming locks survive Redis restarts: By default, Redis keys are lost on restart. For critical locks, consider Redis persistence or a more robust system like ZooKeeper
Further Reading
The official Redis documentation on distributed lock patterns, including the Redlock algorithm.
A critical analysis of Redis-based distributed locks and when they are (and aren't) sufficient.
Salvatore Sanfilippo's response to Kleppmann, defending the Redlock algorithm's safety properties.