Backend Development 16 min read

Implementing Reentrant Distributed Locks with Redis: ThreadLocal and Redis Hash Solutions

This article explains the concept of reentrant distributed locks and provides two Java implementations—one using ThreadLocal for local counting and another using Redis Hash with Lua scripts—detailing the code, pitfalls, and integration with Spring Boot.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Implementing Reentrant Distributed Locks with Redis: ThreadLocal and Redis Hash Solutions

When integrating an external system into a new platform, developers often face the challenge of replicating functionality while ensuring backward compatibility and data migration, which can lead to complex and time‑consuming projects.

The article introduces the concept of reentrant locks, quoting the Wikipedia definition and explaining that a reentrant lock allows the same thread to acquire the lock multiple times without deadlock, unlike a non‑reentrant lock which requires the lock to be released before reacquisition.

Two implementation approaches are presented:

ThreadLocal Based Solution

Using Java's ThreadLocal , each thread maintains its own Map where the key is the lock name and the value is the reentry count. The global variable LOCKS is defined as:

private static ThreadLocal
> LOCKS = ThreadLocal.withInitial(HashMap::new);

The lock acquisition method increments the count if the lock is already held, otherwise it delegates to a Redis lock and sets the count to 1:

public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
Map
counts = LOCKS.get();
if (counts.containsKey(lockName)) {
counts.put(lockName, counts.get(lockName) + 1);
return true;
} else {
if (redisLock.tryLock(lockName, request, leaseTime, unit)) {
counts.put(lockName, 1);
return true;
}
}
return false;
}

The unlock method decrements the count and releases the Redis lock when the count reaches zero, throwing an IllegalMonitorStateException if an unlock is attempted by a thread that does not own the lock.

public void unlock(String lockName, String request) {
Map
counts = LOCKS.get();
if (counts.getOrDefault(lockName, 0) <= 1) {
counts.remove(lockName);
Boolean result = redisLock.unlock(lockName, request);
if (!result) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:" + lockName + " with request: " + request);
}
} else {
counts.put(lockName, counts.get(lockName) - 1);
}
}

While this approach is simple and efficient, it suffers from expiration‑time inconsistencies and cannot handle reentrancy across different threads or processes.

Redis Hash Based Solution

This method stores the reentry count in a Redis Hash, using Lua scripts to perform atomic operations. The lock acquisition Lua script checks existence, creates or increments the hash field, and sets an expiration:

---- 1 represents true
---- 0 represents false
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
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 1;
end;
return 0;

The Java method loads the script and executes it via StringRedisTemplate :

String lockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:lock.lua").openStream(), Charsets.UTF_8);
DefaultRedisScript
lockScript = new DefaultRedisScript<>(lockLuaScript, Boolean.class);
public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
long internalLockLeaseTime = unit.toMillis(leaseTime);
return stringRedisTemplate.execute(lockScript, Lists.newArrayList(lockName), String.valueOf(internalLockLeaseTime), request);
}

The unlock Lua script decrements the counter and deletes the hash when the count reaches zero:

if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
if (counter > 0) then
return 0;
else
redis.call('del', KEYS[1]);
return 1;
end;
return nil;

The corresponding Java unlock method handles the three possible return values (1 = success, 0 = decremented, null = other thread attempted unlock):

String unlockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:unlock.lua").openStream(), Charsets.UTF_8);
DefaultRedisScript
unlockScript = new DefaultRedisScript<>(unlockLuaScript, Long.class);
public void unlock(String lockName, String request) {
Long result = stringRedisTemplate.execute(unlockScript, Lists.newArrayList(lockName), request);
if (result == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:" + lockName + " with request: " + request);
}
}

Additional considerations include version compatibility of spring-data-redis (requiring ≥ 2.1.9 for cluster mode), handling of Lua‑to‑Redis type conversions (numbers, booleans, nil), and potential pitfalls when using raw Jedis connections.

Summary

The reentrant distributed lock relies on counting lock acquisitions; the ThreadLocal approach is straightforward but limited to a single JVM thread, while the Redis Hash approach works across processes at the cost of added complexity and Lua scripting. Both implementations integrate with Spring Boot and illustrate common issues such as lock expiration and type conversion.

JavaRedisSpring Bootdistributed lockthreadlocalLuaReentrant
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.