JD Interview: Redis Lock Expiration Mid‑Task – How Redisson’s Watchdog Auto‑Renews
The article explains what occurs when a Redis distributed lock expires before a business operation completes, and details how Redisson’s watch‑dog mechanism automatically renews the lock, covering the underlying principles, configuration, code examples, and comparisons with alternative renewal approaches.
Redis Distributed Lock Basics
Lock is represented by a Redis key. The value stores a unique client identifier (UUID). A TTL (expiration time) is set to avoid dead‑locks. If the business execution time exceeds the TTL (e.g., TTL 30 s, business 50 s), the lock expires while the task is still running, allowing other clients to acquire the same lock and causing concurrency errors.
Redisson Watchdog Auto‑Renewal
When the no‑argument lock() method is used, Redisson automatically starts a watchdog that renews the lock TTL every TTL/3. With the default TTL of 30 seconds the renewal interval is 10 seconds. The watchdog timeout can be changed via Config.setLockWatchdogTimeout.
Basic Usage
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency> Config config = new Config();
config.useClusterServers()
.setScanInterval(2000)
.addNodeAddress("redis://127.0.0.1:6379")
.addNodeAddress("redis://127.0.0.1:6380");
RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("order_lock");
lock.lock(); // auto‑renew, default 30 s TTL
// business logic …
lock.unlock(); // watchdog stopsWhen to Enable / Disable Watchdog
Enable when execution time is unpredictable (e.g., external API calls, complex DB queries) or strict consistency is required (flash‑sale, inventory deduction).
Disable when the operation duration is known and short; use lock(leaseTime, unit) to set a fixed TTL and avoid the watchdog.
Watchdog Working Principle
Start : After a successful lock(), Redisson creates an ExpirationEntry and stores it in a global ConcurrentHashMap<String, ExpirationEntry>. Then scheduleExpirationRenewal(threadId) launches a Netty TimerTask.
Renewal : The timer fires every TTL/3. It runs a Lua script that checks the lock owner (client UUID) and, if still held, resets the TTL to the original value. On success the method recursively schedules the next renewal.
Stop : Calling unlock() or client crash cancels the timer and removes the entry, allowing the Redis key to expire naturally.
Key Code Snippets
public class RedissonLock extends RedissonExpirable implements RLock {
private long lockWatchdogTimeout = 30000L; // default 30 s
@Override
public void lock() {
lock(-1, null, false); // leaseTime = -1 → start watchdog
}
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) {
// tryAcquire returns null when lock is obtained
Long ttl = tryAcquire(-1, leaseTime, unit, Thread.currentThread().getId());
// watchdog started later in tryAcquireAsync when leaseTime == -1
}
} protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry old = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (old != null) {
old.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
renewExpiration(); // start the periodic task
}
} private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) return;
Timeout task = commandExecutor.getConnectionManager().newTimeout(
timeout -> {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) return;
Long threadId = ent.getFirstThreadId();
if (threadId == null) return;
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock expiration", e);
return;
}
if (res) {
renewExpiration(); // schedule next renewal
} else {
cancelExpirationRenewal(threadId);
}
});
},
lockWatchdogTimeout / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
} protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return 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));
}Lua Script Optimization
Redisson sends the full Lua script only on the first execution; subsequent calls use EVALSHA with the cached SHA1 on the Redis server, reducing network overhead.
Netty TimerTask vs JDK Timer
Implemented with Netty’s HashedWheelTimer (O(1) scheduling, millisecond precision).
Non‑blocking, runs on Netty event‑loop threads, isolates exceptions, and supports massive concurrent timers.
JDK Timer is single‑threaded, blocking and a failure in one task can halt the entire timer thread.
Data Structures
EXPIRATION_RENEWAL_MAP : ConcurrentHashMap<String, ExpirationEntry> stores renewal state per lock key.
ExpirationEntry : Holds Map<Long, Integer> threadIds (thread ID → re‑entry count) and a Timeout reference to the scheduled Netty task.
public static class ExpirationEntry {
private final Map<Long, Integer> threadIds = new LinkedHashMap<>();
private volatile Timeout timeout;
// addThreadId, removeThreadId, hasNoThreads, getFirstThreadId …
}Design Patterns Used
Encapsulation : Complex renewal logic hidden inside RedissonLock, exposing only lock() and unlock().
Observer (Callback) : future.whenComplete() reacts to asynchronous renewal results.
Singleton : The RedissonClient is typically a single instance per application, sharing the same Netty event‑loop pool.
Defensive Design : 1/3 renewal interval, owner verification, automatic cleanup prevent dead‑locks.
Comparison with Alternative Renewal Strategies
Manual guard thread : Requires developer‑written thread management, higher risk of leaks and lower reliability.
Redis key‑space notifications : Pub/Sub fires after the key is already deleted, making renewal impossible; also suffers from possible message loss and adds load to the Redis server.
Redisson watchdog : Client‑side proactive renewal guarantees 100 % success as long as the client stays alive, distributes load across clients, and avoids the pitfalls of server‑side notifications.
Typical Usage Scenario
Business that may run longer than the default TTL, e.g., a 40 second order processing task with a 30 second lock:
RLock lock = redisson.getLock("order_lock_123");
lock.lock(); // watchdog renews every 10 s
try {
// call third‑party payment, DB updates, etc.
} finally {
lock.unlock(); // stop watchdog
}Configuration Example
Config config = new Config();
config.setLockWatchdogTimeout(30000L); // 30 s watchdog timeoutTech Freedom Circle
Crazy Maker Circle (Tech Freedom Architecture Circle): a community of tech enthusiasts, experts, and high‑performance fans. Many top‑level masters, architects, and hobbyists have achieved tech freedom; another wave of go‑getters are hustling hard toward tech freedom.
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.
