Mastering Redisson Distributed Locks: Deep Dive into Implementation, Reentrancy, and Fairness
This article provides a comprehensive guide to Redisson's distributed lock implementation, covering its architecture, comparison with Jedis and Lettuce, basic lock creation, Lua scripts for atomic operations, reentrant lock handling, watchdog-based lock renewal, RLock usage, and the design of fair locks using Redis data structures.
1. Redisson Overview
Redisson is an in‑memory data grid built on top of Redis, offering a variety of distributed Java objects and services to simplify business logic development.
It aims to separate concerns, allowing developers to focus on core business while handling most distributed problems.
Compared with Jedis and Lettuce, Redisson provides a higher‑level abstraction; Jedis and Lettuce are thin wrappers around Redis commands, while Redisson implements a complete distributed solution using Redis, Lua, and Netty.
2. Distributed Lock
How to implement a distributed lock?
A distributed lock is essential for concurrent business scenarios. Various implementations exist (ZooKeeper Znode, database row locks, Redis setNx), but they all ultimately rely on mutual exclusion.
Simple Redis distributed lock example
public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}
public void unlock(String lockName, String uuid) {
if (uuid.equals(redisTemplate.opsForValue().get(lockName))) {
redisTemplate.opsForValue().del(lockName);
}
}This 1.0 version works but the get and del operations are not atomic; under high concurrency it may lead to unsafe behavior. Therefore, a Lua script is introduced to make the lock release atomic.
What is a Lua script?
Lua is a lightweight scripting language built into Redis. It is executed via the eval / evalsha commands, ensuring the whole script runs atomically.
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
endUsing this script eliminates the lock‑release race condition.
Making the lock re‑entrant
Re‑entrancy means the same thread can acquire the same lock multiple times without deadlocking. Redisson stores lock name, thread ID, and a counter in a Redis hash to track re‑entrancy.
-- When the lock does not exist, create it with count 1 and set expiration
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
end
-- If the lock exists and the same thread ID is present, increment the counter
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])Unlocking decrements the counter; when it reaches zero the lock is deleted.
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
end3. Redisson Distributed Lock
Dependency
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>Configuration
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}") private String redisHost;
@Value("${spring.redis.password}") private String password;
private int port = 6379;
@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + port)
.setPassword(password);
config.setCodec(new JsonJacksonCodec());
return Redisson.create(config);
}
}Using the lock
@Resource
private RedissonClient redissonClient;
RLock rLock = redissonClient.getLock(lockName);
try {
boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS);
if (isLocked) {
// business logic
}
} catch (Exception e) {
rLock.unlock();
}4. RLock
RLock is the core interface of Redisson's distributed lock, extending java.util.concurrent.locks.Lock and providing asynchronous methods via RFuture.
How does RLock acquire a lock?
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1L) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
RFuture<Boolean> ttlFuture = tryLockInnerAsync(waitTime, commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlFuture.onComplete((ttl, e) -> {
if (e == null && ttl) {
scheduleExpirationRenewal(threadId);
}
});
return ttlFuture;
}
}If no lease time is set, Redisson starts a watchdog that periodically renews the lock expiration.
Lock renewal (watchdog)
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry old = (ExpirationEntry) EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (old != null) {
old.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
renewExpiration();
}
}
private void renewExpiration() {
ExpirationEntry ee = (ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee != null) {
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = (ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent != null) {
Long tid = ent.getFirstThreadId();
if (tid != null) {
RFuture<Boolean> f = renewExpirationAsync(tid);
f.onComplete((res, e) -> {
if (e == null && res) {
renewExpiration();
}
});
}
}
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}The watchdog runs every leaseTime/3 milliseconds, extending the lock's TTL as long as the owning thread is alive.
5. Fair Lock
Redisson also provides a fair lock based on Redis list and zset structures, ensuring FIFO ordering.
Fair lock acquisition Lua
-- 1. Clean expired queue entries
while true do
local first = redis.call('lindex', KEYS[2], 0)
if first == false then break end
local timeout = tonumber(redis.call('zscore', KEYS[3], first))
if timeout <= tonumber(ARGV[4]) then
redis.call('zrem', KEYS[3], first)
redis.call('lpop', KEYS[2])
else
break
end
end
-- 2. Try to acquire if lock does not exist and queue is empty or head is current thread
if redis.call('exists', KEYS[1]) == 0 and (redis.call('exists', KEYS[2]) == 0 or redis.call('lindex', KEYS[2], 0) == ARGV[2]) then
redis.call('lpop', KEYS[2])
redis.call('zrem', KEYS[3], ARGV[2])
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
end
-- 3. Re‑entrancy check
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
-- 4. Return remaining TTL for current thread
local timeout = redis.call('zscore', KEYS[3], ARGV[2])
if timeout ~= false then
return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4])
end
-- 5. Compute tail node TTL and enqueue
local ttl = redis.call('pttl', KEYS[1])
local last = redis.call('lindex', KEYS[2], -1)
if last ~= false and last ~= ARGV[2] then
ttl = tonumber(redis.call('zscore', KEYS[3], last)) - tonumber(ARGV[4])
end
local score = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4])
if redis.call('zadd', KEYS[3], score, ARGV[2]) == 1 then
redis.call('rpush', KEYS[2], ARGV[2])
end
return ttlThe script maintains a waiting list ( redisson_lock_queue:{...}) and a timeout sorted set ( redisson_lock_timeout:{...}). It cleans expired entries, handles first‑come‑first‑serve acquisition, supports re‑entrancy, and calculates TTL for each waiting thread.
6. Summary
Redisson implements distributed locking with sophisticated use of Redis data structures, Lua scripts, Netty asynchronous I/O, and Java concurrency primitives. It provides re‑entrant, fair, and multi‑node (MultiLock) locks, as well as automatic lock renewal via a watchdog. Understanding its internals offers valuable insights for building reliable distributed systems.
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.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
