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:
<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 0Unlock 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
endRenewal 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 02.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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
