Distributed lock simulator

Two clients use a lock so that only one of them writes to a shared store at a time. The lock is a lease: it expires if the holder goes quiet. Pause a client, delay its write or jump a clock, and see whether the store ends up with a write it should have refused.

Stale write accepted
No
Both held the lock
0.0 s
Lock service
What goes wrong
Lease
Pause
Storage checks tokens
Timeline
Client A Client B Lock service Storage 0s 1s 2s 3s 4s 5s 6s 7s 8s

holds a valid lease paused believes it holds the lock, lease expired lease as the lock service sees it

Safe, thanks to fencing. A's lease ran out, B took the lock with a higher token, and the storage rejected A's late write. The value left is "from B".

Run
Fencing token
the revision of the lock key
Storage checks it
yes
Writes accepted
1 of 2
Value in storage
"from B"
Events
  1. 0.0 sLock grants the lock to A, lease 5000 ms, token 33.
  2. 0.2 sA stops for 8000 ms (a garbage-collection pause): it cannot run, renew its lease or notice anything.
  3. 5.0 sLock A's lease expires; the lock is free.
  4. 5.1 sLock grants the lock to B, lease 5000 ms, token 34.
  5. 5.3 sStorage accepts B's write "from B" (token 34, highest seen 34).
  6. 8.2 sA wakes up, still believes it holds the lock, and writes.
  7. 8.2 sStorage rejects A's write: token 33 is lower than 34, already used by a newer holder.

How to use the simulator

Client A takes the lock first and client B waits for it. Both want to write to the storage service. The timeline shows when each client holds a valid lease, when A is paused, and when A still believes it holds the lock after the lease has expired; arrows to the storage lane are writes, and the log lists every step.

  • Start with A pauses and fencing off. A stops for longer than its lease, B takes the lock and writes, then A wakes up and overwrites B's data.
  • Turn fencing on. The storage now rejects A's write because its token is lower than B's.
  • Pick a pause shorter than the lease. A wakes up in time, and nothing goes wrong with or without fencing.
  • Choose Redlock. Fencing is unavailable, because its lock value is random. Then try Clock jump, where one Redis node's clock runs ahead and two clients get a majority at once.

What is a distributed lock?

A distributed lock is a mutual exclusion lock shared by processes on different machines: before acting on a shared resource, such as a file, a database row or an external API, a process asks a lock service for the lock, and only one process is supposed to hold it at a time. Unlike a mutex inside one program, the holder can crash, be cut off by the network or stop for seconds, and the lock service cannot tell which.

That is why distributed locks are almost always leases, and why a lease on its own does not guarantee that only one process acts at a time.

Leases

A lease is a lock that expires after a fixed time unless its holder renews it. If the holder crashes, the lock frees itself and others can make progress. Cary Gray and David Cheriton introduced leases in 1989 for keeping file caches consistent. In etcd a lock is a key attached to a lease that the client keeps alive; in ZooKeeper it is an ephemeral znode that disappears when the client's session times out; in Redis it is a key set with SET key value NX PX ttl.

The catch is that expiry is measured by the lock service's clock, while the holder decides whether it still holds the lock by its own. The etcd documentation is blunt about it: the lease mechanism by itself does not guarantee mutual exclusion, because the server can revoke a lease while the client still believes it owns it.

The client that does not know it lost the lock

Martin Kleppmann's 2016 essay "How to do distributed locking" gives the standard example. Client 1 takes the lock and then stops, perhaps for a long garbage-collection pause. Its lease expires; client 2 takes the lock and writes. Client 1 resumes, still believing it holds the lock, and writes too, overwriting client 2's work. Other causes have the same effect: the process is swapped out, the virtual machine is suspended, or the write request sits in a network queue until after the lease has expired.

Checking the lease just before writing does not fix it, because the pause can come between the check and the write. Here, with a 5 s lease and an 8 s pause, etcd without fencing ends with "from A" in storage, the stale write. With a 3 s pause the lease is still valid when A wakes, and nothing goes wrong ("from B" is last, as it should be).

The table runs every service and scenario with a 5 s lease, an 8 s pause or delay, and the storage checking tokens wherever the service provides them:

ServiceNormalA pausesA’s write is delayedClock jump
etcdsafesafe: late write rejectedsafe: late write rejecteddoes not apply
ZooKeepersafesafe: late write rejectedsafe: late write rejecteddoes not apply
Redlocksafeunsafe: "from A" keptunsafe: "from A" keptunsafe: "from A" kept

Fencing tokens

The fix Kleppmann describes moves the check to the resource. Each time the lock service grants the lock it hands out a number that only ever increases, a fencing token. The client sends the token with every write, and the storage remembers the highest token it has accepted and rejects anything lower. In the simulation, A gets token 33, B gets 34 after A's lease expires, and when A's write with 33 finally arrives the storage has already seen 34 and refuses it.

The token has to come from something with a single ordered history:

  • etcd gives every change a revision number from one counter. The etcd documentation describes the revision of the lock key as the fencing token, and conditional transactions on revisions as the way etcd enforces exclusion for its own keys.
  • ZooKeeper's lock recipe creates an ephemeral sequential znode under the lock node; the client whose node has the lowest sequence number holds the lock. The znode's creation zxid, or its sequence number, works as a token.
  • Chubby, Google's lock service, has sequencers: a byte string describing the lock and its generation number, which servers can check before acting.

Fencing needs the resource's cooperation: a storage system that can compare tokens atomically with the write. Where it cannot, a conditional write (compare-and-set on a version) gives a similar guarantee without a lock at all.

Redlock

A lock on a single Redis server is lost if that server fails. Redlock, described in the Redis documentation, uses N independent Redis masters (five in the example): the client sets the same key with the same random value on each, and holds the lock if it succeeded on a majority within less than the lock's validity time. The remaining validity is the TTL minus the time spent acquiring.

Kleppmann argued that this is unsafe for correctness for two reasons. It has no fencing token, since the value is random and the independent nodes share no counter. And its safety depends on timing: if one node's clock jumps forward, it can expire a key early, and a second client can then win a majority that includes that node while the first still holds the others. The simulator's Clock jump scenario plays this out.

Salvatore Sanfilippo, Redis's author, replied in "Is Redlock safe?": clock jumps can be avoided by not setting clocks by hand and by having NTP slew the clock gradually rather than step it; the random value can be used with check-and-set on the resource; and the client checks the elapsed time after acquiring the lock. He agreed that a pause after that check leaves the same problem every lease-based lock has. The Redis documentation now links both essays, recommends fencing tokens, and notes that Redis does not use a monotonic clock for key expiry, so a wall-clock change can let more than one process acquire the lock.

Efficiency or correctness

Kleppmann's main advice is to ask what the lock is for.

  • Efficiency: the lock stops two workers doing the same job, such as sending the same email or recomputing the same report. An occasional duplicate costs a little. A single Redis instance with a TTL is simple and adequate.
  • Correctness: two holders at once would corrupt data or break an invariant. Use a lock from a consensus system such as etcd or ZooKeeper, and have the resource check fencing tokens, or design the write to be conditional or idempotent so that a stale holder can do no harm.

History

Gray and Cheriton described leases in 1989. Mike Burrows's paper on Chubby, Google's lock service built on Paxos, appeared at OSDI in 2006 and introduced sequencers. ZooKeeper, from Yahoo, was presented at USENIX ATC in 2010 by Patrick Hunt, Mahadev Konar, Flavio Junqueira and Benjamin Reed, with locks as a recipe built from its primitives rather than a built-in operation. CoreOS released etcd, based on Raft, in 2013. In February 2016 Kleppmann published his critique of Redlock and Sanfilippo his reply, and Flavio Junqueira followed with a note on how fencing applies to ZooKeeper.

Distributed lock questions and answers

What is a fencing token?

A number that the lock service increases every time it grants the lock, and that the client sends with every write. The storage remembers the highest token it has seen and rejects writes with a lower one, so a client that lost its lock without noticing cannot overwrite the work of the next holder. In this page's example, A gets token 33, B later gets 34, and the storage rejects A's late write.

Why is a distributed lock with a timeout not safe on its own?

Because the lock holder cannot know for certain that its lease is still valid at the moment its write reaches the storage. A garbage-collection pause, a swapped-out process or a delayed network packet can hold it up past the expiry, after which another client may already hold the lock. Checking the time just before writing does not help, since the pause can come right after the check.

Is Redlock safe?

It is debated. Martin Kleppmann argued in 2016 that Redlock depends on bounded clock drift, network delay and process pauses, and that a clock jump on one Redis node can let two clients hold the lock at once; Redlock also has no fencing token. Salvatore Sanfilippo, Redis’s author, replied that clocks can be kept from jumping and that the random lock value can be used with check-and-set. The Redis documentation now recommends fencing tokens and notes that Redis does not use a monotonic clock for expiry.

How do etcd and ZooKeeper provide fencing tokens?

Both are consensus systems with a single, totally ordered history. In etcd every change gets a revision number, and the etcd documentation describes the revision of the lock key as the fencing token. In ZooKeeper the lock is an ephemeral sequential znode, and its creation zxid or sequence number serves the same purpose.

When is a Redis lock good enough?

When the lock is for efficiency rather than correctness: to stop two workers doing the same expensive job, where a rare duplicate only wastes some work. If two holders at once would corrupt data, use a lock from a consensus system and fencing tokens checked by the storage, or make the operation itself safe with conditional writes.

What is a lease?

A lock or right that expires after a fixed time unless renewed. Leases were introduced by Cary Gray and David Cheriton in 1989 for cache consistency. They let a lock service recover from a client that crashes while holding the lock, at the price that a client that is merely slow loses the lock too.

Further reading

Published Updated