Backend Development 10 min read

Mastering Distributed Locks in Spring Boot 3 with Redis and Lua

This article introduces a comprehensive Spring Boot 3 practical case collection and walks through building a reentrant, auto‑renewing distributed lock using Redis and Lua scripts, providing full code snippets and configuration guidance for robust backend concurrency control.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Distributed Locks in Spring Boot 3 with Redis and Lua

Spring Boot 3 practical case collection (over 100 articles) is announced, with a PDF ebook and a promise of permanent updates for subscribers.

1. Introduction

In distributed systems, concurrent access to the same resource can cause inconsistency or deadlock. A distributed lock solves this problem, and implementing it yourself helps understand its inner workings.

2. Practical Implementation

2.1 Environment preparation

Add the Redis starter dependency:

<code>&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;
&lt;/dependency&gt;</code>

2.2 Define lock/unlock Lua scripts

Lock script (lock.lua):

<code>local key = KEYS[1]
local lockId = ARGV[1]
local expireTime = ARGV[2]
if (redis.call('exists', key) == 0) then
    redis.call('hset', key, lockId, 1)
    redis.call('pexpire', key, expireTime)
    return 1
end
if (redis.call('hexists', key, lockId) == 1) then
    redis.call('hincrby', key, lockId, 1)
    redis.call('pexpire', key, expireTime)
    return 1
end
return 0</code>

Unlock script (unlock.lua):

<code>local key = KEYS[1]
local lockId = ARGV[1]
if (redis.call('hexists', key, lockId) == 0) then
    return nil
end
local count = redis.call('hincrby', key, lockId, -1)
if (count > 0) then
    redis.call('pexpire', key, ARGV[2])
    return 0
else
    redis.call('del', key)
    return 1
end</code>

Renewal script (renewal.lua):

<code>local key = KEYS[1]
local lockId = ARGV[1]
local expireTime = ARGV[2]
if (redis.call('hexists', key, lockId) == 1) then
    redis.call('pexpire', key, expireTime)
    return 1
end
return 0</code>

2.3 Lua script configuration

Register the scripts as Spring beans:

<code>@Configuration
public class RedisLockConfig {
    @Bean
    public DefaultRedisScript<Long> lockScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("lua/lock.lua"));
        script.setResultType(Long.class);
        return script;
    }
    @Bean
    public DefaultRedisScript<Long> unlockScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("lua/unlock.lua"));
        script.setResultType(Long.class);
        return script;
    }
    @Bean
    public DefaultRedisScript<Long> renewalScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("lua/renewal.lua"));
        script.setResultType(Long.class);
        return script;
    }
}</code>

2.4 Distributed lock implementation

Core lock class:

<code>@Component
public class RedisDistributedLock {
    private static final String LOCK_PREFIX = "pack:lock:";
    private static final long LOCK_EXPIRE_TIME = 30 * 1000;
    private static final ThreadLocal<Map<String, String>> lockHolder = ThreadLocal.withInitial(HashMap::new);
    private final StringRedisTemplate redisTemplate;
    private final DefaultRedisScript<Long> lockScript;
    private final DefaultRedisScript<Long> unlockScript;
    private final DefaultRedisScript<Long> renewalScript;
    private final String id = UUID.randomUUID().toString().replaceAll("-", "");
    private final String lockKey;
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    public RedisDistributedLock(StringRedisTemplate redisTemplate,
                               DefaultRedisScript<Long> lockScript,
                               DefaultRedisScript<Long> unlockScript,
                               DefaultRedisScript<Long> renewalScript,
                               String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockScript = lockScript;
        this.unlockScript = unlockScript;
        this.renewalScript = renewalScript;
        this.lockKey = lockKey;
    }

    public void lock() {
        long expireMillis = LOCK_EXPIRE_TIME;
        String actualKey = LOCK_PREFIX + lockKey;
        while (true) {
            String lockId = generateLockId();
            Long result = redisTemplate.execute(lockScript, List.of(actualKey), lockId, String.valueOf(expireMillis));
            if (result != null && result == 1) {
                lockHolder.get().put(actualKey, lockId);
                startRenewalThread(actualKey, lockId, expireMillis);
                return;
            }
            try {
                TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 50) + 50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public void unlock() {
        long expireMillis = LOCK_EXPIRE_TIME;
        String actualKey = LOCK_PREFIX + lockKey;
        String lockId = lockHolder.get().get(actualKey);
        if (lockId == null) {
            return;
        }
        Long ret = redisTemplate.execute(unlockScript, List.of(actualKey), lockId, String.valueOf(expireMillis));
        if (ret != null && ret == 1) {
            lockHolder.get().remove(actualKey);
            stopRenewalThread();
        }
    }

    private String generateLockId() {
        return id + ":" + Thread.currentThread().threadId();
    }

    private void startRenewalThread(String key, String lockId, long expireTime) {
        long delay = expireTime / 3;
        scheduler.scheduleAtFixedRate(() -> {
            Long ret = redisTemplate.execute(renewalScript, List.of(key), lockId, String.valueOf(expireTime));
            if (ret == null || ret == 0) {
                scheduler.shutdownNow();
            }
        }, delay, delay, TimeUnit.MILLISECONDS);
    }

    private void stopRenewalThread() {
        if (!scheduler.isShutdown()) {
            scheduler.shutdownNow();
        }
    }
}
</code>

2.5 Usage example

Service that decrements a product count under the distributed lock:

<code>@Service
public class ProductService {
    private int count = 20;
    private final PackLock packLock;

    public ProductService(PackLock packLock) {
        this.packLock = packLock;
    }

    public void calc() {
        if (count <= 0) {
            return;
        }
        RedisDistributedLock lock = packLock.getLock("xxx");
        lock.lock();
        try {
            if (count > 0) {
                count--;
            }
        } finally {
            lock.unlock();
        }
    }
}
</code>

The article concludes with a reminder to like, share, and follow, and provides links to related recommended articles.

JavaRedisSpring BootDistributed Locklua
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

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.