Backend Development 15 min read

Implementing Distributed Locks with Redis and AOP in Spring Boot

This article explains how to protect time‑consuming business operations by using Redis as a distributed lock together with Spring AOP, covering annotation design, aspect implementation, lock acquisition, automatic renewal via a scheduled executor, and practical testing examples.

Top Architect
Top Architect
Top Architect
Implementing Distributed Locks with Redis and AOP in Spring Boot

1. Business Background

Some business requests are long‑running and need to be locked to prevent concurrent modifications, while also ensuring database consistency.

2. Analysis Process

Redis is used as a distributed lock, storing the lock state in Redis so that all JVMs in the cluster share the same lock information.

Core Steps

Create a custom @interface annotation to mark the method parameters that participate in locking.

Add an AOP pointcut that scans for the custom annotation.

Define an @Aspect bean that intercepts the target method.

Use ProceedingJoinPoint to execute the original method before and after the lock.

Perform lock acquisition before the method and delete the key after execution.

The essential steps are lock, unlock, and renewal.

Lock Acquisition

We use RedisTemplate.opsForValue().setIfAbsent to set a key with a random UUID value. If the key already exists, the request is rejected. After acquiring the lock, an expiration time is set and the task is added to a delayed queue for renewal.

Timeout Issue

If the business method takes longer than the lock timeout, the key may expire early, allowing another thread to acquire the lock and cause data inconsistency.

Solution: Add Renewal

A ScheduledExecutorService periodically scans the task queue and extends the lock expiration before it expires.

/**
 * Thread pool, each JVM uses one thread to maintain keyAliveTime, periodically executing 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);
}

3. Design Scheme

Based on the analysis, the following design is proposed:

Intercept annotation @RedisLock to obtain necessary parameters.

Perform lock operation.

Renew the lock periodically.

Release the lock after business execution.

4. Implementation Details

Below are the key code pieces.

Enum for Business Types

public enum RedisLockTypeEnum {
    ONE("Business1", "Test1"),
    TWO("Business2", "Test2");
    private String code;
    private String desc;
    RedisLockTypeEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    public String getCode() { return code; }
    public String getDesc() { return desc; }
    public String getUniqueKey(String key) {
        return String.format("%s:%s", this.getCode(), key);
    }
}

Lock Definition Holder

public class RedisLockDefinitionHolder {
    /** Business unique key */
    private String businessKey;
    /** Lock time (seconds) */
    private Long lockTime;
    /** Last update time (ms) */
    private Long lastModifyTime;
    /** Current thread */
    private Thread currentTread;
    /** Total retry attempts */
    private int tryCount;
    /** Current retry count */
    private int currentCount;
    /** Renewal period (ms) = lockTime/3 */
    private Long modifyPeriod;
    public RedisLockDefinitionHolder(String businessKey, Long lockTime, Long lastModifyTime, Thread currentTread, int tryCount) {
        this.businessKey = businessKey;
        this.lockTime = lockTime;
        this.lastModifyTime = lastModifyTime;
        this.currentTread = currentTread;
        this.tryCount = tryCount;
        this.modifyPeriod = lockTime * 1000 / 3;
    }
    // getters and setters omitted for brevity
}

Custom Annotation

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RedisLockAnnotation {
    /** Parameter index, default 0 */
    int lockFiled() default 0;
    /** Retry count */
    int tryCount() default 3;
    /** Lock type */
    RedisLockTypeEnum typeEnum();
    /** Lock expiration (seconds) */
    long lockTime() default 30;
}

Aspect Implementation

The aspect consists of three parts: pointcut definition, around advice, and lock renewal.

Pointcut

@Pointcut("@annotation(cn.sevenyuan.demo.aop.lock.RedisLockAnnotation)")
public void redisLockPC() {}

Around Advice

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

Renewal Task

private static ConcurrentLinkedQueue
holderList = new ConcurrentLinkedQueue<>();
private static final ScheduledExecutorService SCHEDULER = new ScheduledThreadPoolExecutor(1,
    new BasicThreadFactory.Builder().namingPattern("redisLock-schedule-pool").daemon(true).build());
static {
    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);
}

5. Testing

A controller method is annotated with @RedisLockAnnotation and simulates a long‑running task using Thread.sleep :

@GetMapping("/testRedisLock")
@RedisLockAnnotation(typeEnum = RedisLockTypeEnum.ONE, lockTime = 3)
public Book testRedisLock(@RequestParam("userId") Long userId) {
    try {
        log.info("sleep before");
        Thread.sleep(10000);
        log.info("sleep after");
    } catch (Exception e) {
        log.info("has some error", e);
    }
    return null;
}

When the request exceeds the lock timeout, the scheduled renewal thread interrupts the business thread, causing an InterruptedException . Log output demonstrates lock acquisition, renewal attempts, and eventual interruption.

6. Summary

For time‑consuming business operations, using a Redis‑based distributed lock combined with AOP ensures that only one request can modify critical data at a time. Adding a renewal mechanism via ScheduledExecutorService prevents premature lock release, while thread interruption handles excessive retries.

JavaAOPRedisSpring BootDistributed LockScheduledExecutorService
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login 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.