How to Build a Robust Re‑entrant Distributed Lock with Redis, Lua and Java

This article explains why a MySQL repeatable‑read transaction can produce an incorrect negative balance, then shows how to prevent such anomalies by implementing a reliable, re‑entrant distributed lock using Redis commands, Lua scripts, and Java/Spring‑Boot code, while also covering lock expiration, atomicity, and the RedLock algorithm.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How to Build a Robust Re‑entrant Distributed Lock with Redis, Lua and Java

MySQL Repeatable‑Read Anomaly

When an account row (id=1, balance=1000) is updated by two concurrent transactions that each deduct 1000, the snapshot read of transaction 2 sees the old balance (1000) even though transaction 1 has already committed, leading the Java‑level balance check to pass and the update to produce a negative balance (id=1, balance=-1000).

if (balance - amount < 0) {
    throw new XXException("余额不足,扣减失败");
}

The update statement is:

UPDATE account SET balance = balance - 1000 WHERE id = 1;

Adding a condition to the UPDATE prevents the error:

UPDATE account SET balance = balance - 1000 WHERE id = 1 AND balance > 0;

Redis Distributed Lock Basics

Redis can provide a simple lock with SETNX, but it lacks an expiration time, which can cause dead locks if the client crashes.

Redis 2.6.12 introduced the atomic SET key value NX EX seconds command, which sets the key only if it does not exist and also sets a TTL.

SET lock_name anystring NX EX lock_time

Unlocking

Unlocking with DEL lock_name is unsafe because another client may acquire the lock after expiration and the original client could delete it inadvertently.

To avoid this, store a unique identifier (e.g., UUID) as the lock value and delete only when the stored value matches:

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

Java / Spring‑Boot Implementation

Using Spring’s StringRedisTemplate (Redis 2.1+), a non‑blocking lock can be acquired:

public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
    return stringRedisTemplate.opsForValue()
        .setIfAbsent(lockName, request, leaseTime, unit);
}

For older versions, the same effect can be achieved with a low‑level RedisCallback:

public Boolean doOldTryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
    return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
        RedisSerializer<String> ks = stringRedisTemplate.getKeySerializer();
        RedisSerializer<String> vs = stringRedisTemplate.getValueSerializer();
        return connection.set(
            ks.serialize(lockName),
            vs.serialize(request),
            Expiration.from(leaseTime, unit),
            RedisStringCommands.SetOption.SET_IF_ABSENT);
    });
}

Unlocking is performed with the Lua script shown earlier, loaded via DefaultRedisScript:

public Boolean unlock(String lockName, String request) {
    DefaultRedisScript<Boolean> unlockScript = new DefaultRedisScript<>();
    unlockScript.setLocation(new ClassPathResource("simple_unlock.lua"));
    unlockScript.setResultType(Boolean.class);
    return stringRedisTemplate.execute(unlockScript, Collections.singletonList(lockName), request);
}

Re‑entrancy and Advanced Issues

Because SETNX (or SET … NX) fails if the key already exists, a lock is not re‑entrant. Two approaches can add re‑entrancy:

Maintain a ThreadLocal counter in the client and release the lock only when the counter reaches zero.

Store the lock count in a Redis hash and manipulate it atomically with Lua scripts.

Lock expiration can cause “lock‑stealing” if a long‑running task exceeds the TTL. Solutions include:

Choosing a TTL comfortably longer than the expected execution time.

Running a watchdog thread that periodically extends the TTL before it expires.

In clustered environments, the RedLock algorithm (N independent Redis masters) mitigates split‑brain scenarios; libraries such as Redisson already implement it.

Summary

Simple Redis locks using SETNX/DEL are easy but unsafe. Using SET … NX EX with a unique value and an atomic Lua unlock script provides a robust lock. Adding re‑entrancy, proper TTL handling, and, if needed, the RedLock algorithm yields a production‑grade distributed lock suitable for Java and Spring‑Boot applications.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

spring-bootdistributed-lock
Java Backend Technology
Written by

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!

0 followers
Reader feedback

How this landed with the community

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.