Distributed locks and why naive SETNX is wrong

Redis Course · lesson 9 of 19 · 6 min read

A Redis lock is a lease that can expire while you still believe you hold it.

Open this lesson in the learning hub

Key points

  • The correct acquire is one command: SET lock:job token NX PX 30000, with a token unique to this holder.
  • SETNX followed by EXPIRE is two commands, and a crash in between leaves a lock that never expires.
  • Releasing with plain DEL is a bug: if your lease already expired you delete a lock somebody else now owns.
  • The safe release is a Lua compare-and-delete - delete the key only if its value still equals your token.
  • Every lock is a lease, so a GC pause or a slow disk longer than the TTL means two clients believe they hold it.
  • A fencing token is a number that only ever increases, checked by the resource so a stale holder gets rejected.

Example

# acquire: one command, unique token, always a TTL
SET lock:invoice:991 4f2a-9c NX PX 30000

# release: compare and delete, never a bare DEL
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('DEL', KEYS[1])
      end
      return 0" 1 lock:invoice:991 4f2a-9c

# fencing token: a number the resource can reject if it goes backwards
INCR fence:invoice:991   # -> 34, send it with every write

SET NX PX to acquire, Lua compare-and-delete to release, and a fencing token if correctness truly matters.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Redis Course course, and every lesson in it is listed on the Redis Course contents page.