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 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><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency></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.
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.
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.