Implementing a Distributed Redis Lock with Spring AOP and Automatic Renewal
This article explains how to protect time‑consuming business operations using a Redis‑based distributed lock, detailing the annotation design, AOP pointcut, lock acquisition and release, timeout handling, and a scheduled renewal mechanism with sample Java code.
The article introduces a business scenario where long‑running requests need to be locked to prevent concurrent modifications of critical data, and proposes using Redis as a distributed lock manager.
Analysis Process : By storing lock state in Redis, the solution avoids JVM isolation issues in a cluster and defines a clear operation order to protect data integrity.
Design Steps :
Create a custom annotation @RedisLockAnnotation to mark methods that require locking.
Add an AOP pointcut that scans for this annotation.
Define an @Aspect class to intercept the annotated methods.
Use ProceedingJoinPoint to execute the original method before and after lock handling.
Perform lock acquisition before method execution and delete the key after completion.
Lock Acquisition :
The lock is obtained with RedisTemplate.opsForValue().setIfAbsent , storing a random UUID as the value and setting an expiration time.
Timeout Issue : If the intercepted method takes longer than the lock’s TTL, the lock may expire early, causing multiple threads to acquire the lock simultaneously.
Solution – Automatic Renewal :
A ScheduledExecutorService periodically scans a queue of lock holders and extends the TTL before it expires.
/**
* 线程池,每个 JVM 使用一个线程去维护 keyAliveTime,定时执行 runnable
*/
private static final ScheduledExecutorService SCHEDULER =
new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("redisLock-schedule-pool").daemon(true).build());
static {
SCHEDULER.scheduleAtFixedRate(() -> {
// do something to extend time
}, 0, 2, TimeUnit.SECONDS);
}The renewal logic checks if the remaining time is less than one‑third of the original TTL and, if so, resets the expiration and increments a retry counter.
// 扫描的任务队列
private static ConcurrentLinkedQueue
holderList = new ConcurrentLinkedQueue();
/**
* 线程池,维护keyAliveTime
*/
private static final ScheduledExecutorService SCHEDULER = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("redisLock-schedule-pool").daemon(true).build());
{
SCHEDULER.scheduleAtFixedRate(() -> {
Iterator
iterator = holderList.iterator();
while (iterator.hasNext()) {
RedisLockDefinitionHolder holder = iterator.next();
if (holder == null) { iterator.remove(); continue; }
if (redisTemplate.opsForValue().get(holder.getBusinessKey()) == null) { iterator.remove(); continue; }
if (holder.getCurrentCount() > holder.getTryCount()) {
holder.getCurrentTread().interrupt();
iterator.remove();
continue;
}
long curTime = System.currentTimeMillis();
boolean shouldExtend = (holder.getLastModifyTime() + holder.getModifyPeriod()) <= curTime;
if (shouldExtend) {
holder.setLastModifyTime(curTime);
redisTemplate.expire(holder.getBusinessKey(), holder.getLockTime(), TimeUnit.SECONDS);
log.info("businessKey : [" + holder.getBusinessKey() + "], try count : " + holder.getCurrentCount());
holder.setCurrentCount(holder.getCurrentCount() + 1);
}
}
}, 0, 2, TimeUnit.SECONDS);
}Core AOP Aspect :
@Pointcut("@annotation(cn.sevenyuan.demo.aop.lock.RedisLockAnnotation)")
public void redisLockPC() {}
@Around(value = "redisLockPC()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Method method = resolveMethod(pjp);
RedisLockAnnotation annotation = method.getAnnotation(RedisLockAnnotation.class);
RedisLockTypeEnum typeEnum = annotation.typeEnum();
Object[] params = pjp.getArgs();
String ukString = params[annotation.lockFiled()].toString();
String businessKey = typeEnum.getUniqueKey(ukString);
String uniqueValue = UUID.randomUUID().toString();
Object result = null;
try {
boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(businessKey, uniqueValue);
if (!isSuccess) {
throw new Exception("You can't do it,because another has get the lock =-=");
}
redisTemplate.expire(businessKey, annotation.lockTime(), TimeUnit.SECONDS);
Thread currentThread = Thread.currentThread();
holderList.add(new RedisLockDefinitionHolder(businessKey, annotation.lockTime(), System.currentTimeMillis(),
currentThread, annotation.tryCount()));
result = pjp.proceed();
if (currentThread.isInterrupted()) {
throw new InterruptedException("You had been interrupted =-=");
}
} catch (InterruptedException e) {
log.error("Interrupt exception, rollback transaction", e);
throw new Exception("Interrupt exception, please send request again");
} catch (Exception e) {
log.error("has some error, please check again", e);
} finally {
redisTemplate.delete(businessKey);
log.info("release the lock, businessKey is [" + businessKey + "]");
}
return result;
}Testing :
A sample controller method demonstrates the lock in action by sleeping for 10 seconds, exceeding the lock TTL, and triggering the renewal and interruption logic.
@GetMapping("/testRedisLock")
@RedisLockAnnotation(typeEnum = RedisLockTypeEnum.ONE, lockTime = 3)
public Book testRedisLock(@RequestParam("userId") Long userId) {
try {
log.info("睡眠执行前");
Thread.sleep(10000);
log.info("睡眠执行后");
} catch (Exception e) {
log.info("has some error", e);
}
return null;
}The log output shows the lock being acquired, renewal attempts, and finally an InterruptedException when the lock expires before the method finishes.
Conclusion : By combining a Redis distributed lock, Spring AOP interception, and a scheduled renewal thread, the solution safely guards time‑consuming business operations from concurrent execution, preventing data inconsistency.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.