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.