Redis Distributed Locks
A distributed lock lets only one worker run a critical task at a time, even when many servers compete. Redis builds locks with a single command and an automatic timer. Locks protect jobs such as sending an invoice, updating stock, or running a nightly report.
The Washroom Key Analogy
A small cafe has one washroom key hanging on a hook. A visitor takes the key, uses the room, and returns the key. Anyone who finds the hook empty must wait. A distributed lock works the same way. The key is a Redis entry, and the visitors are programs on different servers.
Worker A ──► takes lock "report:lock" ──► runs the job Worker B ──► finds lock taken ──► waits or skips Worker A ──► releases the lock Worker B ──► retries and takes the lock
Taking a Lock with SET NX PX
127.0.0.1:6379> SET report:lock "worker-A-7f3c" NX PX 30000 OK 127.0.0.1:6379> SET report:lock "worker-B-91d2" NX PX 30000 (nil)
- NX sets the key only when it does not exist.
- PX 30000 expires the key after 30,000 milliseconds.
- The value holds a unique token that identifies the owner.
Worker A receives OK and owns the lock. Worker B receives nil and knows the lock is busy.
Why the Timer Matters
Suppose Worker A crashes before it releases the lock. Without an expiry, the lock stays forever and every other worker waits indefinitely. The PX timer frees the lock automatically after 30 seconds.
Releasing a Lock Safely
A worker must delete the lock only when the lock still carries its own token. Consider a slow worker whose lock already expired. Another worker has since taken a new lock under the same key. A plain DEL would remove the new owner's lock.
Unsafe release: Worker A's lock expires at 30 s. Worker B takes the lock at 31 s. Worker A finishes at 32 s and calls DEL → removes B's lock! Safe release: Worker A checks the token first and deletes only if it matches.
The check and the delete must happen as one step. A short Lua script achieves this.
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
127.0.0.1:6379> EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 report:lock "worker-A-7f3c"
(integer) 1
Choosing the Lock Time
- Set the expiry longer than the normal job time.
- Keep the job short so the lock never expires mid-task.
- Renew the lock from a background thread for long jobs, and check the token before each renewal.
The Redlock Algorithm
A single Redis server is a single point of failure. When it fails over to a replica, the replica may not yet know about a fresh lock. The Redlock algorithm reduces this risk. A client asks five independent Redis servers for the lock and counts the lock as held only when a majority of three accepts it. Many client libraries include Redlock helpers.
Developers still debate how far any timing-based lock can protect data. For work where a double run causes real damage, add a second safeguard, such as a version number checked by the database.
Practical Example – One Invoice Run per Night
Three servers run the same nightly invoice job at 02:00. Each server calls: SET invoice:nightly:lock "server-id" NX PX 600000 Server 2 receives OK and runs the job. Servers 1 and 3 receive nil and skip the run. Server 2 releases the lock with the token-checking script. Result: Customers receive exactly one invoice.
Key Points
- SET with NX and PX creates a lock with an automatic expiry.
- A unique token in the value identifies the owner.
- Release the lock with a script that compares the token before deleting.
- Redlock spreads the lock across several Redis servers.
- Keep locked sections short and add database safeguards for critical data.
