Understanding Redisson Distributed Locks: Reentrancy, Fairness, and Watchdog Mechanism
This article explains how Redisson implements distributed locks on Redis, covering basic concepts, differences from Jedis and Lettuce, the Lua scripts for lock acquisition, reentrancy handling, automatic lease renewal via a watchdog, Pub/Sub based unlock notifications, and the design of a fair lock using Redis lists and sorted sets.
Redisson is a Java client built on Redis that provides a rich set of distributed objects and services, including a powerful distributed lock implementation.
Compared with Jedis and Lettuce, Redisson offers higher‑level abstractions such as reentrant locks, automatic lease renewal, and fair locking semantics.
1. Basic Distributed Lock
The lock is stored as a Redis hash where the key is the lock name and the field is a unique UUID:threadId value. The hash value stores the reentrancy count. A simple Lua script performs the following steps:
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])If the lock does not exist, it is created and an expiration is set; if the same thread already holds the lock, the reentrancy counter is incremented and the expiration is refreshed.
2. Unlock
Unlocking is also performed by a Lua script that decrements the counter and removes the lock when the count reaches zero, then publishes an unlock message via Redis Pub/Sub:
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
endThe LockPubSub listener receives the unlock message, runs any queued callbacks, and releases a semaphore that wakes waiting threads.
3. Watchdog (Lock Renewal)
When a lock is acquired without an explicit lease time, Redisson registers a watchdog task that periodically (leaseTime/3) executes the following Lua script to extend the expiration:
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('pexpire', KEYS[1], ARGV[1])
return 1
end
return 0This ensures that long‑running business logic does not lose the lock even if the original lease would have expired.
4. Fair Lock
RedissonFairLock uses a Redis list ( redisson_lock_queue:{name} ) to keep waiting thread IDs in FIFO order and a sorted set ( redisson_lock_timeout:{name} ) to store each thread’s timeout as the score. The acquisition Lua script performs six logical steps:
Clean up expired entries at the head of the queue.
If the lock is free and the current thread is at the head, remove it from the queue and acquire the lock.
Handle reentrancy exactly as the non‑fair lock.
Return the remaining TTL for the current thread.
Calculate the TTL of the tail thread to determine the waiting time for new threads.
Insert the current thread at the tail of the list and add/refresh its timeout in the sorted set.
The core Lua script (simplified) looks like this:
-- Clean expired head entries
while true do
local first = redis.call('lindex', KEYS[2], 0)
if not first then break end
local timeout = redis.call('zscore', KEYS[3], first)
if tonumber(timeout) <= tonumber(ARGV[4]) then
redis.call('zrem', KEYS[3], first)
redis.call('lpop', KEYS[2])
else
break
end
end
-- Try to acquire lock if free and head matches
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
-- Reentrancy
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 TTL and enqueue
local ttl = redis.call('pttl', KEYS[1])
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 ttlWhen the lock holder releases the lock, it publishes an unlock message; waiting threads receive the notification, remove themselves from the queue, and retry acquisition, preserving FIFO order.
5. Summary
Redisson combines Redis data structures, Lua scripting, Netty‑based asynchronous futures, and Pub/Sub to provide a feature‑rich distributed lock that supports reentrancy, automatic lease renewal, and fair queuing. The implementation is more complex than a naïve lock but offers robust semantics for production‑grade backend services.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
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.