The full picture
| Need | Failure cost | Acceptable implementation |
|---|---|---|
| Efficiency (dedupe work) | wasted CPU | Redis SET NX PX — good enough |
| Correctness (single writer) | data corruption | etcd/ZK lease + fencing token checked by the resource |
| Leader election for a service | duplicate processing | consensus lease, with idempotent work as a backstop |
| Cross-service mutual exclusion | varies | usually a design smell — reshape ownership instead |
// 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.