How to Use Redis Locks in Java with Redisson
In programming terms, locks control access to shared resources, preventing multiple threads from reading or modifying data at the same time and corrupting it. Inside a single JVM, Java's Lock objects handle this. But the moment your application runs on more than one server — any horizontally scaled cluster — those in-process locks stop helping: each instance has its own, and none can see the others. Two nodes touching the same data at once is a distributed race condition, and it shows up as lost updates, double charges, or duplicated work.
Distributed locking solves this by putting the lock somewhere every node shares — Redis or Valkey — so only one node at a time enters a critical section. Redisson exposes this through objects that mirror java.util.concurrent, so distributed locking is no harder than the standard Java Lock. This guide covers each lock type, and — just as importantly — the behavior that determines whether your lock is actually safe: the watchdog, failover handling, and fencing.
About Java Lock Objects and Distributed Locking
A thread acquires the lock before touching the resource, does its work, and releases the lock so others can proceed. In a distributed system, that same discipline has to hold across servers: only one thread on any instance may hold the lock at a time. That coordination is what Redisson's lock objects provide on top of Redis or Valkey. For the underlying concept, see distributed locking in Java.
Lock
Redisson's RLock object implements a reentrant lock: the owning thread can lock the same resource more than once, and a counter tracks the depth, releasing the resource only when matching unlocks bring it back to zero. A basic lock:
RLock lock = redisson.getLock("myLock");
// traditional lock method
lock.lock();
// or acquire and automatically unlock after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
// or wait up to 100 seconds to acquire, then auto-unlock after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
// ...
} finally {
lock.unlock();
}
}
Always release in finally, and remember that only the thread that acquired the lock may unlock it — otherwise Redisson throws IllegalMonitorStateException. If you need to release from a different thread, use a semaphore instead.
The Watchdog and the leaseTime Trade-Off
Notice that the example above can be called two ways — with no lease, or with a lease time like 10, TimeUnit.SECONDS. The difference is the single most important thing to understand about Redisson locks, and it's easy to miss.
If the node holding a lock crashes, the lock must not stay held forever. Redisson prevents that with a watchdog: while the owning JVM is alive, it renews the lock's expiry in the background. The watchdog's lease defaults to 30 seconds (Config.lockWatchdogTimeout) and is renewed well before it elapses; if the JVM dies, renewals stop and the lock expires on its own.
The catch: the watchdog runs only when you do not pass a leaseTime. The moment you specify one, Redisson honors that exact duration and does not renew it.
// Watchdog ON — held until you unlock() or the JVM dies.
lock.lock();
// Watchdog OFF — released after exactly 10 seconds, finished or not.
lock.lock(10, TimeUnit.SECONDS);
So the trade-off is:
- No lease (watchdog): your work can take as long as it needs without the lock vanishing. The cost is that after a crash the lock lingers up to ~30 seconds before another node can take over.
- Explicit lease: the lock frees quickly after a crash, but if your work runs longer than the lease, the lock expires while you're still working and a second node can acquire it — recreating the race condition you were preventing.
A safe default is the no-argument lock() and let the watchdog manage expiry. Use an explicit leaseTime only when you can guarantee the work finishes within it — and where correctness is critical, fence the operation (see below) rather than trusting the lease to be long enough.
Fair Lock
A fair lock grants the lock in the order threads requested it — first in, first out — which avoids starvation when contention is high:
RLock lock = redisson.getFairLock("myLock");
lock.lock();
// or
lock.lock(10, TimeUnit.SECONDS);
// or
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
// ...
} finally {
lock.unlock();
}
}
MultiLock
MultiLock groups several RLock objects — which may live on different Redisson instances — and acquires them together as a single unit:
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RLock multiLock = anyRedisson.getMultiLock(lock1, lock2, lock3);
multiLock.lock();
try {
// ...
} finally {
multiLock.unlock();
}
MultiLock is for locking several distinct keys as one operation. It is not a replacement for the old RedLock object (which is deprecated); for the algorithm behind that, see the Redlock algorithm.
ReadWriteLock
A ReadWriteLock pairs a shared read lock (many concurrent holders) with an exclusive write lock (one holder), which is ideal when reads vastly outnumber writes:
RReadWriteLock rwlock = redisson.getReadWriteLock("myLock");
RLock lock = rwlock.readLock();
// or
RLock lock = rwlock.writeLock();
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
Spin Lock
When a huge number of locks are acquired and released in a short time, Redisson's usual pub/sub notification — one subscription per lock — can strain the server. A spin lock instead uses exponential-backoff polling:
RLock lock = redisson.getSpinLock("myLock");
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
Surviving a Redis Failover
Even with a healthy watchdog, one hazard remains. Acquiring an RLock writes to the Redis master, and because replication is asynchronous, that write may not have reached a replica yet. If the master then fails over to a replica that never received it, the new master has no record of the lock — and a second node can acquire it.
Redisson closes this window by default. After each acquisition it confirms the lock has propagated to the connected replicas; if they don't acknowledge within a timeout, it releases the lock and the acquisition fails, so you never keep a lock that wasn't safely replicated:
Config config = new Config();
config.setCheckLockSyncedSlaves(true) // default
.setSlavesSyncTimeout(1000); // milliseconds, default
This is the modern answer to the problem the Redlock algorithm was originally designed for, which is why a multi-node Redlock quorum is usually unnecessary — and why Redisson's old RedLock object is now deprecated.
Fenced Lock
Sometimes a client acquires a lock, then suffers a long pause — a stop-the-world GC, say — its lock expires, another client acquires it, and the first client wakes up still believing it holds the lock and writes. No lock can prevent that on its own; the fix, as Martin Kleppmann's well-known analysis of distributed locking describes, is a fencing token.
A Fenced Lock hands out a monotonically increasing token on each acquisition. You pass it to the resource you're protecting; the resource remembers the highest token it has seen and rejects any write carrying a lower one, fencing out the stale holder:
RFencedLock lock = redisson.getFencedLock("myLock");
Long token = lock.lockAndGetToken();
try {
// the resource must reject any write whose token is lower
// than the highest it has already accepted
storage.write(data, token);
} finally {
lock.unlock();
}
tryLockAndGetToken(...) returns null when the lock isn't acquired, and getToken() reads the current token without acquiring. Use a fenced lock whenever the lock guards writes to an external system that can enforce the token; for purely in-process coordination, a plain RLock is enough. (RFencedLock is available from the Redisson 3.23 series onward.)
Using Locks in Spring Boot
With the Redisson Spring Boot starter on the classpath, a RedissonClient is auto-configured and injected like any other bean:
@Service
public class OrderService {
private final RedissonClient redisson;
public OrderService(RedissonClient redisson) {
this.redisson = redisson;
}
public void process(long orderId) {
RLock lock = redisson.getLock("order:" + orderId);
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
}
}
Point the client at your Redis or Valkey deployment through the usual application.yaml configuration. (Confirm the current starter coordinates and config keys against the Redisson docs.)
A Correctness Checklist
-
Always release in
finally. A lock leaked on an exception path is a future outage. -
Know which mode you're in. No
leaseTimemeans the watchdog manages expiry; an explicitleaseTimemeans it doesn't, and your work must finish within it. - Keep the synced-slaves check on. It's the default; leave it unless you've measured a reason not to.
-
Fence correctness-critical writes with
RFencedLock, and have the resource enforce the token. -
Scope the lock tightly — lock
order:42, not a global key, so you don't serialize work that didn't need it. -
Don't reach for the deprecated
RedLock. A single master with replicas, the synced-slaves check, and fencing where needed cover what it was meant to.
Using Redisson to Manage Redis Locks
Redisson turns distributed locking into the familiar Java Lock pattern, but the safety lives in the defaults: acquire an RLock, let the watchdog manage expiry unless you have a reason to set a lease, keep the synced-slaves check enabled, and add a fencing token when correctness is on the line. Get those right and the race conditions that only appear under multi-node load stop happening.
To learn more, compare Redisson PRO vs. Community Edition. For the concepts behind this guide, see race condition, the Redlock algorithm, and Redis distributed lock.
Locks are also what make a repeated request safe to process once — see idempotency.