Backend Development 30 min read

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.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Understanding Redisson Distributed Locks: Reentrancy, Fairness, and Watchdog Mechanism

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
end

The 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 0

This 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 ttl

When 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.

JavaRedisDistributed LockRedissonwatchdogFair LockReentrant
Sohu Tech Products
Written by

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.

0 followers
Reader feedback

How this landed with the community

login 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.