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.
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());
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
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.
