How Redisson Implements Distributed Locks: From Basic Concepts to Fair Lock Mechanics
This article explains Redisson's in‑memory data grid features, compares it with Jedis and Lettuce, and walks through the implementation of re‑entrant, watchdog‑enabled, and fair distributed locks using Redis Lua scripts and Java code examples.
Redisson Overview
Redisson is a Java in‑memory data grid built on Redis that provides distributed objects (BitSet, Set, Map, List, Queue, etc.) and services (Lock, Semaphore, Bloom filter, Remote service, Scheduler, etc.) to simplify Redis usage.
Compared with Jedis (a direct Redis command wrapper) and Lettuce (a Netty‑based client supporting clustering), Redisson adds higher‑level abstractions such as distributed locks.
Distributed Lock Basics
How to implement a simple Redis distributed lock
Using Spring Data Redis, a lock can be acquired with setIfAbsent (SETNX + EXPIRE) and released by checking the lock value before deletion.
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 is not atomic for get and delete, so a Lua script is introduced.
Lua script for unlocking
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
endThe script is executed via DefaultRedisScript in Spring.
Re‑entrant Lock Implementation
To support re‑entrancy, a Redis hash stores threadId → count. The lock acquisition Lua checks whether the hash exists; if not, it creates it with count 1 and sets an expiration. If the thread already holds the lock, the count is incremented.
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 (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 hash 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;
endWatchdog (Lock Renewal)
If no lease time is set, Redisson starts a watchdog that periodically (≈ leaseTime/3) renews the lock expiration using another Lua script, ensuring the lock stays alive while the owning thread is active.
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end;
return 0;Fair Lock
RedissonFairLock guarantees FIFO acquisition using a Redis list (waiting queue) and a sorted set (timeouts). The Lua script performs six steps:
Clean expired entries from the queue.
If the lock does not exist and the queue is empty or the head equals the current thread, acquire the lock.
Handle re‑entrancy.
Return the remaining TTL for the current thread.
Compute the tail node's TTL.
Append the current thread to the queue with a score of ttl + waitTime + now.
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;
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;
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;
local ttl = redis.call('zscore', KEYS[3], ARGV[2]);
if ttl ~= false then
return ttl - tonumber(ARGV[3]) - tonumber(ARGV[4]);
end;
local last = redis.call('lindex', KEYS[2], -1);
local ttl;
if last ~= false and last ~= ARGV[2] then
ttl = tonumber(redis.call('zscore', KEYS[3], last)) - tonumber(ARGV[4]);
else
ttl = redis.call('pttl', KEYS[1]);
end;
local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);
if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then
redis.call('rpush', KEYS[2], ARGV[2]);
end;
return ttl;This mechanism ensures that threads acquire the lock in the order they arrived, similar to Java's AQS fair lock.
Conclusion
Redisson provides a rich set of distributed synchronization primitives built on Redis, including re‑entrant locks, watchdog‑based lease renewal, and FIFO fair locks. Understanding the underlying Lua scripts and Redis data structures (hashes, lists, sorted sets) helps developers appreciate how Redisson achieves correctness and performance in single‑node and multi‑node environments.
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.
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.
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.
