封装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),想要支持的话需要自己开发一个插件
相关推荐
lazy H2 分钟前
【计算机网络】第 15 篇:Java BIO、NIO 与高并发网络通信——从 Socket 理解 I/O 模型
网络·后端·tcp/ip·计算机网络·nio·408
geovindu13 分钟前
rust: Semaphore Pattern
开发语言·后端·设计模式·rust·代理模式·企业级信号量模式·同步型模式
Thneonl2 小时前
Claude Code 频繁 Compacting?发布次日我根治了 2 个痛点
后端·rust
环境栈笔记3 小时前
指纹浏览器安卓怎么用?从运行位置到功能验收,先把三种方案分清
前端·人工智能·后端·自动化
小小张说故事4 小时前
Python 的 list 为什么能装下任何类型?拆开 PyObject* 数组,以及它让你多付了多少内存
后端·python
小蒜学长4 小时前
基于SpringBoot 框架的自媒体新闻系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·mysql数据库·自媒体新闻管理系统
IT_陈寒5 小时前
JavaScript实战技巧总结:深挖数组遍历的性能陷阱与实战解法
前端·人工智能·后端
学长毕设14 小时前
基于springboot的食品仓库管理系统
java·spring boot·后端
滴滴答答哒15 小时前
ASP.NET Core + FaceRecognitionDotNet 人脸识别实战:DI 单例、特征向量持久化与 1:N 比对
后端·asp.net
captain37617 小时前
MyBatis操作数据库(入门2)
java·数据库·spring boot·后端·spring·mybatis