Implementing Distributed Locks with Redis and Redisson
This article explains how to implement a distributed lock using Redis, discusses challenges with lock expiration, describes automatic renewal techniques such as watchdog mechanisms, and provides detailed Java code examples of Redisson's tryLock and renewal processes.
Redis Implementation of Distributed Lock
Use a specific key as the lock marker and store a unique client identifier as its value.
The key can be set only when it does not exist, ensuring that only one client obtains the lock at a time (mutual exclusion).
Set an expiration time to avoid deadlocks caused by abnormal termination.
After the business logic finishes, delete the key to release the lock, verifying the value so that only the lock owner can release it.
Problem
If the lock expiration is set to 30 seconds but the business operation takes 40 seconds, the lock will expire while the task is still running, allowing another client to acquire the lock.
We can try to set a reasonable expiration time that covers the expected execution duration, but determining the optimal lock time is difficult.
Setting the lock time too short increases the probability of premature expiration and lock failure.
Setting it too long means that if the service crashes and cannot release the lock, the stale lock will persist for a long period.
Usually the lock time is chosen based on historical average execution time plus a safety buffer, but this is still an empirical process.
Alternatively, we could omit the expiration and rely on explicit unlocking, but if the client crashes the lock becomes a deadlock.
Automatic Renewal
One approach is to set an initial lock time and start a watchdog thread that periodically extends the lock's lease.
Although it sounds simple, implementing it correctly is not trivial.
Just like releasing the lock, the watchdog must first verify that the lock is still held by the same client; otherwise it may unintentionally extend a lock owned by another client.
The watchdog should renew the lock only after a reasonable interval to avoid wasting resources.
If the business logic finishes, the watchdog thread must be terminated; otherwise it would keep running and waste resources.
Watchdog
Redisson provides a watchdog mechanism that implements automatic renewal.
Redisson tryLock
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
// 1. Try to acquire the lock
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
// If the time spent acquiring exceeds the maximum wait time, fail
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
current = System.currentTimeMillis();
/**
* 2. Subscribe to lock release events and block waiting for the lock to be released.
* This avoids wasteful busy‑waiting when the lock is held by another client.
*/
RFuture
subscribeFuture = subscribe(threadId);
// await blocks using a CountDownLatch; if timeout expires, cancel subscription and fail
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(threadId);
return false;
}
try {
// Update remaining wait time
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
/**
* 3. After receiving the release signal, keep trying to acquire the lock within the remaining wait time.
*/
while (true) {
long currentTime = System.currentTimeMillis();
// Try to acquire again
ttl = tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
return true;
}
// Reduce remaining wait time
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
/**
* 6. Block waiting for the lock using a semaphore.
*/
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
// Wait up to ttl milliseconds for a permit
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
// Wait up to the remaining wait time
getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
// Update remaining wait time after blocking
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
}
} finally {
// 7. Cancel the subscription regardless of success
unsubscribe(subscribeFuture, threadId);
}
return get(tryLockAsync(waitTime, leaseTime, unit));
}When tryAcquire returns null, the lock is successfully obtained; otherwise it returns the remaining TTL of an existing lock.
If client 2 fails to acquire the lock, it subscribes to the Redis channel for lock‑release events; if the wait exceeds the maximum, it returns false.
During the retry loop, each attempt first checks the lock and obtains its remaining TTL; if the lock is still held, the thread blocks on a semaphore until a release message arrives.
This design avoids a busy while‑true loop by leveraging Redis pub/sub and semaphore blocking, reducing wasted CPU cycles.
How the Watchdog Automatically Renews
When a client successfully acquires a lock, Redisson starts a watchdog thread.
private
RFuture
tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture
ttlRemainingFuture = tryLockInnerAsync(config.getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}The watchdog is activated only when leaseTime == -1 ; the default lock lease is 30 seconds.
If a custom lease time is set, the lock will expire after that period without automatic renewal.
Renewal Principle
The renewal works by executing a Lua script that resets the lock's expiration to 30 seconds.
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
renewExpiration();
}
}
protected RFuture
renewExpirationAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.
singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}The watchdog runs a background task that, every internalLockLeaseTime / 3 (approximately every 10 seconds for the default 30 s lease), checks whether the client still holds the lock. If it does, the task extends the key's TTL, keeping the lock alive.
If the service crashes, the watchdog thread disappears, the lock is no longer renewed, and after 30 seconds it expires, allowing other clients to acquire it.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.