Implementing a Redis Distributed Lock with Timeout Handling in Java
This article explains how to build a robust Redis-based distributed lock in a Spring MVC Java project, addressing lock expiration issues by adding a postponement thread, and demonstrates the solution with complete source code, Maven dependencies, testing using JMeter, and performance results.
Redis distributed locks often fail when the lock expires before the business logic finishes, causing multiple threads to hold the lock simultaneously. This guide presents a complete solution using Spring MVC, Maven, and Java to ensure exclusive lock ownership even when execution exceeds the lock timeout.
Preparation
Required tools include JMeter for load testing, Redis Desktop Manager for inspecting keys, and Postman for API calls. Download links are provided via Baidu Cloud.
Implementation Overview
The project consists of three core classes:
DistributedLock – utility class handling lock acquisition, release, and postponement.
PcInformationServiceImpl – service layer that uses the lock.
PostponeTask – daemon thread that extends the lock's TTL before it expires.
Version 01 Code
DistributedLock
package com.cn.pinliang.common.util;
import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.io.Serializable;
import java.util.Collections;
@Component
public class DistributedLock {
@Autowired
private RedisTemplate<Serializable, Object> redisTemplate;
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "EX";
// Unlock script (Lua)
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* Distributed lock
* @param key
* @param value
* @param expireTime seconds
* @return
*/
public boolean lock(String key, String value, long expireTime) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* Unlock
*/
public Boolean unLock(String key, String value) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}PcInformationServiceImpl (excerpt)
public JsonResult add() throws Exception {
String key = "add_information_lock";
String value = RandomUtil.produceStringAndNumber(10);
long expireTime = 10L;
boolean lock = distributedLock.lock(key, value, expireTime);
String threadName = Thread.currentThread().getName();
if (lock) {
System.out.println(threadName + " 获得锁...............");
Thread.sleep(30000);
distributedLock.unLock(key, value);
System.out.println(threadName + " 解锁了...............");
} else {
System.out.println(threadName + " 未获取到锁...............");
return JsonResult.fail("未获取到锁");
}
return JsonResult.succeed();
}Running this version without the postponement thread shows that when the lock expires after 10 seconds while the business logic runs for 30 seconds, another thread can acquire the lock, violating exclusivity.
Version 02 Enhancements
Added a postponement mechanism to extend the lock before it expires.
DistributedLock (updated)
... (same imports as before) ...
private static final Long POSTPONE_SUCCESS = 1L;
private static final String POSTPONE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end";
// lock method now starts a PostponeTask when lock succeeds
public boolean lock(String key, String value, long expireTime) {
Boolean locked = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
return LOCK_SUCCESS.equals(result) ? Boolean.TRUE : Boolean.FALSE;
});
if (locked) {
PostponeTask postponeTask = new PostponeTask(key, value, expireTime, this);
Thread thread = new Thread(postponeTask);
thread.setDaemon(Boolean.TRUE);
thread.start();
}
return locked;
}
// new postpone method
public Boolean postpone(String key, String value, long expireTime) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(POSTPONE_LOCK_SCRIPT, Lists.newArrayList(key), Lists.newArrayList(value, String.valueOf(expireTime)));
return POSTPONE_SUCCESS.equals(result) ? Boolean.TRUE : Boolean.FALSE;
});
}
// unlock unchanged
}PostponeTask
package com.cn.pinliang.common.thread;
import com.cn.pinliang.common.util.DistributedLock;
public class PostponeTask implements Runnable {
private String key;
private String value;
private long expireTime;
private boolean isRunning;
private DistributedLock distributedLock;
public PostponeTask(String key, String value, long expireTime, DistributedLock distributedLock) {
this.key = key;
this.value = value;
this.expireTime = expireTime;
this.isRunning = true;
this.distributedLock = distributedLock;
}
@Override
public void run() {
long waitTime = expireTime * 1000 * 2 / 3; // wait 2/3 of TTL
while (isRunning) {
try {
Thread.sleep(waitTime);
if (distributedLock.postpone(key, value, expireTime)) {
System.out.println("延时成功...........................................................");
} else {
stop();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void stop() {
this.isRunning = false;
}
}With this version, the postponement thread renews the lock after two‑thirds of the original TTL, ensuring the lock remains while the business logic is still running. Test results show the key persists in Redis after 10 seconds, and subsequent requests cannot acquire the lock until the original thread finishes and releases it.
Testing and Results
Using JMeter with five concurrent threads, the first request acquires the lock, others are blocked. After the lock’s original 10 second TTL, the postponement thread extends it, keeping the key alive. Only after the 30‑second business logic completes does the lock get released, allowing other threads to proceed.
Images in the original article illustrate the Redis key state and console logs confirming successful postponement and correct lock release.
Conclusion
The presented approach solves the lock‑timeout‑while‑business‑logic‑still‑running problem by adding a daemon postponement thread that safely extends the lock’s TTL. For production‑grade reliability, consider using Redisson’s implementation of the Redlock algorithm, which provides similar functionality with built‑in safety guarantees.
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.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.
