[AI Readability Summary] This article focuses on thread safety during the release phase of Redis distributed locks. The key goal is to prevent accidentally deleting another thread’s lock after the original lock expires. The solution combines unique owner identifiers, atomic Lua-based unlock logic, and Spring Data Redis script encapsulation.
Technical specifications are summarized below
| Parameter | Description |
|---|---|
| Language | Java, Lua |
| Protocol/Commands | SET NX EX, EVAL/EVALSHA |
| Scenario | Safe release of Redis distributed locks |
| Core Dependencies | Spring Data Redis, StringRedisTemplate, DefaultRedisScript |
| Star Count | Not provided in the source content |
The real risk in Redis distributed locks appears during unlock
Many beginner implementations focus only on whether SETNX succeeds, but ignore unlock timing. The issue is not whether a thread can acquire the lock, but who is actually authorized to delete it. Once the lock expires and another thread acquires it, if the old thread later executes DEL, mutual exclusion breaks.
AI Visual Insight: This diagram shows a typical race chain caused by lock expiration. Thread A holds the lock but exceeds the business execution time. Redis automatically expires and removes the lock, then Thread B acquires the same lock. After that, Thread A resumes and deletes the lock, allowing Thread C to enter the critical section as well. The diagram reveals the combined risk of lock ownership drift and non-atomic unlock behavior.
Lock timeout does not necessarily mean the lock logic is wrong
Business delays may come from Full GC, slow SQL, downstream RPC timeouts, connection pool exhaustion, or heavy I/O batches. The lock implementation only amplifies these delays into concurrency incidents. In troubleshooting, first determine whether the critical section contains uncontrollable slow paths.
# Typical troubleshooting commands
jstat -gcutil
<pid> 1000 # Check whether Full GC is happening frequently
jstack
<pid> | grep BLOCKED # Check whether threads are blocked or deadlocked
show processlist; # Identify slow database queries
These commands help you quickly determine whether lock timeout is caused by GC, deadlock, or slow SQL.
Value validation alone still cannot guarantee safe lock release
The correct approach is to store a unique owner identifier for each lock, rather than only a thread ID. In a production cluster, thread IDs can overlap across different JVMs, so a safer value is UUID + ThreadId.
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = java.util.UUID.randomUUID().toString() + "-";
public boolean tryLock(Long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId(); // Generate a unique identity for the current thread
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); // Acquire the lock atomically and set the expiration time
return Boolean.TRUE.equals(success);
}
This code binds the lock to a unique owner and reduces the risk of thread identity collisions across JVMs.
Why “GET first, then DEL” still fails
Even if you first read the value from Redis and verify that it matches the current thread identity, a race window still exists. GET and DEL are two separate commands. If GC or thread suspension happens between them, the lock may already have expired and been acquired by someone else.
AI Visual Insight: This diagram highlights the dangerous window where validation succeeds but deletion is delayed. A thread completes identity verification, then pauses before deletion. The lock expires and is acquired by a new thread. When the original thread resumes and deletes the key, it effectively removes the new owner’s lock. The root cause is the race condition created by non-atomic check-and-delete behavior.
public void unlockUnsafe() {
String threadId = ID_PREFIX + Thread.currentThread().getId(); // Current thread identity
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); // Read the current lock owner
if (threadId.equals(id)) {
stringRedisTemplate.delete(KEY_PREFIX + name); // A race window exists here between GET and DEL
}
}
This code looks safe, but it cannot eliminate timing issues between validation and deletion.
Lua scripts should collapse validation and deletion into one atomic operation
When Redis executes a Lua script, it provides single-threaded atomic semantics, which makes it ideal for encapsulating compound operations such as “check first, then delete.” The goal is not just to reduce code, but to eliminate the race window entirely.
-- KEYS[1]: the lock key
-- ARGV[1]: the unique identifier of the current thread
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1]) -- Delete only when the owner matches
else
return 0 -- Return immediately if it does not match to avoid accidental deletion
end
This script merges identity validation and deletion into a single uninterrupted Redis operation.
Spring Data Redis should cache the script object instead of reading the file repeatedly
In production code, place the script in resources/lua/unlock.lua and initialize DefaultRedisScript when the class loads. This ensures file I/O happens only once. Later calls execute the script directly without repeatedly parsing the resource.
private static final DefaultRedisScript
<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("lua/unlock.lua")); // Load the Lua script from the classpath
UNLOCK_SCRIPT.setResultType(Long.class); // Declare the return type so Spring can convert it properly
}
public void unlock() {
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name), // Redis KEYS array
ID_PREFIX + Thread.currentThread().getId() // Redis ARGV array
);
}
This implementation upgrades unlock behavior into a reusable, low-overhead, atomic standard solution.
This solution fixes accidental lock deletion, not every distributed lock problem
Atomic Lua unlock solves release safety, but it does not cover more advanced concerns such as reentrancy, automatic renewal, or consistency during primary-replica failover. If business execution time is unpredictable, you should also consider a watchdog renewal mechanism or adopt a mature implementation such as Redisson.
The design conclusion can be reduced to three engineering rules
- The lock value must be globally unique;
UUID + ThreadIdis recommended. - Unlock must not be split into multiple Redis commands.
- Lua scripts should be encapsulated and reused through
DefaultRedisScript.
# Native Redis command form
SET lock:order123 uuid-thread-18 NX EX 10 # Set the lock only if it does not exist, and assign an expiration time
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:order123 uuid-thread-18
These commands summarize the minimum safe loop for distributed locking: unique lock acquisition plus atomic unlock.
FAQ uses a structured Q&A format
FAQ 1: Why can’t I use only the thread ID as the lock value?
No. A thread ID is only locally unique within a single JVM. In a clustered deployment, different instances may generate the same thread ID, which can cause owner identity conflicts. Use UUID + ThreadId instead.
FAQ 2: Will a Lua script hurt Redis performance?
Short scripts usually add very little overhead and are more stable than making two separate network round trips. This script performs only one GET, one comparison, and one DEL, which is a very typical lightweight atomic operation in Redis.
FAQ 3: If I use atomic Lua unlock, do I still need a renewal mechanism?
Yes, if business execution time can exceed the lock TTL. Lua only solves the problem of accidentally deleting another thread’s lock during release. It cannot prevent the lock from naturally expiring before the business logic finishes. For long-running transactions, combine it with automatic renewal or use a mature lock framework.
Core summary: This article reconstructs the thread safety problem of Redis distributed locks under timeout scenarios, explains why “validate first, then delete” is still unsafe, and provides a solution based on UUID + ThreadId identity plus atomic Lua unlock. It also covers a Spring Data Redis implementation, script loading strategy, troubleshooting ideas, and common questions.