Backend Development 17 min read

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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing Distributed Locks with Redis: Principles, Challenges, and Optimizations

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;
end

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

JavaRedisDistributed LockLuaRedlockSpring Data Redisreentrancy
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.