What Is a Redis Lock?
A Redis lock is a distributed lock that uses Valkey or Redis as the shared coordination point, so that only one process at a time may operate on a given resource — even when those processes run on different servers. The lock itself is just a key: whoever manages to create it holds the lock, and deleting the key releases it.
In-process locks cannot do this. A Java synchronized block or a ReentrantLock coordinates threads inside one JVM and knows nothing about a second instance of your application running elsewhere. Once you scale horizontally, two instances can both believe they are the only one processing an order, running a scheduled job, or charging a card. A distributed lock moves the arbiter out of the process and into a store every instance can reach, and Valkey or Redis is a common choice because it is fast, already present in most stacks, and executes its commands atomically.
How a Redis Lock Works
The lock lifecycle is three operations: acquire, hold the critical section, release. Acquisition relies on a single atomic command that creates the key only if it does not already exist, and attaches an expiration at the same moment:
SET lock:order:123 <random-token> NX PX 30000
Three parts carry the whole design. NX means set only if the key is absent, so exactly one client can win. PX 30000 sets a 30-second time to live, so a client that crashes cannot hold the lock forever. The random token identifies the owner, and it is what makes a safe release possible.
Doing this in two steps — the older SETNX command followed by a separate EXPIRE — is the classic mistake. If the client dies between the two commands, the lock exists with no expiry and blocks every other client permanently. Use the single SET ... NX PX form.
Release is the step most implementations get wrong. Deleting the key unconditionally can delete a lock that now belongs to someone else. The release must check ownership and delete in one atomic step, which requires a Lua script:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
The script deletes the key only when the stored token matches the one held by the caller. Because Valkey and Redis run a script to completion without interleaving other commands, the check and the delete cannot be separated.
Where Naive Redis Locks Go Wrong
The two snippets above are the entire happy path, which is why so many teams write their own. The difficulty is not acquiring the lock; it is everything that happens around the edges.
The lock expires while the work is still running. A TTL protects you against a dead client, but it is a guess about how long the job takes. Set it too short and the lock expires mid-operation, letting a second client in while the first is still writing. Set it too long and every crash blocks the resource for that duration. A fixed TTL forces you to choose which failure you prefer.
The wrong client releases the lock. Once a lock has expired out from under client A, a plain DEL from A will remove the lock that client B is now legitimately holding. This is the failure the Lua compare-and-delete above prevents, and it is routinely omitted.
A failover loses the lock entirely. Replication to a replica is asynchronous. A client can acquire a lock on the master and get an acknowledgement before that write has reached any replica. If the master then fails over, the promoted replica has no record of the lock, and a second client acquires it cleanly. Two clients now hold the same lock, and neither has done anything wrong.
A paused client resumes believing it still holds the lock. A long garbage-collection pause, a hypervisor stall, or an unlucky network delay can suspend a client past its own lock expiry. It wakes up and continues writing, unaware that its lease ended and another client took over. No lock service can prevent the pause; the resource being protected has to be able to reject the stale writer. That is what fencing tokens are for, and it is the crux of the long-running debate over the Redlock algorithm.
Redis Locks on Java With Redisson
Redisson implements Valkey and Redis based locks and synchronizers as ordinary Java objects, so the commands, the Lua release script, and the failure handling described above are already taken care of. Its RLock implements java.util.concurrent.locks.Lock, which means the API is the one Java developers already know — the difference is that it now spans every JVM in the cluster.
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// critical section — coordinated across the whole cluster
} finally {
lock.unlock();
}
The expiry problem is handled by a watchdog. Rather than making you guess a TTL, Redisson renews the lock in the background for as long as the owning instance is alive, so work that runs longer than expected keeps its lock, while a crashed client still loses it. The default watchdog timeout is 30 seconds and is configurable through Config.lockWatchdogTimeout. When you do want a hard ceiling, pass an explicit lease time and the lock releases itself:
RLock lock = redisson.getLock("myLock");
// tryLock(waitTime, leaseTime, unit) can throw InterruptedException,
// so the enclosing method must handle or declare it.
if (lock.tryLock(100, 10, TimeUnit.SECONDS)) {
try {
// waited up to 100 seconds; lock auto-releases after 10
} finally {
lock.unlock();
}
} else {
// could not acquire — skip the work, retry later, or fail fast
}
Ownership is enforced: only the thread that locked an RLock may unlock it, and any other thread attempting to do so gets an IllegalMonitorStateException. When a different thread or process genuinely needs to do the releasing, that is a job for a semaphore rather than a lock.
Choosing a Lock Type
Redisson offers several lock variants that share the same API and watchdog behavior, differing in ordering guarantees, fencing, and how waiting threads are notified.
| Lock type | Obtained with | What it adds |
|---|---|---|
| Lock | getLock() | Reentrant general-purpose mutual exclusion |
| Non-Reentrant Lock | getNonReentrantLock() | Rejects re-entry by the holding thread, surfacing accidental recursion |
| Fair Lock | getFairLock() | Grants the lock in request order (FIFO), preventing starvation |
| Non-Reentrant Fair Lock | getNonReentrantFairLock() | FIFO ordering without reentrancy |
| MultiLock | getMultiLock(...) | Treats several locks, possibly on different instances, as one |
| ReadWriteLock | getReadWriteLock() | Many concurrent readers, one exclusive writer |
| Spin Lock | getSpinLock() | Backoff polling instead of pub/sub, for very large numbers of locks |
| Fenced Lock | getFencedLock() | Issues a fencing token the protected resource can verify |
Fair locks queue waiting threads and wait five seconds for each thread that has died, so a run of failures adds up — five dead threads means a 25-second delay. Spin locks exist because the pub/sub channel that normally wakes waiting threads is distributed to every node in a cluster; when thousands of locks are acquired and released per second, that traffic becomes the bottleneck, and exponential backoff is cheaper.
Surviving Failover and Stale Holders
The two hardest failure modes have direct answers. For the replication gap, Redisson verifies after each acquisition that the lock actually reached the connected replicas. If they do not acknowledge in time, the lock is released and the acquisition fails, so a client never keeps a lock that a failover could erase. The check is controlled by checkLockSyncedSlaves, which is enabled by default, and slavesSyncTimeout, which defaults to 1000 milliseconds.
Config config = new Config();
config.setCheckLockSyncedSlaves(true) // default
.setSlavesSyncTimeout(1000); // milliseconds, default
For the paused-client problem, no amount of care inside the lock service is sufficient, because the danger is a client that has already been told it holds the lock. RFencedLock returns a monotonically increasing token on each acquisition. The protected resource records the highest token it has accepted and rejects anything lower, so a client resuming from a long pause with a stale token is fenced out:
RFencedLock lock = redisson.getFencedLock("myLock");
Long token = lock.lockAndGetToken();
try {
// pass the token to the protected resource, which must reject any write
// whose token is lower than the highest it has already accepted
storage.write(data, token);
} finally {
lock.unlock();
}
Fencing is only worth the effort when the resource you are guarding can enforce the token. For coordination between your own application instances, a plain RLock with the replica check is enough.
Redis Locks and Related Tools
A lock is not always the right instrument, and reaching for one by reflex is a common source of avoidable contention.
- Semaphores — when the requirement is at most N at once rather than exactly one, use a distributed semaphore. Permits are not tied to a thread, so one thread can acquire and another can release.
- Optimistic locking — when conflicts are rare, skip the lock, detect the conflict at write time with a version check, and retry. This avoids paying coordination cost on every operation.
- Idempotency — designing the operation so that running it twice is harmless often removes the need for a lock altogether, and is more robust than any locking scheme.
-
Redlock — an algorithm that acquires the lock on a quorum of independent masters. Redisson's
RedLockobject is deprecated and superseded byRLockwith replica checking, and byRFencedLockwhere a fencing token is needed.
Redis Lock: Frequently Asked Questions
How Do I Release a Redis Lock Safely?
Never delete the key unconditionally. Store a random token as the lock's value and release it with a Lua script that deletes the key only if the stored token matches yours. Otherwise you risk deleting a lock that expired and was legitimately re-acquired by another client.
What Happens If a Client Crashes While Holding a Redis Lock?
The lock's TTL expires and the key is removed automatically, so the resource is not blocked forever. This is why a lock must always be created with an expiry in the same atomic command that creates it. With Redisson, the watchdog stops renewing the lock once the owning instance stops responding, producing the same outcome without a hand-tuned TTL.
Is a Redis Lock Safe During a Failover?
Not automatically. Replication is asynchronous, so a lock acknowledged by the master may not have reached a replica before that replica is promoted, allowing a second client to acquire the same lock. Redisson closes this window by confirming replica synchronization after acquisition and failing the acquisition if it cannot be confirmed.
Is SETNX Enough to Build a Distributed Lock?
No. SETNX is deprecated in favor of SET with the NX option, and on its own it sets no expiry — a client that dies before issuing EXPIRE leaves a permanent lock. It also provides no ownership check on release. Use SET key token NX PX ttl together with a Lua compare-and-delete.
How Long Should a Redis Lock TTL Be?
Long enough to cover the worst realistic run time of the critical section, since expiring early admits a second client while the first is still working. Because that number is hard to predict, prefer a renewing lease — Redisson's watchdog extends the lock while the owner is alive — and use an explicit lease time only when you want a guaranteed upper bound.
Should I Use Redlock?
Usually not. Redlock requires a quorum of independent masters, which is significant operational cost for a deployment that is normally one master with replicas, and its safety guarantees are contested. Redisson deprecated its RedLock object in favor of RLock with replica synchronization checking, plus RFencedLock when the protected resource can validate a fencing token.