How to Build a Reentrant Distributed Lock with Redis, ThreadLocal, and Lua
This article explains why redoing a system can be simpler than refactoring, then dives into implementing a reentrant distributed lock using Redis with both ThreadLocal and Redis Hash approaches, providing detailed Java code, Lua scripts, and practical tips for handling lock expiration and multi‑process reentrancy.
Redo is always simpler than refactor
When integrating a third‑party system (the "old system") into a new internal platform (the "new system"), the old system's existing merchants must continue to use the same external interfaces, and all data must be fully migrated before the final cut‑over.
The project turned out to be far more complex than anticipated, with many hidden pitfalls.
After the migration, we need a distributed lock based on Redis. The basic lock works, but it lacks reentrancy, so this article shows how to add that feature.
Reentrancy
According to Wikipedia, a reentrant (or re‑entrant) routine can be interrupted at any point and safely called again by the operating system without causing errors. In multithreaded terms, a thread that already holds a lock can acquire it again without deadlock.
In Java, a reentrant lock can be illustrated as:
public synchronized void a() {
b();
}
public synchronized void b() {
// pass
}If the lock were non‑reentrant, the second acquisition would block even though the same thread already holds it.
Reentrancy solves this by counting lock acquisitions; the lock is released only when the count reaches zero.
Implementation approaches
ThreadLocal based solution
Redis Hash based solution
ThreadLocal based solution
Implementation
Java's ThreadLocal provides each thread with its own instance. We store a Map<String, Integer> in a ThreadLocal where the key is the lock name and the value is the reentrancy count.
private static ThreadLocal<Map<String, Integer>> LOCKS = ThreadLocal.withInitial(HashMap::new);Lock acquisition checks the map; if the lock already exists, the count is incremented. Otherwise, it tries to acquire the Redis lock and sets the count to 1.
public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
Map<String, Integer> 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;
}Unlocking decrements the count; when it reaches zero the entry is removed and the Redis lock is released.
public void unlock(String lockName, String request) {
Map<String, Integer> 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);
}
}Remember to clear the ThreadLocal after use to avoid memory leaks.
Redis Hash based solution
Implementation
We store the reentrancy count in a Redis hash, using the lock name as the hash key and a unique request identifier (e.g., UUID) as the field. Lua scripts atomically handle increment, expiration, and unlocking.
Lock acquisition Lua script:
-- 1 = true, 0 = 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;Java method that loads the script and executes it via StringRedisTemplate:
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);
}Unlocking Lua script:
-- if hash field does not exist, return nil
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;Java unlock method handling the three possible return values (1 = success, 0 = count decremented, null = other thread attempted unlock):
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);
}
}If using older Spring‑Data‑Redis versions, the Lua scripts may not be supported in cluster mode, requiring either an upgrade or direct Jedis calls.
Related issues
Lock expiration: the ThreadLocal approach may keep a stale count if the Redis lock expires.
Cross‑thread/process reentrancy: ThreadLocal works only for the same thread; Redis Hash solves the multi‑process case.
Spring‑Data‑Redis version compatibility and data‑type conversion pitfalls (Lua number → Redis integer, Lua boolean → 1/null, Lua nil → null).
Summary
The key to a reentrant distributed lock is counting lock acquisitions. The ThreadLocal solution is simple and efficient but struggles with lock expiration and cross‑process scenarios. The Redis Hash solution handles those cases at the cost of added complexity, requiring Lua scripting and careful handling of Spring‑Data‑Redis quirks.
References
https://www.sofastack.tech/blog/sofa-jraft-rheakv-distributedlock/
https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html
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 Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
