How Redisson Implements Distributed Locks: Mechanism, Code, and Best Practices

This article explains Redisson's distributed lock mechanism, detailing the lock acquisition, renewal, and release processes, illustrating the design with a flash‑sale example, and providing complete Java code snippets that demonstrate lock handling, spin‑retry, subscription, and Lua script integration.

Ops Development Stories
Ops Development Stories
Ops Development Stories
How Redisson Implements Distributed Locks: Mechanism, Code, and Best Practices

Principle Description

Thread 1 acquires the lock and starts a background thread that renews the lock every 10 seconds.

When contention occurs, Thread 2 attempts to lock; if it fails, it spins and after a certain number of attempts blocks and subscribes to unlock messages.

When Thread 1 releases the lock, Redis publishes an unlock message; the observer wakes up waiting threads, allowing Thread 2 to compete again.

For re‑entrancy, Redisson stores the current thread's UUID (tid) in a Redis hash.

Test Code

Below is a flash‑sale example to demonstrate the lock:

Dependency Version

implementation 'org.redisson:redisson-spring-boot-starter:3.17.0'

Sample Code

public class RedissonTest {

    public static void main(String[] args) {
        // 1. Configuration
        Config config = new Config();
        String address = "redis://127.0.0.1:6379";
        SingleServerConfig serverConfig = config.useSingleServer();
        serverConfig.setAddress(address);
        serverConfig.setDatabase(0);
        config.setLockWatchdogTimeout(5000);
        Redisson redisson = (Redisson) Redisson.create(config);

        RLock rLock = redisson.getLock("goods:1000:1");
        // 2. Acquire lock
        rLock.lock();
        try {
            System.out.println("todo 逻辑处理 1000000.");
        } finally {
            if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
                // 3. Release lock
                rLock.unlock();
            }
        }
    }
}

Lock Design

The core lock call is rLock.lock();. Its call stack includes org.redisson.RedissonLock#tryLockInnerAsync, which executes a Lua script to perform the lock operation.

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
        "if (redis.call('exists', KEYS[1]) == 0) then " +
        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
        "return nil; " +
        "end; " +
        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
        "return nil; " +
        "end; " +
        "return redis.call('pttl', KEYS[1]);",
        Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

The lock renewal is performed in org.redisson.RedissonLock#tryAcquireAsync via scheduleExpirationRenewal. The renewal task runs in a delayed queue, avoiding the need to spawn a thread per lock.

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime != -1) {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
        if (ttlRemaining == null) {
            if (leaseTime != -1) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
}

The renewal logic creates a delayed task that re‑executes the Lua script to extend the lock's TTL. The interval is set to one‑third of the lease time to ensure the lock does not expire before the next renewal.

protected 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);
        try {
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

The actual renewal executes renewExpirationAsync, which runs another Lua script to reset the lock's expiration.

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), 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(getRawName()),
        internalLockLeaseTime, getLockName(threadId));
}

Spin‑Retry on Lock Acquisition

If org.redisson.RedissonLock#lock(long, TimeUnit, boolean) fails to acquire the lock, it enters a spin‑retry loop, periodically checking the remaining TTL and waiting on a latch until the lock becomes available.

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    if (ttl == null) return;
    CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
    RedissonLockEntry entry = interruptibly ? commandExecutor.getInterrupted(future) : commandExecutor.get(future);
    try {
        while (true) {
            ttl = tryAcquire(-1, leaseTime, unit, threadId);
            if (ttl == null) break;
            if (ttl >= 0) {
                try {
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    if (interruptibly) throw e;
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                }
            } else {
                if (interruptibly) entry.getLatch().acquire(); else entry.getLatch().acquireUninterruptibly();
            }
        }
    } finally {
        unsubscribe(entry, threadId);
    }
}

Subscription to Unlock Messages

When a lock is released, Redisson publishes an unlock message. Threads waiting for the lock subscribe to this channel; upon receiving the message, they are awakened to retry acquisition.

CompletableFuture future = subscribe(threadId);

Unlock Design

The core unlock call rLock.unlock(); triggers org.redisson.RedissonLock#unlockInnerAsync, which runs a Lua script to decrement the re‑entrancy counter, delete the lock if the counter reaches zero, and publish an unlock message.

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
        "return nil;" +
        "end; " +
        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
        "if (counter > 0) then " +
        "redis.call('pexpire', KEYS[1], ARGV[2]); " +
        "return 0; " +
        "else " +
        "redis.call('del', KEYS[1]); " +
        "redis.call('publish', KEYS[2], ARGV[1]); " +
        "return 1; " +
        "end; " +
        "return nil;",
        Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

After unlocking, org.redisson.pubsub.LockPubSub#onMessage wakes up blocked threads by releasing their latches and executing any queued listeners.

protected void onMessage(RedissonLockEntry value, Long message) {
    if (message.equals(UNLOCK_MESSAGE)) {
        Runnable runnable = value.getListeners().poll();
        if (runnable != null) runnable.run();
        value.getLatch().release();
    } else if (message.equals(READ_UNLOCK_MESSAGE)) {
        while (true) {
            Runnable runnable = value.getListeners().poll();
            if (runnable == null) break;
            runnable.run();
        }
        value.getLatch().release(value.getLatch().getQueueLength());
    }
}
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-lockredisson
Ops Development Stories
Written by

Ops Development Stories

Maintained by a like‑minded team, covering both operations and development. Topics span Linux ops, DevOps toolchain, Kubernetes containerization, monitoring, log collection, network security, and Python or Go development. Team members: Qiao Ke, wanger, Dong Ge, Su Xin, Hua Zai, Zheng Ge, Teacher Xia.

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.