Notes
The problem
A mutex in your code only works inside one process. Two servers share no memory, only a network. So both decide to run the job. You need a lock that lives outside both of them, in a service every server agrees on.
- a mutex works inside ONE
- process
- two servers share nothing
- but the network
- need: a lock that lives
- outside both, that every
- server agrees on
Acquire
The basic lock: create a key only if it doesn't exist. Worker one succeeds and runs the job; worker two finds it taken and waits or skips. When done, worker one deletes the key, but only if it still holds its own value, so it never deletes someone else's lock.
SET lock:billing w1
NX ← only if absent
PX 30000 ← expires in 30 s
w1: OK → runs the job
w2: nil → waits / skips
release: delete ONLY if the
value is still w1 Leases
If worker one crashes holding the lock, nobody can ever run the job again. So every distributed lock is a lease: it expires after, say, thirty seconds unless the holder renews it. For long jobs, the holder sends a heartbeat every ten seconds to extend it.
- what if w1 crashes while
- holding the lock?
- without expiry: locked forever
- lease: the lock expires
- after 30 s unless renewed
- long job? renew (heartbeat)
- every 10 s
The pause
Now the subtle part. Worker one holds the lock, then freezes for forty seconds: a garbage-collection pause, a paused VM. Its lease expires and worker two takes the lock. Worker one wakes up, still believing it holds the lock, and writes. Two writers. Expiry alone can't prevent this, because the paused process doesn't know time passed.
- w1 holds the lock
- w1 freezes 40 s
- (GC pause, VM paused, swap)
- lease expires → w2 acquires
- w1 wakes up, still thinks
- it holds the lock, writes
- two writers. Expiry alone
- cannot prevent this.
Fencing
The fix is a fencing token. Each time the lock is granted, the lock service returns a number that only goes up. Worker one got thirty-three, worker two got thirty-four. Every write carries its token, and the storage rejects any token lower than the highest it has seen. The zombie's write with thirty-three bounces.
each grant returns a number that only goes up w1 got token 33 w2 got token 34 storage remembers the highest token seen (34) and rejects any write carrying 33
Leader election
Leader election is the same idea, held for a long time: one node is the leader as long as it keeps renewing its lease. Use etcd, ZooKeeper or Consul for this. They run a consensus protocol internally, so the lock itself survives a server dying. Kubernetes controllers and Kafka elect their leaders exactly this way.
- same idea, long-lived:
- one node is "leader" while
- it keeps renewing its lease
- etcd / ZooKeeper / Consul:
- consensus inside (episode 34)
- so the lock itself survives
- node failures
- Kafka, Kubernetes, HBase
- all elect leaders this way
Redis locks
What about Redis? Ask why you need the lock. If it's for efficiency, avoiding duplicate work where an occasional double run is harmless, a single Redis key with NX and an expiry is fine. If it's for correctness, money or uniqueness, use a consensus-backed store and fencing tokens. Redlock, the multi-Redis algorithm, is famously debated for exactly this pause problem.
- single Redis SET NX PX:
- fine for efficiency locks
- (avoid duplicate work; an
- occasional double run is OK)
- for correctness locks
- (money, uniqueness):
- use a consensus store +
- fencing tokens
- Redlock is debated; know why
Alternatives
Often the best lock is no lock. A unique constraint in the database guarantees one booking per seat. A conditional update, 'update where version equals seven', does optimistic locking. Idempotent jobs from episode twenty-nine make double runs harmless. And a queue partition per key gives one owner per entity naturally.
- often a better design:
- unique constraint in the DB
- (one booking per seat)
- conditional write / version:
- UPDATE … WHERE version = 7
- idempotent jobs (episode 29)
- single partition owner
- (queue partition per key)
Say it like this
The interview answer. Take a lock in a consensus-backed store like etcd, with a lease that the holder renews. Pass a fencing token with every write, and have the storage reject older tokens, because pauses happen. Then mention the alternatives: unique constraints, conditional writes and idempotency, which often remove the need for a lock.
- "How do you make sure only
- one worker runs it?"
- lock in etcd / ZooKeeper,
- with a lease + renewals
- fencing token checked by
- the storage
- or avoid it: unique constraint,
- conditional write, idempotency