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.

Tech Freedom Circle
Tech Freedom Circle
Tech Freedom Circle
JD Interview: Redis Lock Expiration Mid‑Task – How Redisson’s Watchdog Auto‑Renews

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 stops

When 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 timeout
backendJavaconcurrencyRedisDistributed LockredissonWatchdog
Tech Freedom Circle
Written by

Tech 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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.