Implementing Distributed Locks with Redis: Principles, Challenges, and Optimizations
This article explains why traditional local locks fail in distributed systems, surveys common distributed‑lock approaches, and provides a step‑by‑step guide to building a robust Redis‑based lock in Java—including expiration handling, UUID safety, Lua‑script atomicity, re‑entrancy, automatic renewal, and the RedLock algorithm—while comparing its performance against plain local locks.
Introduction
When a monolithic application evolves into a distributed cluster, local JVM locks become ineffective because multiple processes run on different machines. A cross‑JVM mutual‑exclusion mechanism, i.e., a distributed lock, is required to protect shared resources.
Common Distributed‑Lock Implementations
Database‑based locks
Cache‑based locks (Redis, Memcached) – this article focuses on Redis
Zookeeper‑based locks
Each solution has trade‑offs in performance, safety, and implementation difficulty (Redis > Zookeeper > MySQL for speed; Zookeeper > Redis ≈ MySQL for safety).
Key Characteristics of a Distributed Lock
Exclusive mutual exclusion (e.g., SETNX or SET key value EX 3 NX )
Dead‑lock prevention via expiration
Atomicity of lock acquisition and release (Lua scripts)
Protection against accidental deletion (store a UUID per lock)
Automatic renewal
Re‑entrancy support (hash + Lua)
Cluster‑wide safety (RedLock algorithm)
Basic Redis Lock Using SETNX
The simplest lock uses SETNX to create a key only if it does not exist. Multiple clients compete; only one succeeds.
public void testLock() {
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock) {
// business logic
this.redisTemplate.delete("lock");
} else {
// retry after 1s
Thread.sleep(1000);
testLock();
}
}This approach fails if the client crashes after acquiring the lock because the lock is never released.
Adding Expiration
Set an expiration atomically with SET key value EX 3 NX to avoid dead locks, but if the business logic runs longer than the TTL the lock may be released prematurely.
public void testLock() {
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111", 3, TimeUnit.MINUTES);
if (lock) {
// business logic
} else {
// retry
}
}Preventing Wrong Deletion with UUID
Store a unique UUID as the lock value; before deleting, compare the stored value with the UUID to ensure the lock belongs to the current client.
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.MINUTES);
if (lock) {
// business logic
if (StringUtils.equals(redisTemplate.opsForValue().get("lock"), uuid)) {
this.redisTemplate.delete("lock");
}
}Atomic Release with Lua
Lua scripts guarantee that the check‑and‑delete operation is atomic.
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), uuid);Re‑entrant Lock Using Redis Hash
Store the lock owner UUID and a counter in a hash. Increment the counter on each re‑entry and decrement on unlock; when the counter reaches zero, delete the hash.
if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[1], 1);
redis.call('expire', KEYS[1], ARGV[2]);
return 1;
else
return 0;
endAutomatic Renewal (Watch‑Dog)
A background thread periodically extends the TTL while the lock holder is still alive.
private void renewTime(String lockName, String uuid, Long expire) {
String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('expire', KEYS[1], ARGV[2]); return 1; else return 0; end";
new Thread(() -> {
while (this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
Lists.newArrayList(lockName), uuid, expire.toString())) {
Thread.sleep(expire / 3);
}
}).start();
}RedLock Algorithm for Redis Cluster
In a clustered environment, acquire the lock on multiple independent Redis masters and consider the lock successful only if a majority of instances grant it, mitigating the risk of a master failure before replication.
Performance Comparison
Benchmarks using ab show that pure local locks work only within a single JVM, while the Redis‑based distributed lock maintains correctness across multiple service instances, albeit with lower throughput than a single‑node local lock.
Conclusion
By combining expiration, UUID verification, Lua‑script atomicity, re‑entrancy via hash, automatic renewal, and the RedLock algorithm, developers can build a reliable distributed lock on Redis that overcomes the shortcomings of local JVM locks in modern micro‑service architectures.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.