超越@Transactional!自定义@DistributedLock注解深度实战


种一棵树最好的时间是10年前,其次就是现在,加油!
                                                                                   --by蜡笔小柯南

还在每个方法里重复写着 RLock lock = redissonClient.getLock(key) 这样的模板代码吗?不仅繁琐,还容易忘记释放锁导致死锁。

想象一下,如果能像使用@Transactional一样简单地使用分布式锁,会是怎样的体验?本文将带你实现功能更强大的@DistributeLock注解,支持SpEL表达式等高级特性!

依赖导入

需要导入RedissonAspect 等依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.24.3</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.20.1</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.21</version>
        </dependency>

自定义注解

创建DistributeLock注解,并配置此注解可以使用的范围,@Target和@Retention是Java中的元注解, @Target说明了注解所能使用的范围,即注解可以用在哪些地方,@Retention用于描述注解的生命周期,即注解在什么范围内有效。

  • @Target: 表示注解可以用在哪里

    • ElementType.METHOD: 方法声明,表示用在方法上
    • ElementType.TYPE:用于类、接口(包括注解接口)、枚举或记录声明
    • ElementType.FIELD:字段声明(包括枚举常量),用于字段上
    • ElementType.PARAMETER:形参声明,用于参数上
    • ElementType.CONSTRUCTOR:构造声明,用于构造方法上
    • ElementType.LOCAL_VARIABLE:局部变量声明,用于局部变量上
    • ElementType.ANNOTATION_TYPE:注解接口声明
    • ElementType.PACKAGE:包声明
    • ElementType.TYPE_PARAMETER:类型参数声明
    • ElementType.TYPE_USE:类型的使用
    • ElementType.MODULE:模块声明
    • ElementType.RECORD_COMPONENT:记录组件
  • @Retention:表述注解的生命周期

    • RetentionPolicy.SOURCE:只保留在源文件,编译成class时将丢弃
    • CLASS:被保留在class文件中,但在运行时不会保留,即在运行时无法获取,这是默认的生命周期策略
    • RUNTIME:不但在class文件中存在,运行时也存在,因此可以通过反射去获取

我们自定义的DistributeLock注解用于方法上,所以将@Target定义为ElementType.METHOD,在运行时需要获取注解的相关信息,所以将@Retention定义为RetentionPolicy.RUNTIME

/**
 * 分布式锁注解
 * @author 蜡笔小柯南
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributeLock {

    /**
     * 锁的key
     */
    public String key() default DistributeLockConstant.NONE_KEY;
    
    /**
     * 锁的使用场景
     */
    public String scene();

    /**
     * key的SPEL表达式
     */
    public String keyExpression() default DistributeLockConstant.NONE_KEY;

    /**
     * 超时时间,毫秒
     * 默认情况下不设置超时时间,会自动续期
     */
    public int expireTime() default DistributeLockConstant.DEFAULT_EXPIRE_TIME;

    /**
     * 加锁等待时长,毫秒
     * 默认情况下不设置等待时长,会一直等待直到获取到锁
     */
    public int waitTime() default DistributeLockConstant.DEFAULT_WAIT_TIME;
}

自定义注解常量类

创建名为DistributeLockConstant的常量类,用于设置默认值

/**
 * 分布式锁常量
 *
 * @author 蜡笔小柯南
 */
public class DistributeLockConstant {

    /**
     * 默认的key名称
     */
    public static final String NONE_KEY = "NONE";

    /**
     * 默认超时时间,默认值:-1
     */
    public static final int DEFAULT_EXPIRE_TIME = -1;

    /**
     * 默认枷锁等待时间,默认值:-1
     */
    public static final int DEFAULT_WAIT_TIME = -1;
}

自定义切面类

创建名为DistributeLockAspect的切面,使得自定义注解生效

/**
 * 分布式锁切面
 *
 * @author 蜡笔小柯南
 */
@Component
@Aspect
public class DistributeLockAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(com.yogurt.lock.DistributeLock)")
    public Object process(ProceedingJoinPoint pjp) throws Exception {
        // 获取方法
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        // 获取方法上的注解
        DistributeLock distributeLock = method.getAnnotation(DistributeLock.class);
        // key 先尝试获取key,如果key有值,则使用key;如果key是NONE,则尝试获取keyExpression
        String key = distributeLock.key();
        if (DistributeLockConstant.NONE_KEY.equals(key)) {
            // SpEL表达式
            String keyExpression = distributeLock.keyExpression();
            // 如果keyExpression是NONE,则抛出异常,提示错误信息
            if (DistributeLockConstant.NONE_KEY.equals(keyExpression)) {
                throw new RuntimeException("key和keyExpression不能同时为空");
            }
            EvaluationContext context = new StandardEvaluationContext();
            // 获取运行时参数的名称
            DefaultParameterNameDiscoverer discoverer
                    = new DefaultParameterNameDiscoverer();
            // 获取参数名称
            String[] parameterNames = discoverer.getParameterNames(method);
            // 将参数绑定到context中
            if (parameterNames != null) {
                // 获取参数值,如果参数是对象,则获取的是对象的地址
                Object[] args = pjp.getArgs();
                for (int i = 0; i < parameterNames.length; i++) {
                    context.setVariable(parameterNames[i], args[i]);
                }
            }
            // 解析SpEL表达式
            SpelExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(keyExpression);
            // 解析表达式,获取结果
            key = expression.getValue(context, String.class);
        }
        // 获取锁的场景
        String scene = distributeLock.scene();
        // 锁的key,锁的场景+key
        String lockKey = scene + "#" + key;
        // 锁的过期时间
        int expireTime = distributeLock.expireTime();
        // 锁的等待时间
        int waitTime = distributeLock.waitTime();
        // 获取锁
        RLock rLock= redissonClient.getLock(lockKey);
        Object result = null;
        try {
            // 加锁是否成功标识 true:成功 false:失败
            boolean lockFlag = false;
            // 如果等待时间是默认值,说明没有设置等待时间
            if (waitTime == DistributeLockConstant.DEFAULT_WAIT_TIME) {
                // 锁的过期时间是默认值,说明没有设置锁的过期时间
                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
                    // 加锁,无任何参数
                    rLock.lock();
                } else {
                    // 说明锁的过期时间不是默认值,则设置锁的过期时间
                    rLock.lock(expireTime, TimeUnit.MILLISECONDS);
                }
                lockFlag = true;
            } else {
                // 等待时间不是默认值,说明设置了等待时间,继续判断锁的过期时间是否是默认值
                // 过期时间如果是默认值,则只设置锁的等待时间
                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
                    lockFlag = rLock.tryLock(waitTime, TimeUnit.MILLISECONDS);
                } else {
                    // 锁的过期时间不是默认值,则设置锁的等待时间和锁的过期时间
                    lockFlag = rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS);
                }
            }
            if (!lockFlag) {
                throw new RuntimeException("获取锁失败... key : " + lockKey);
            }
            // 实际方法执行
            result = pjp.proceed();
        } catch (Throwable e) {
            throw new Exception(e);
        } finally {
            if (rLock.isHeldByCurrentThread()) {
                // 释放锁
                rLock.unlock();
            }
        }
        return result;
    }
}

使用@Around环绕通知的方式,其中@Around注解中,使用@annotation(com.yogurt.mp.lock.DistributeLock)@annotation中指定我们自定义注解的全限定类名,如:我的在com.yogurt.mp.lock包下。

表示我们这个AOP切面,在标注了@DistributeLock注解的情况下,才会生效。

整个AOP切面的作用是,在实际方法执行之前,先去获取锁,成功获取到锁后,再调用pjp.proceed()执行实际的方法,在finally块中释放锁,最终返回数据。

测试

使用key的方式

创建Controller,名为GoodsController

/**
 * @author 蜡笔小柯南
 */
@RestController
@RequestMapping("/goods")
public class GoodsController {

    @PostMapping("/test")
    @DistributeLock(scene = "goods", key = "1001")
    public void test(@RequestBody Goods goods) {
        System.out.println("商品ID:" + goods.getId());
        System.out.println("商品名称:" + goods.getGoodsName());
        System.out.println("商品数量:" + goods.getGoodsCount());
        System.out.println("商品价格:" + goods.getPrice());
    }
}

使用@DistributeLock注解,设置scenekey

测试数据入参:

{
  "goodsName": "橙子",
  "goodsCount": 1,
  "price": "3"
}

以debug的方式启动,在redis中会以Hash的形式保存一个键值对,键为:goods#1001,即scene + # + key,在注解中,scene设置的是goods,key设置的是1001,所以最终保存到redis中的key就是goods#1001

在这里插入图片描述

使用key的方式,我们发现key的值,是在标注注解的时候,就写死的,如:@DistributeLock(scene = "goods", key = "1001"),可以看到,key被固定写成了1001,如果所有的请求需要获取同一把锁,只有获取成功才能继续后续处理,这种方式可以实现。

而如果我们需要根据商品的id来锁,比如:多个请求都是要访问商品id为1的数据,要为商品id为1的加锁,谁获取锁成功,谁能继续执行后续逻辑,其余的请求等待锁的释放。这时,一个新请求要访问商品id为2的数据,商品id为2的数据从来没有被访问过,那么这个新的请求就能成功获取到锁,进行执行后续逻辑。

如果使用固定key的方式,就是不管访问哪个商品的数据,都要去竞争同一把锁,即便是请求1要访问商品id为1的数据,请求2要访问商品id为2的数据,在请求1获取锁成功还未释放锁时,请求2只能等待锁的释放。

接下来,使用SpEL表达式的方式实现!

Spel表达式方式

将注解中的key替换为keyExpression

    @PostMapping("/test")
    @DistributeLock(scene = "goods", keyExpression = "#goods.id")
    public void test(@RequestBody Goods goods) {
        System.out.println("商品ID:" + goods.getId());
        System.out.println("商品名称:" + goods.getGoodsName());
        System.out.println("商品数量:" + goods.getGoodsCount());
        System.out.println("商品价格:" + goods.getPrice());
    }

#是SpEL表达式的固定写法,goods.id 表示取 goods 对象中的 id 属性值

goods需要和形参列表中的参数名称保持一致,#goods.id是通过get方法来获取的,要保证对象中有对应属性的get方法

如:

  • #goods.id:需要有getId()方法
  • #goods.goodsName:需要有getGoodsName()方法

测试数据入参,添加id字段

{
  "id": 1,
  "goodsName": "菠萝",
  "goodsCount": 20,
  "price": "66"
}

我们看redis中保存的键值对,key是:goods#1,入参的参数中,id的值为1,所以拼接好最后的key就是goods#1

在这里插入图片描述

如果将入参中id的值修改为2,我们再看redis中是如何保存的

{
  "id": 2,
  "goodsName": "榴莲",
  "goodsCount": 20,
  "price": "666"
}

可以看到,key是:goods#2,根据我们传入id参数的不同,能够动态的改变

在这里插入图片描述

控制台输出结果

商品ID2
商品名称:榴莲
商品数量:20
商品价格:666

两种方式的区别

key的方式:

如果需要获取同一把锁,则可以使用固定key的方式

SpEL表达式方式:

如果需要根据参数的不同,获取不同的锁,可以使用SpEL表达式的方式


现在,你的武器库里又多了一件神器。@DistributedLock注解的强大超乎你的想象!

不管在任何时候,我希望你永远不要害怕挑战,不要畏惧失败。每一个错误都是向成功迈出的一步,每一个挑战都是成长的机会,因为每一次的努力,都会使我们离梦想更近一点。只要你行动起来,任何时候都不算晚。最后,把座右铭送给大家:种一棵树最好的时间是10年前,其次就是现在,加油!共勉 💪。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蜡笔小柯南

多谢投喂,感恩家人

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值