封装Spring Boot Redisson分布式锁

文章基础知识

  • Redis、Redisson
  • JDK8 特性
  • Java异常
  • 可重入锁
  • SpEL表达式
  • 自定义注解
  • AOP
  • Spring SPI
  • Spring Boot Starter

上述知识都比较简单,有不了解的问AI即可

需要的场景

  • 分布式系统多节点部署
  • 需要使用锁,使用Java内置的锁无法满足实际场景
  • 使用可重入锁

目录结构

1. 普通注解

@LockKey

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: Kenan
 * @Date: 2025-07-22 07:37
 * @Description: 分布式锁
 */
@Target({ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface LockKey {

}

@UseRLock

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * @Author: Kenan
 * @Date: 2025-07-22 07:37
 * @Description: 分布式锁
 */
@Target(ElementType.METHOD) // 仅用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
public @interface UseRLock {
    /**
     * 最大超时时间 单位ms
     */
    long maxLockTime() default 5000;

    TimeUnit maxLockTimeUnit() default TimeUnit.MILLISECONDS;

    /**
     * lock的area名称,为了区分不同业务
     */
    String area() default "default";

    /**
     * lock的类名称,可自定义设置
     * 默认取 package+class
     */
    String clazz() default "default";

    /**
     * lock的method名称,可以自定义设置
     * 默认取方法名称
     */
    String method() default "default";

    /**
     * 当policy=LockPolicy.REJECT,异常信息可通过此字段来定义
     */
    String errorMsg() default "";

    String value() default "默认操作"; // 可配置参数

    LockPolicy policy() default LockPolicy.REJECT;

    enum LockPolicy {
        WAIT,   // 等待
        REJECT  // 存在锁直接拒绝
    }
}

@UseRLockAspect

java 复制代码
import com.scaffold.boot.lock.annonation.LockKey;
import com.scaffold.boot.lock.annonation.UseRLock;
import com.scaffold.boot.lock.exception.RLockException;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.concurrent.TimeUnit;

/**
 * @Author: Kenan
 * @Date: 2025-07-22 07:44
 * @Description: 锁参数切面
 */
@Aspect
@Component
@AllArgsConstructor
public class UseRLockAspect {
    private final static String REDIS_NS = "rlock:";
    private final static String DEFAULT = "default";
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    private final RedissonClient redissonClient;

    @AfterThrowing(value = "@annotation(com.scaffold.boot.lock.annonation.UseRLock)", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) throws Throwable {
        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
            unlock(joinPoint);
        } else {
            TransactionSynchronizationManager.registerSynchronization(getSynchronization(joinPoint));
        }
    }

    @AfterReturning("@annotation(com.scaffold.boot.lock.annonation.UseRLock)")
    public void after(JoinPoint joinPoint) throws Throwable {

        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
            unlock(joinPoint);
        } else {
            TransactionSynchronizationManager.registerSynchronization(getSynchronization(joinPoint));
        }
    }

    private TransactionSynchronizationAdapter getSynchronization(JoinPoint joinPoint) {
        return new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                try {
                    unlock(joinPoint);
                } catch (Exception ex) {
                    log.error("unlock:{}", ex.getMessage(), ex);
                }
            }
        };
    }

    private void unlock(JoinPoint joinPoint) throws NoSuchMethodException {
        UseRLock annotation = getUseRLock(joinPoint);
        String lockKey = getLockKey(annotation.area(), annotation.clazz(), annotation.method(), joinPoint);
        RLock lock = redissonClient.getLock(lockKey);
        if (lock == null) {
            return;
        }
        try {
            if (lock.isHeldByCurrentThread() && lock.isLocked()) {
                lock.unlock();
            }
        } catch (IllegalMonitorStateException ex) {
            log.error("unlock失败:{}", ex.getMessage(), ex);
        }
    }

    @Around("@annotation(com.scaffold.boot.lock.annonation.UseRLock)")
    public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
        lock(pjp);
        return pjp.proceed();
    }

    private void lock(ProceedingJoinPoint pjp) throws NoSuchMethodException {
        UseRLock annotation = getUseRLock(pjp);
        TimeUnit maxLockTimeUnit = annotation.maxLockTimeUnit();
        long maxLockTime = annotation.maxLockTime();
        String area = annotation.area();
        String clazz = annotation.clazz();
        String method = annotation.method();
        UseRLock.LockPolicy policy = annotation.policy();
        String lockKey = getLockKey(area, clazz, method, pjp);
        RLock lock = redissonClient.getLock(lockKey);
        boolean locked = lock.isLocked();
        if (locked && UseRLock.LockPolicy.REJECT.equals(policy)) {
            String errorMsg = StringUtils.isBlank(annotation.errorMsg()) ? "此业务调用仅允许一个并发处理" : annotation.errorMsg();
            throw new RuntimeException(errorMsg);
        }
        lock.lock(maxLockTime, maxLockTimeUnit);
    }

    private UseRLock getUseRLock(JoinPoint pjp) throws NoSuchMethodException {
        Signature signature = pjp.getSignature();
        MethodSignature mSignature = (MethodSignature) signature;
        Class<?> declaringType = mSignature.getDeclaringType();
        Method method = declaringType.getDeclaredMethod(mSignature.getName(), mSignature.getParameterTypes());
        return method.getAnnotation(UseRLock.class);
    }

    private String getLockKey(String area, String clazz, String methodName, JoinPoint pjp) throws NoSuchMethodException {
        Signature signature = pjp.getSignature();
        MethodSignature mSignature = (MethodSignature) signature;
        Class<?> declaringType = mSignature.getDeclaringType();
        Method method = declaringType.getDeclaredMethod(mSignature.getName(), mSignature.getParameterTypes());
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        if (parameterAnnotations.length == 0) {
            throw new RLockException("@LockKey must be assigned");
        }
        if (DEFAULT.equals(clazz)) {
            clazz = declaringType.getName();
        }
        if (DEFAULT.equals(methodName)) {
            methodName = method.getName();
        }

        Parameter[] parameters = method.getParameters();
        Object[] args = pjp.getArgs();
        StringBuilder lockKey = new StringBuilder();
        lockKey.append(REDIS_NS);
        lockKey.append(area);
        lockKey.append(":");
        lockKey.append(clazz);
        lockKey.append(":");
        lockKey.append(methodName);

        boolean hasLockKey = false;
        for (int index = 0; index < args.length; index++) {
            Parameter parameter = parameters[index];
            LockKey annotation = parameter.getAnnotation(LockKey.class);
            if (annotation != null) {
                lockKey.append(":");
                Object arg = args[index];
                String argStr = arg.toString();
                if (StringUtils.isBlank(argStr)) {
                    throw new RLockException("@LockKey parameter must not be null");
                }
                hasLockKey = true;
                lockKey.append(argStr);
            }
        }
        if (!hasLockKey) {
            throw new RLockException("@LockKey must be assigned");
        }

        return lockKey.toString();
    }

}

Demo

java 复制代码
    @UseRLock(maxLockTime = 1, maxLockTimeUnit = TimeUnit.SECONDS, area = "system", policy = UseRLock.LockPolicy.REJECT)
    public Boolean testLock(@LockKey String no, @LockKey String aaa) {
        return Boolean.TRUE;
    }

2. SpEL方式

@SpELRLock

java 复制代码
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * @Author: Kenan
 * @Date: 2025-07-22 07:37
 * @Description: 分布式锁
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SpELRLock {
    /**
     * 最大超时时间 单位ms
     */
    long maxLockTime() default 5000;

    TimeUnit maxLockTimeUnit() default TimeUnit.MILLISECONDS;

    /**
     * lock的area名称,为了区分不同业务
     */
    String area() default "default";

    /**
     * lock的类名称,可自定义设置
     * 默认取 package+class
     */
    String clazz() default "default";

    /**
     * lock的method名称,可以自定义设置
     * 默认取方法名称
     */
    String method() default "default";

    /**
     * 当policy=LockPolicy.REJECT,异常信息可通过此字段来定义
     */
    String errorMsg() default "";

    String value() default "默认操作";

    LockPolicy policy() default LockPolicy.REJECT;

    /**
     * SpEL表达式,用于生成锁的key
     * 示例: "#userId" 或 "'order:' + #order.id"
     */
    String key();

    enum LockPolicy {
        WAIT,   // 等待
        REJECT  // 存在锁直接拒绝
    }
}

业务代码 @UseRLockAspect

java 复制代码
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * @Author: Kenan
 * @Date: 2025-07-22 07:37
 * @Description: 分布式锁
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SpELRLock {
    /**
     * 最大超时时间 单位ms
     */
    long maxLockTime() default 5000;

    TimeUnit maxLockTimeUnit() default TimeUnit.MILLISECONDS;

    /**
     * lock的area名称,为了区分不同业务
     */
    String area() default "default";

    /**
     * lock的类名称,可自定义设置
     * 默认取 package+class
     */
    String clazz() default "default";

    /**
     * lock的method名称,可以自定义设置
     * 默认取方法名称
     */
    String method() default "default";

    /**
     * 当policy=LockPolicy.REJECT,异常信息可通过此字段来定义
     */
    String errorMsg() default "";

    String value() default "默认操作";

    LockPolicy policy() default LockPolicy.REJECT;

    /**
     * SpEL表达式,用于生成锁的key
     * 示例: "#userId" 或 "'order:' + #order.id"
     */
    String key();

    enum LockPolicy {
        WAIT,   // 等待
        REJECT  // 存在锁直接拒绝
    }
}

Demo

java 复制代码
    @SpELRLock(maxLockTime = 1, maxLockTimeUnit = TimeUnit.SECONDS,
            area = "system", policy = SpELRLock.LockPolicy.REJECT,
            key = "#no?.toString().concat(':').concat(#userSaveReqVO?.id?.toString())")
    public Boolean testSpELLock(String no, UserSaveReqVO userSaveReqVO) {
        return Boolean.TRUE;
    }

注意事项

  • resources目录下的META-INF.spring目录结构是上下级的关系,不是同一层级用.分隔。注入方式从Spring Boot2.7开始为图中的注入方式,以前的版本直接百度即可。

两种方式的对比

  • 普通注解解析变量局限在对象中的字段需要额外处理,但是有多个字段共同去使用时则需要在多个字段上标注@LockKey。
  • 使用SpEL表达式对不了解此内容的有局限性,容易导致对此内容不了解造成数据异常。但是对函数入口字段没有侵入。

TIPS

  • 到官网查到截止2025.1版本都不支持自定义的SpEL表达式,虽然这个需求已经在5年前就有人反馈了...
  • 在IDEA的设置中添加SpEL表达式也不会起作用(只能识别到Bean),想要支持的话需要自己开发一个插件
相关推荐
smileNicky5 小时前
SpringBoot系列之从繁琐配置到一键启动之旅
java·spring boot·后端
David爱编程6 小时前
为什么必须学并发编程?一文带你看懂从单线程到多线程的演进史
java·后端
long3166 小时前
java 策略模式 demo
java·开发语言·后端·spring·设计模式
rannn_1117 小时前
【Javaweb学习|黑马笔记|Day1】初识,入门网页,HTML-CSS|常见的标签和样式|标题排版和样式、正文排版和样式
css·后端·学习·html·javaweb
柏油7 小时前
Spring @Cacheable 解读
redis·后端·spring
柏油8 小时前
Spring @TransactionalEventListener 解读
spring boot·后端·spring
两码事9 小时前
告别繁琐的飞书表格API调用,让飞书表格操作像操作Java对象一样简单!
java·后端
shark_chili10 小时前
面试官再问synchronized底层原理,这样回答让他眼前一亮!
后端
灵魂猎手10 小时前
2. MyBatis 参数处理机制:从 execute 方法到参数流转全解析
java·后端·源码
易元10 小时前
模式组合应用-桥接模式(一)
后端·设计模式