Interview Prep Zoneby CuriouserLabs

Question 5 of 5 · staff level

When do you actually need a distributed lock — and what's wrong with the Redis implementation everyone copies?

🎤 Say this first

First ask why you want one, because the answer changes the requirements. For efficiency (don't do the same expensive job twice) a lock may fail occasionally with no harm — Redis SET key val NX PX ttl is fine. For correctness (never two writers) an occasional failure corrupts data, and a TTL-based lock over an asynchronously replicated store is not sufficient: Redis replication is async, so a failover can hand the same lock to two clients, and a GC pause can leave a holder writing after its lease expired. Redlock (locking a majority of independent Redis nodes) is contested precisely because it still depends on bounded clock drift and pause times. If you need correctness, use a consensus-backed lock (etcd/ZooKeeper) plus fencing tokens — or, far better, redesign so a single writer owns each partition and no lock is needed.

The full picture

NeedFailure costAcceptable implementation
Efficiency (dedupe work)wasted CPURedis SET NX PX — good enough
Correctness (single writer)data corruptionetcd/ZK lease + fencing token checked by the resource
Leader election for a serviceduplicate processingconsensus lease, with idempotent work as a backstop
Cross-service mutual exclusionvariesusually a design smell — reshape ownership instead
The three bugs in the lock people actually ship
// BUG 1: no owner check — anyone can delete anyone's lock.
if (redis.setnx(key, "1")) { work(); redis.del(key); }

// BUG 2: TTL added non-atomically — a crash between the two leaks the lock forever.
redis.setnx(key, token); redis.expire(key, 30);

// Better: atomic set with owner + TTL, and a compare-and-delete release.
boolean got = redis.set(key, token, SetArgs.Builder.nx().px(30_000)) != null;
// release must be a Lua script: delete ONLY if the value is still my token
//   if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) end

// BUG 3 — the one no client-side fix reaches: work() outran the 30s TTL.
// Someone else now holds the lock and you are both writing. Only a fencing
// token rejected by the STORAGE layer prevents the corruption.
  • Name the two failure sources separately: process pauses (GC, CPU starvation, VM migration) break the assumption that you still hold the lease; async replication breaks the assumption that the lock service agrees with itself. Redlock addresses neither fully — that's the substance of the Kleppmann/antirez debate, and knowing it's about pauses and clocks rather than 'Redis is bad' is the mark of having read it.
  • Lease renewal (a watchdog) is a mitigation, not a fix: Redisson's watchdog extends the TTL while you work, which shrinks the window but cannot close it — the pause can straddle the renewal too. Useful, honest to mention, never sufficient on its own.
  • The strongest answer is usually to eliminate the lock: partition the keyspace so each key has exactly one owner (Kafka consumer-group assignment, sharded actors, single-writer-per-aggregate). Locks are coordination; partitioning is avoided coordination, and avoided coordination scales.
  • Or make it a conditional write: UPDATE … WHERE version = ? (optimistic concurrency), a DynamoDB conditional put, an S3 pre-condition, or a unique index on an idempotency key. The database is already a consensus system with fencing built in — use it instead of inventing one.
  • If you keep the lock, monitor it: how often is it contended, how often does work exceed the TTL, how often does release fail? Those three metrics turn an invisible correctness risk into an operable one.

🔄 Likely follow-up questions

  • Walk through the exact interleaving where a GC pause causes two lock holders.
  • Why is SET NX PX with a random token strictly better than SETNX + EXPIRE?
  • How would you redesign a job scheduler so it needs no distributed lock at all?