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:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2 Define lock/unlock Lua scripts

Lock script (lock.lua):

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

Unlock script (unlock.lua):

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

Renewal script (renewal.lua):

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

2.3 Lua script configuration

Register the scripts as Spring beans:

@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;
    }
}

2.4 Distributed lock implementation

Core lock class:

@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();
        }
    }
}

2.5 Usage example

Service that decrements a product count under the distributed lock:

@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();
        }
    }
}

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

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.

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

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.