Mastering Redis Distributed Locks: Strategies, Pitfalls, and Redisson Watchdog

Redis distributed locks rely on a key-value marker with unique client IDs, expiration times, and careful release checks; the article explores lock timeout challenges, automatic renewal via watchdog mechanisms, Redisson's tryLock implementation, Lua‑based renewal scripts, and best practices to avoid deadlocks and resource waste.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Mastering Redis Distributed Locks: Strategies, Pitfalls, and Redisson Watchdog

Redis Distributed Lock Implementation

Use a specific key as lock marker stored in Redis, with a unique client identifier as value.

The key can be set only when it does not exist, ensuring mutual exclusion.

Set an expiration time to avoid deadlocks caused by abnormal termination.

When business logic finishes, delete the key after verifying the value so only the lock owner can release it.

Problem

If the lock expires after 30 seconds but the business runs longer (e.g., 40 seconds), the lock may be acquired by another client while the original task is still running.

Choosing an appropriate expiration time is difficult: too short increases the chance of premature expiration; too long prolongs the impact of a dead lock if the client crashes.

Typical approach is to set the timeout based on historical average execution time plus a buffer, but this is not easy.

Alternatively, omitting expiration can cause permanent deadlocks when a client crashes.

Automatic Renewal

One can set an initial lock timeout and start a watchdog thread that periodically extends the lock’s lease.

Implementation is non‑trivial because the watchdog must verify that the lock is still held by the same client and avoid unnecessary renewals or resource waste.

It must detect changes in lock ownership before renewing.

Renewal should occur at reasonable intervals, not continuously.

If the business completes, the watchdog should stop.

Watchdog (Redisson)

Redisson provides a watchdog mechanism that automatically renews the lock.

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 lock
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    if (ttl == null) {
        return true;
    }
    // adjust remaining wait time
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
        acquireFailed(threadId);
        return false;
    }
    current = System.currentTimeMillis();
    // subscribe to lock release events
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    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 {
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(threadId);
            return false;
        }
        while (true) {
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(leaseTime, unit, threadId);
            if (ttl == null) {
                return true;
            }
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(threadId);
                return false;
            }
            // wait for lock release signal
            if (ttl >= 0 && ttl < time) {
                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(threadId);
                return false;
            }
        }
    } finally {
        // always unsubscribe
        unsubscribe(subscribeFuture, threadId);
    }
    return get(tryLockAsync(waitTime, leaseTime, unit));
}

Key points:

Returning null indicates successful lock acquisition; a numeric value indicates the lock is held and provides the remaining TTL.

If acquisition fails, the client subscribes to Redis channel events for lock release and retries until the wait timeout expires.

The loop uses Redis Pub/Sub and a semaphore to block efficiently instead of busy‑waiting.

Watchdog Automatic Renewal

When a client successfully acquires a lock, Redisson starts a watchdog thread that periodically extends the lock’s lease.

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
        TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) return;
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

The watchdog is enabled only when leaseTime is set to -1, using a default 30‑second lock time.

If a custom lease time is provided, the lock will expire without automatic renewal.

Renewal Principle

The renewal uses a Lua script to reset the lock’s expiration to 30 seconds.

protected RFuture<Boolean> 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 stores active lock entries in RedissonLock.EXPIRATION_RENEWAL_MAP and every internalLockLeaseTime / 3 seconds checks whether the client still holds the lock; if so, it extends the key’s TTL. If the service crashes, the watchdog stops and the lock expires after the default period, allowing other clients to acquire it.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaconcurrencyredisdistributed-lockredissonWatchdog
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

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.