Java FencedLock and Fencing Tokens
A fencing token is a monotonically increasing number handed out each time a client acquires a distributed lock. The client passes the token to whatever resource the lock protects, and that resource records the highest token it has accepted and refuses any operation carrying a lower one. A lock holder that stalls, loses its lease, and wakes up still believing it is in charge is rejected by the resource itself — without the resource needing to know anything about locks, leases, or cluster membership.
It is the answer to a problem no lock service can solve on its own, and it is the mechanism behind the FencedLock data structure. In Java, Redisson implements it for Valkey and Redis through the RFencedLock interface.
Why the Lock Alone Is Not Enough
A distributed lock gives you mutual exclusion by granting a lease: you hold the lock for a bounded period, after which it expires so a crashed client cannot block everyone else forever. That lease is what makes the lock safe to operate. It is also what makes it unsafe to trust.
Consider the sequence:
- Client A acquires the lock on
order:1042with a 30-second lease. - Client A begins its work, then stalls — a long garbage-collection pause, a hypervisor stall, an unlucky network delay, a suspended container.
- Thirty seconds elapse. The lock expires. Nothing is wrong with the lock service; it did exactly what it promised.
- Client B acquires the lock cleanly and starts working.
- Client A resumes. From its own point of view no time has passed and it still holds the lock. It writes.
Both clients now write to the same resource, and the lock service is blameless. It cannot help: by the time A resumes, the only component in a position to notice that A is stale is the resource being written to. This is the argument Martin Kleppmann made in 2016, and it is why the Redis project's own guidance on distributed locks now states that you should implement fencing tokens, and that this applies to any distributed locking system.
The failure is not specific to Valkey or Redis, or to the Redlock algorithm the debate formed around. It applies to ZooKeeper, to etcd, to a lock table in PostgreSQL, and to any lock with a lease — which is every lock that tolerates a client crashing. A separate and narrower hazard, losing a lock during failover because replication is asynchronous, is covered in split-brain; Redisson addresses that one with replica-synchronization checking, which is on by default.
Fencing tokens close the pause problem specifically. Add one to the sequence above and step 5 fails: A carries token 41, the resource has already accepted a write from B carrying token 42, and A's write is refused.
How the Protected Resource Validates the Token
This is the half of the pattern most explanations leave out, and it is the half that does the work. Issuing a token is easy. The guarantee comes entirely from the resource's willingness to enforce it, and the enforcement has to be atomic — a read of the stored high-water mark followed by a separate write reintroduces exactly the race the lock was supposed to remove.
In practice that means a single conditional operation, expressed in whatever language the resource speaks.
Guarding a Relational Database
Store the highest accepted token alongside the row, and make the token comparison part of the WHERE clause so the check and the write are one statement:
ALTER TABLE inventory ADD COLUMN fence_token BIGINT NOT NULL DEFAULT 0;
UPDATE inventory
SET stock = ?,
fence_token = ?
WHERE product_id = ?
AND fence_token <= ?
The database evaluates the predicate and applies the update in one atomic step. A stale writer matches no rows, and the driver reports it:
RFencedLock lock = redisson.getFencedLock("inventory:42");
Long token = lock.tryLockAndGetToken(100, 30, TimeUnit.SECONDS);
if (token == null) {
throw new IllegalStateException("could not acquire lock for inventory:42");
}
try {
int updated = jdbcTemplate.update(
"UPDATE inventory SET stock = ?, fence_token = ? " +
"WHERE product_id = ? AND fence_token <= ?",
newStock, token, 42, token);
if (updated == 0) {
// another holder has already written with a higher token —
// this client was fenced out and must not retry blindly
throw new StaleTokenException("fenced out at token " + token);
}
} finally {
lock.unlock();
}
Note what is not here: no SELECT of the current token, no comparison in Java, no second round trip. The moment you split the check from the write, a paused client can pass the check and then write after a newer holder has already committed.
Guarding Data in Valkey or Redis
When the protected resource is itself in Valkey or Redis, the equivalent of a conditional UPDATE is a Lua script, which the server executes atomically:
private static final String FENCED_WRITE =
"local seen = redis.call('get', KEYS[1]); " +
"if seen and tonumber(seen) > tonumber(ARGV[1]) then " +
" return 0; " +
"end; " +
"redis.call('set', KEYS[1], ARGV[1]); " +
"redis.call('set', KEYS[2], ARGV[2]); " +
"return 1;";
RScript script = redisson.getScript(StringCodec.INSTANCE);
Long accepted = script.eval(RScript.Mode.READ_WRITE,
FENCED_WRITE, RScript.ReturnType.LONG,
Arrays.asList("{inventory:42}:fence", "{inventory:42}:stock"),
token, newStock);
if (accepted == 0) {
throw new StaleTokenException("fenced out at token " + token);
}
On Redisson 3.x the return-type constant is named RScript.ReturnType.INTEGER; it was renamed to LONG in 4.0.
Note the braces. They make inventory:42 the hash tag, so both keys land in the same slot on Redis Cluster, where a script may only touch keys that live on one node. On a single instance they are harmless.
Why the Comparison Is "Greater or Equal"
The resource rejects a token that is lower than the highest it has accepted, not one that is merely equal. This matters more than it looks.
A lock holder normally acquires the lock once and then performs several writes inside the critical section, all carrying the same token. If the resource demanded a strictly greater token, the holder's own second write would be refused. The rule that makes the pattern usable is: accept token >= highest_accepted, reject anything below it. That is why the SQL predicate above reads fence_token <= ? and the Lua guard rejects only when the stored value is strictly greater.
One consequence worth planning for: the stored high-water mark and the lock's counter have to have compatible lifetimes. Redisson keeps the counter in its own key, separate from the lock itself, and increments it with INCR. If that key is ever lost — flushed, evicted, or absent after a restore from a backup — the counter restarts from a value below what your resource has already recorded, and the resource will correctly but inconveniently reject every subsequent write. The failure is safe rather than silent, which is the right direction, but it is a state you want to be able to recognize and reset deliberately.
When the Resource Cannot Enforce a Token
Fencing only works if the thing you are protecting can perform the conditional check. Plenty of resources cannot: a third-party HTTP API with no conditional-write support, an SMTP server, a payment gateway, a legacy service you do not own. Passing a token to a resource that ignores it buys nothing, and it is worth being blunt about that rather than adding a parameter that looks like safety.
When enforcement is impossible, the alternatives are:
- Idempotency keys — make the operation safe to apply twice instead of trying to prevent the second attempt. This is the standard answer for external APIs, and the better one whenever it is available.
- Move the effect behind something you control — write the intent to your own database under a fencing token, and let a separate consumer perform the external call. The outbox pattern is this idea in its usual form.
- Accept the risk consciously — if the lock is protecting against duplicated work rather than corrupted data, an occasional double execution is a cost, not a bug. Kleppmann's distinction between locking for efficiency and locking for correctness is precisely this line, and a plain RLock is the right tool on the efficiency side.
RFencedLock in Java
Redisson is a Valkey and Redis client for Java that implements distributed objects and services as ordinary Java types. Its RFencedLock extends RLock, which in turn implements java.util.concurrent.locks.Lock — so the lock behaves like a familiar Java lock, with the difference that it spans every JVM connected to the same Valkey or Redis deployment. It is available from Redisson 3.19.0 onward.
Acquire the lock and read the token in one call:
RFencedLock lock = redisson.getFencedLock("myLock");
// block until acquired; the watchdog renews the lease while the JVM is alive
Long token = lock.lockAndGetToken();
try {
// pass the token to the protected resource, which must reject
// any operation carrying a lower token — see the section above
inventory.write(newStock, token);
} finally {
lock.unlock();
}
Or bound both the wait and the lease, and handle failure to acquire:
// wait up to 100 seconds for the lock, then hold it for at most 30
Long token = lock.tryLockAndGetToken(100, 30, TimeUnit.SECONDS);
if (token != null) {
try {
inventory.write(newStock, token);
} finally {
lock.unlock();
}
}
The full interface:
| Method | Returns | Behavior |
|---|---|---|
lockAndGetToken() | Long | Waits until the lock is available, then returns the increased token |
lockAndGetToken(leaseTime, unit) | Long | Intended to release the lock after leaseTime. Current releases do not apply the arguments — use tryLockAndGetToken(waitTime, leaseTime, unit) for a bounded lease |
tryLockAndGetToken() | Long | Returns the token, or null immediately if the lock is held |
tryLockAndGetToken(waitTime, unit) | Long | Waits up to waitTime; returns null if it cannot acquire |
tryLockAndGetToken(waitTime, leaseTime, unit) | Long | Bounded wait and bounded lease |
getToken() | Long | Reads the current token without acquiring the lock |
Because it extends RLock, everything on the ordinary lock is available too — isHeldByCurrentThread(), getHoldCount(), remainTimeToLive(), forceUnlock() — and the same rules apply: only the owner may unlock, or the call throws IllegalMonitorStateException.
Reentrancy and the token. RFencedLock is reentrant, so the same thread may acquire it more than once. Every acquisition increments the token, including a reentrant one, so nested acquisitions do not observe a stable value. Read the token once at the outermost acquisition and pass that value down, rather than calling lockAndGetToken() again in a nested method.
Asynchronous, reactive, and RxJava3 variants exist through RFencedLockAsync, RFencedLockReactive and RFencedLockRx:
RFencedLock lock = redisson.getFencedLock("myLock");
// capture the acquiring thread's id before going asynchronous: the callback
// runs on a Netty event-loop thread, so the no-argument unlockAsync() would
// try to release the lock as the wrong owner
long threadId = Thread.currentThread().getId();
lock.tryLockAndGetTokenAsync(100, 30, TimeUnit.SECONDS)
.whenComplete((token, exception) -> {
if (exception != null || token == null) {
return;
}
try {
inventory.write(newStock, token);
} finally {
lock.unlockAsync(threadId);
}
});
When Not to Use a Fenced Lock
Fencing has a real cost — a schema column or an extra key, a conditional write on every operation, and an error path in your application for the rejection. It earns that cost only in specific conditions:
| Situation | Use |
|---|---|
| The lock guards correctness and the resource can enforce the token | getFencedLock() |
| The lock coordinates your own application instances, and a rare duplicate is wasteful but harmless | getLock() with replica checking |
| The resource cannot check a token | Idempotency keys, or the outbox pattern |
| You need several clients in the section at once, up to a limit | RSemaphore |
| Waiting threads must be served in arrival order | getFairLock() |
| Conflicts are rare and you can retry the whole operation | Optimistic locking, which may remove the need for a lock at all |
As the guide to Redis locks puts it: fencing is only worth the effort when the resource you are guarding can enforce the token. If it cannot, a fenced lock is an ordinary lock with extra ceremony.
Redisson's older RedLock object, which implemented the Redlock algorithm across a quorum of independent masters, is deprecated and superseded by RLock with replica-synchronization checking and by RFencedLock where a token is needed.
Fencing Tokens: Frequently Asked Questions
What Is a Fencing Token?
A monotonically increasing number issued each time a lock is acquired. The client passes it to the protected resource, which records the highest token it has accepted and rejects any operation carrying a lower one. This blocks a stale lock holder that resumes after a pause, without the resource needing to know anything about locks or cluster membership.
Why Does a Distributed Lock Need a Fencing Token?
Because every distributed lock grants a lease, and a client can be suspended past its own lease expiry by a garbage-collection pause, a hypervisor stall, or network delay. It then resumes believing it still holds the lock. No lock service can prevent this, because the client has already been told it is the owner. Only the resource being written to is in a position to reject the stale writer.
How Does the Protected Resource Check the Token?
With a single atomic conditional operation. In a relational database, store the highest accepted token in a column and make the comparison part of the WHERE clause of the update, then treat zero affected rows as a rejection. In Valkey or Redis, use a Lua script that compares the incoming token against a stored value and refuses to write when it is lower. Reading the stored token and comparing it in application code reintroduces the race the lock was meant to remove.
Should the Resource Accept a Token Equal to the Last One?
Yes. A lock holder typically makes several writes inside one critical section, all carrying the same token, so the rule is to accept anything greater than or equal to the highest token accepted and reject only what is strictly lower.
What Version of Redisson Added RFencedLock?
Redisson 3.19.0. Earlier releases have RLock and its variants but no fenced lock, and the older RedLock object is now deprecated.
Is Redisson's RFencedLock the Same as Hazelcast's FencedLock?
The fencing mechanism is the same — each acquisition returns an increasing token that the protected resource validates. The lock service underneath differs: Hazelcast's runs on a Raft-backed CP subsystem, while Redisson's runs on Valkey or Redis, which replicate asynchronously. Because the safety guarantee comes from the resource's token check rather than from the lock service, the pattern behaves the same way in application code.
Does a Fencing Token Prevent Split-Brain?
Not on its own, and it is not meant to. Quorum and majority rules prevent two leaders from being elected; a fencing token handles the different case of a leader that was legitimately elected, then stalled, and resumed after its authority had passed to someone else. The two mechanisms are complementary — see split-brain for how they fit together.