封装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),想要支持的话需要自己开发一个插件
相关推荐
爷_1 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
不过普通话一乙不改名4 小时前
第一章:Go语言基础入门之函数
开发语言·后端·golang
豌豆花下猫5 小时前
Python 潮流周刊#112:欢迎 AI 时代的编程新人
后端·python·ai
Electrolux5 小时前
你敢信,不会点算法没准你赛尔号都玩不明白
前端·后端·算法
whhhhhhhhhw6 小时前
Go语言-fmt包中Print、Println与Printf的区别
开发语言·后端·golang
ん贤6 小时前
Zap日志库指南
后端·go
Spliceㅤ6 小时前
Spring框架
java·服务器·后端·spring·servlet·java-ee·tomcat
IguoChan7 小时前
10. Redis Operator (3) —— 监控配置
后端
Micro麦可乐8 小时前
前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践
前端·spring boot·后端·jwt·refresh token·无感token刷新
方块海绵9 小时前
浅析 MongoDB
后端