通用分布式锁组件

04-08 1504阅读 0评论

通用分布式锁组件

  • 1 Redisson
    • 1.1介绍
    • 1.2 为什么要使用Redisson实现分布式锁
      • 1.2.1 锁续期的问题
      • 1.2.2 获取锁尝试的问题
      • 1.2.3 可重入问题
      • 1.3 Wath Dog的自动延期机制
      • 1.4 快速了解
      • 1.5 项目集成
      • 2 定义通用分布式锁组件
        • 2.1 实现思路分析
        • 2.2 定义注解
        • 2.3 定义切面
        • 2.4 使用锁
        • 2.5.工厂模式切换锁类型
          • 2.5.1 锁类型枚举
          • 2.5.2 锁对象工厂
          • 2.5.3 改造切面代码
          • 2.6 锁失败策略
            • 2.6.1 策略分析
            • 2.6.2 策略实现
            • 2.7 基于SPEL的动态锁名
              • 2.7.1 SPEL表达式
              • 2.7.2 解析SPEL
              • 2.8 完整代码

                自定义注解实现通用分布式锁组件。

                1 Redisson

                Redisson官网:https://redisson.org/

                1.1介绍

                通用分布式锁组件

                Redisson是一个基于Redis的工具包,可以帮助开发人员更轻松地使用Redis,功能非常强大。将JDK中很多常见的队列、锁、对象都基于Redis实现了对应的分布式版本并提供高级的分布式锁,分布式集合,分布式对象,以及其他的高级Redis功能。

                1.2 为什么要使用Redisson实现分布式锁

                1.2.1 锁续期的问题

                当对业务进行加锁时,锁的过期时间,绝对不能想当然的设置一个值。

                假设线程A在执行某个业务时加锁成功并设置锁过期时间。但该业务执行时间过长,业务的执行时间超过了锁过期时间,那么在业务还没执行完时,锁就自动释放了。

                接着后续线程就可以获取到锁,又来执行该业务。就会造成线程A还没执行完,后续线程又来执行,导致同一个业务逻辑被重复执行。因此对于锁的超时时间,需要结合着业务执行时间来判断,让锁的过期时间大于业务执行时间。

                业务执行时间的影响因素太多了,无法确定一个准确值,只能是一个估值。无法百分百保证业务执行期间,锁只能被一个线程占有。

                如想保证的话,可以在创建锁的同时创建一个守护线程,同时定义一个定时任务每隔一段时间去为未释放的锁增加过期时间。当业务执行完,释放锁后,再关闭守护线程。 这种实现思想可以用来解决锁续期。

                通用分布式锁组件

                1.2.2 获取锁尝试的问题

                我们的项目中, 可能会有这样的情况:

                多个线程竞争获得锁, 同一时刻只有一个线程获得到锁, 其它线程应该尝试获得锁。而我们在使用Redis实现分布式锁的时候,获得不到锁了,就不再尝试获得锁了,而是直接放弃了。

                如果要实现,我们可以采取自旋的方式,同时设置一个超时时间。

                1.2.3 可重入问题

                当一个线程拥有一个锁时,它可以重复获取该锁而不会被自己所持有的锁阻塞。可重入锁通常用于高并发环境中,以保证线程安全性和避免死锁的发生。而我们在使用Redis实现分布式锁的时候,根本没办法重入。

                像这样的问题还有很多,如果要实现一个生产级别,比较完美的分布式锁,是个很耗时耗力的工作。所以工作里面一般不会自己封装分布式锁,如果使用Redis实现分布式锁,一般选择Redisson来实现。

                1.3 Wath Dog的自动延期机制

                刚才提到过,自己实现的锁可能存在锁续期的问题,但是Redission就提供了一种自动延期机制解决了这个问题。

                通用分布式锁组件

                如果拿到分布式锁的节点(微服务)宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。

                Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。

                默认情况下,看门狗的续期时间是30s,也可以通过修改config.lockWatchdogTimeout来另行指定。

                另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。

                1. watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
                2. watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;

                1.4 快速了解

                首先引入依赖:

                
                    org.redisson
                    redisson
                
                

                然后是配置:

                 @Configuration
                 public class RedisConfig {
                    @Bean
                    public RedissonClient redissonClient() {
                        // 配置类
                        Config config = new Config();
                        // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址 
                        config.useSingleServer()
                            .setAddress("redis://192.168.150.101:6379")
                            .setPassword("123456");
                        // 创建客户端
                        return Redisson.create(config);
                    }
                 }
                

                最后是基本用法:

                 @Autowired
                 private RedissonClient redissonClient;
                 @Test
                 void testRedisson() throws InterruptedException {
                    // 1.获取锁对象,指定锁名称
                    RLock lock = redissonClient.getLock("anyLock");
                    try {
                        // 2.尝试获取锁,参数:waitTime、leaseTime、时间单位
                        boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
                        if (!isLock) {
                            // 获取锁失败处理 ..
                        } else {
                            // 获取锁成功处理
                        }
                    } finally {
                        // 4.释放锁
                        lock.unlock();
                    }
                 }
                

                利用Redisson获取锁时可以传3个参数:

                • waitTime:获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
                • leaseTime:锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效。
                • TimeUnit:时间单位

                  1.5 项目集成

                  关键基础配置:

                  import cn.hutool.core.collection.CollectionUtil;
                  import cn.hutool.core.util.StrUtil;
                  import com.tianji.common.autoconfigure.redisson.aspect.LockAspect;
                  import lombok.extern.slf4j.Slf4j;
                  import org.redisson.Redisson;
                  import org.redisson.api.RedissonClient;
                  import org.redisson.config.Config;
                  import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
                  import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
                  import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
                  import org.springframework.boot.context.properties.EnableConfigurationProperties;
                  import org.springframework.context.annotation.Bean;
                  import org.springframework.context.annotation.Configuration;
                  import java.time.Duration;
                  import java.util.ArrayList;
                  import java.util.List;
                  @Slf4j
                  @ConditionalOnClass({RedissonClient.class, Redisson.class})
                  @Configuration
                  @EnableConfigurationProperties(RedisProperties.class)
                  public class RedissonConfig {
                      private static final String REDIS_PROTOCOL_PREFIX = "redis://";
                      private static final String REDISS_PROTOCOL_PREFIX = "rediss://";
                      @Bean
                      @ConditionalOnMissingBean
                      public LockAspect lockAspect(RedissonClient redissonClient){
                          return new LockAspect(redissonClient);
                      }
                      @Bean
                      @ConditionalOnMissingBean
                      public RedissonClient redissonClient(RedisProperties properties){
                          log.debug("尝试初始化RedissonClient");
                          // 1.读取Redis配置
                          RedisProperties.Cluster cluster = properties.getCluster();
                          RedisProperties.Sentinel sentinel = properties.getSentinel();
                          String password = properties.getPassword();
                          int timeout = 3000;
                          Duration d = properties.getTimeout();
                          if(d != null){
                              timeout = Long.valueOf(d.toMillis()).intValue();
                          }
                          // 2.设置Redisson配置
                          Config config = new Config();
                          if(cluster != null && !CollectionUtil.isEmpty(cluster.getNodes())){
                              // 集群模式
                              config.useClusterServers()
                                      .addNodeAddress(convert(cluster.getNodes()))
                                      .setConnectTimeout(timeout)
                                      .setPassword(password);
                          }else if(sentinel != null && !StrUtil.isEmpty(sentinel.getMaster())){
                              // 哨兵模式
                              config.useSentinelServers()
                                      .setMasterName(sentinel.getMaster())
                                      .addSentinelAddress(convert(sentinel.getNodes()))
                                      .setConnectTimeout(timeout)
                                      .setDatabase(0)
                                      .setPassword(password);
                          }else{
                              // 单机模式
                              config.useSingleServer()
                                      .setAddress(String.format("redis://%s:%d", properties.getHost(), properties.getPort()))
                                      .setConnectTimeout(timeout)
                                      .setDatabase(0)
                                      .setPassword(password);
                          }
                          // 3.创建Redisson客户端
                          return Redisson.create(config);
                      }
                      private String[] convert(List nodesObject) {
                          List nodes = new ArrayList(nodesObject.size());
                          for (String node : nodesObject) {
                              if (!node.startsWith(REDIS_PROTOCOL_PREFIX) && !node.startsWith(REDISS_PROTOCOL_PREFIX)) {
                                  nodes.add(REDIS_PROTOCOL_PREFIX + node);
                              } else {
                                  nodes.add(node);
                              }
                          }
                          return nodes.toArray(new String[0]);
                      }
                  }
                  

                  几个关键点:

                  • 这个配置上添加了条件注解@ConditionalOnClass({RedissonClient.class, Redisson.class}) 也就是说,只要引用了配置所在模块,并且引用了Redisson依赖,这套配置就会生效。不引入Redisson依赖,配置自然不会生效,从而实现按需引入。
                  • RedissonClient的配置无需自定义Redis地址,而是直接基于SpringBoot中的Redis配置即可。而且不管是Redis单机、Redis集群、Redis哨兵模式都可以支持

                    2 定义通用分布式锁组件

                    Redisson的分布式锁使用并不复杂,基本步骤包括:

                    • 1)创建锁对象
                    • 2)尝试获取锁
                    • 3)处理业务
                    • 4)释放锁

                      但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多:

                      通用分布式锁组件

                      可以发现,非业务代码格式固定,每次获取锁总是在重复编码。我们可不可以对这部分代码进行抽取和简化呢?

                      2.1 实现思路分析

                      要优化这部分代码,需要通过整个流程来分析:

                      通用分布式锁组件

                      可以发现,只有红框部分是业务功能,业务前、后都是固定的锁操作。既然如此,我们完全可以基于AOP的思想,将业务部分作为切入点,将业务前后的锁操作作为环绕增强。

                      但是,我们该如何标记这些切入点呢?

                      不是每一个service方法都需要加锁,因此我们不能直接基于类来确定切入点;另外,需要加锁的方法可能也较多,我们不能基于方法名作为切入点,这样太麻烦。因此,最好的办法是把加锁的方法给标记出来,利用标记来确定切入点。如何标记呢?

                      最常见的办法就是基于注解来标记了。同时,加锁时还有一些参数,比如:锁的key名称、锁的waitTime、releaseTime等等,都可以基于注解来传参。

                      因此,注解的核心作用是两个:

                      • 标记切入点
                      • 传递锁参数

                        综上,我们计划利用注解来标记切入点,传递锁参数。同时利用AOP环绕增强来实现加锁、释放锁等操作。

                        2.2 定义注解

                        注解本身起到标记作用,同时还要带上锁参数:

                        • 锁名称
                        • 锁等待时间
                        • 锁超时时间
                        • 时间单位
                        • 方法结束是否释放锁
                          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;
                          @Retention(RetentionPolicy.RUNTIME)
                          @Target(ElementType.METHOD)
                          public @interface MyLock {
                              /**
                               * 加锁key的表达式,支持SPEL表达式
                               */
                              String name();
                              /**
                               * 阻塞超时时长,不指定 waitTime 则按照Redisson默认时长
                               */
                              long waitTime() default 1;
                              /**
                               * 锁自动释放时长,默认是-1,其实是30秒 + watchDog模式
                               */
                              long leaseTime() default -1;
                              /**
                               * 时间单位,默认为秒
                               */
                              TimeUnit timeUnit() default TimeUnit.SECONDS;
                              /**
                               * 如果设定了false,则方法结束不释放锁,而是等待leaseTime后自动释放
                               */
                              boolean autoUnlock() default true;
                          }
                          

                          2.3 定义切面

                          接下来,我们定义一个环绕增强的切面,实现加锁、释放锁:

                          package com.tianji.promotion.utils;
                          import lombok.RequiredArgsConstructor;
                          import org.aspectj.lang.ProceedingJoinPoint;
                          import org.aspectj.lang.annotation.Around;
                          import org.aspectj.lang.annotation.Aspect;
                          import org.redisson.api.RLock;
                          import org.redisson.api.RedissonClient;
                          import org.springframework.stereotype.Component;
                          @Component
                          @Aspect
                          @RequiredArgsConstructor
                          public class MyLockAspect implements Ordered{
                              private final RedissonClient redissonClient;
                              @Around("@annotation(myLock)")
                              public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
                              	if (!myLock.autoUnlock() && myLock.leaseTime() 
                                      // 不手动释放锁时,必须指定leaseTime时间
                                      throw new BizIllegalException("leaseTime不能为空");
                                  }
                                  // 1.创建锁对象
                                  RLock lock = redissonClient.getLock(myLock.name());
                                  // 2.尝试获取锁
                                  boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
                                  // 3.判断是否成功
                                  if(!isLock) {
                                      // 3.1.失败,快速结束
                                      throw new BizIllegalException("请求太频繁");
                                  }
                                  try {
                                      // 3.2.成功,执行业务
                                      return pjp.proceed();
                                  } finally {
                                      // 4.释放锁
                                      if (myLock.autoUnlock()) {
                                          lock.unlock();
                                      }
                                  }
                              }
                              /**
                               * 指定切面注解的优先执行顺序
                               * 这里设置锁注解要优先于其他注解执行
                               * (先加锁,再执行事务)
                               * @return
                               */
                              @Override
                              public int getOrder() {
                                  return 0;
                              }
                          }
                          
                              RE_ENTRANT_LOCK, // 可重入锁
                              FAIR_LOCK, // 公平锁
                              READ_LOCK, // 读锁
                              WRITE_LOCK, // 写锁
                              ;
                          }
                          
                              //封装的是方法引用
                              private final Map
                                  this.lockHandlers = new EnumMap redissonClient.getReadWriteLock(name).readLock());
                                  this.lockHandlers.put(WRITE_LOCK, name -> redissonClient.getReadWriteLock(name).writeLock());
                              }
                              public RLock getLock(MyLockType lockType, String name){
                                  //.apply调用方法引用封装的方法
                                  return lockHandlers.get(lockType).apply(name);
                              }
                          }
                          

                          说明:

                          • MyLockFactory内部持有了一个Map,key是锁类型枚举,值是创建锁对象的Function。注意这里不是存锁对象,因为锁对象必须是多例的,不同业务用不同锁对象;同一个业务用相同锁对象。
                          • MyLockFactory内部的Map采用了EnumMap。只有当Key是枚举类型时可以使用EnumMap,其底层不是hash表,而是简单的数组。由于枚举项数量固定,因此这个数组长度就等于枚举项个数,然后按照枚举项序号作为角标依次存入数组。这样就能根据枚举项序号作为角标快速定位到数组中的数据。

                            2.5.3 改造切面代码

                            我们将锁对象工厂注入MyLockAspect,然后就可以利用工厂来获取锁对象了:

                            private final MyLockFactory myLockFactory;
                            RLock lock = myLockFactory.getLock(myLock.lockType(),myLock.name());
                            

                            通用分布式锁组件

                            此时,在业务中,就能通过注解来指定自己要用的锁类型了:

                            通用分布式锁组件

                            2.6 锁失败策略

                            多线程争抢锁,大部分线程会获取锁失败,而失败后的处理方案和策略是多种多样的。目前,我们获取锁失败后就是直接抛出异常,没有其它策略,这与实际需求不一定相符。

                            2.6.1 策略分析

                            接下来,我们就分析一下锁失败的处理策略有哪些。

                            大的方面来说,获取锁失败要从两方面来考虑:

                            • 获取锁失败是否要重试?有三种策略:
                              • 不重试,对应API:lock.tryLock(0, 10, SECONDS),也就是waitTime小于等于0
                              • 有限次数重试:对应API:lock.tryLock(5, 10, SECONDS),也就是waitTime大于0,重试一定waitTime时间后结束
                              • 无限重试:对应API lock.lock(10, SECONDS) , lock就是无限重试
                              • 重试失败后怎么处理?有两种策略:
                                • 直接结束
                                • 抛出异常

                                  对应的API和策略名如下:

                                  通用分布式锁组件

                                  重试策略 + 失败策略组合,总共以下几种情况:

                                  通用分布式锁组件

                                  那么该如何用代码来表示这些失败策略,并让用户自由选择呢?

                                  相信大家应该能想到一种设计模式:策略模式。同时,我们还需要定义一个失败策略的枚举。在MyLock注解中定义这个枚举类型的参数,供用户选择。

                                  注意:

                                  一般的策略模式大概是这样:

                                  • 定义策略接口
                                  • 定义不同策略实现类
                                  • 提供策略工厂,便于根据策略枚举获取不同策略实现

                                    而在策略比较简单的情况下,我们完全可以用枚举代替策略工厂,简化策略模式。

                                    综上,我们可以定义一个基于枚举的策略模式,简化开发。

                                    2.6.2 策略实现

                                    我们定义一个失败策略枚举,直接将失败策略定义到枚举中:

                                    package com.xxx.utils;
                                    import com.xxx.common.exceptions.BizIllegalException;//自定义业务异常
                                    import org.redisson.api.RLock;
                                    public enum MyLockStrategy {
                                        SKIP_FAST(){
                                            @Override
                                            public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
                                                return lock.tryLock(0, prop.leaseTime(), prop.unit());
                                            }
                                        },
                                        FAIL_FAST(){
                                            @Override
                                            public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
                                                boolean isLock = lock.tryLock(0, prop.leaseTime(), prop.unit());
                                                if (!isLock) {
                                                    throw new BizIllegalException("请求太频繁");
                                                }
                                                return true;
                                            }
                                        },
                                        KEEP_TRYING(){
                                            @Override
                                            public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
                                                lock.lock( prop.leaseTime(), prop.unit());
                                                return true;
                                            }
                                        },
                                        SKIP_AFTER_RETRY_TIMEOUT(){
                                            @Override
                                            public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
                                                return lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
                                            }
                                        },
                                        FAIL_AFTER_RETRY_TIMEOUT(){
                                            @Override
                                            public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
                                                boolean isLock = lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
                                                if (!isLock) {
                                                    throw new BizIllegalException("请求太频繁");
                                                }
                                                return true;
                                            }
                                        },
                                        ;
                                        public abstract boolean tryLock(RLock lock, MyLock prop) throws InterruptedException;
                                    }
                                    

                                    然后,在MyLock注解中添加枚举参数:

                                    /**
                                    * 定义锁失败后的策略
                                    * @return
                                    */
                                    MyLockStrategy lockStrategy() default MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT;
                                    

                                    通用分布式锁组件

                                    最后,修改切面代码,基于用户选择的策略来处理:

                                    boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);
                                    

                                    通用分布式锁组件

                                    最后,修改切面代码,基于用户选择的策略来处理:

                                    通用分布式锁组件

                                    这个时候,我们就可以在使用锁的时候自由选择锁类型、锁策略了:

                                    通用分布式锁组件

                                    2.7 基于SPEL的动态锁名

                                    现在还剩下最后一个问题,就是锁名称的问题。

                                    在当前业务中,我们的锁对象本来应该是当前登录用户,是动态获取的。而加锁是基于注解参数添加的,在编码时就需要指定。怎么办?

                                    Spring中提供了一种表达式语法,称为SPEL表达式,可以执行java代码,获取任意参数。

                                    思路:

                                    我们可以让用户指定锁名称参数时不要写死,而是基于SPEL表达式。在创建锁对象时,解析SPEL表达式,动态获取锁名称。

                                    思路很简单,不过SPEL表达式的解析还是比较复杂的。不推荐自己编写。

                                    2.7.1 SPEL表达式

                                    SPEL的表达式语法可以参考官网文档:https://docs.spring.io/spring-framework/docs/3.0.x/reference/expressions.html

                                    中文文档:https://itmyhome.com/spring/expressions.html

                                    首先,在使用锁注解时,锁名称可以利用SPEL表达式,例如我们指定锁名称中要包含参数中的用户id,则可以这样写:

                                    通用分布式锁组件

                                    而如果是通过UserContext.getUser()获取,则可以利用下面的语法:

                                    @MyLock(name="lock:coupon:#{T(com.common.util.UserContext).getUser()}")
                                    

                                    这里T(类名).方法名()就是调用静态方法。

                                    2.7.2 解析SPEL

                                    在切面中,我们需要基于注解中的锁名称做动态解析,而不是直接使用名称:

                                    通用分布式锁组件

                                    其中获取锁名称用的是getLockName()这个方法:

                                    /**
                                     * SPEL的正则规则
                                     */
                                    private static final Pattern pattern = Pattern.compile("\#\{([^\}]*)\}");
                                    /**
                                     * 方法参数解析器
                                     */
                                    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
                                    /**
                                     * 解析锁名称
                                     * @param name 原始锁名称
                                     * @param pjp 切入点
                                     * @return 解析后的锁名称
                                     */
                                    private String getLockName(String name, ProceedingJoinPoint pjp) {
                                        // 1.判断是否存在spel表达式
                                        if (StringUtils.isBlank(name) || !name.contains("#")) {
                                            // 不存在,直接返回
                                            return name;
                                        }
                                        // 2.构建context,也就是SPEL表达式获取参数的上下文环境,这里上下文就是切入点的参数列表
                                        EvaluationContext context = new MethodBasedEvaluationContext(
                                                TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
                                        // 3.构建SPEL解析器
                                        ExpressionParser parser = new SpelExpressionParser();
                                        // 4.循环处理,因为表达式中可以包含多个表达式
                                        Matcher matcher = pattern.matcher(name);
                                        while (matcher.find()) {
                                            // 4.1.获取表达式
                                            String tmp = matcher.group();
                                            String group = matcher.group(1);
                                            // 4.2.这里要判断表达式是否以 T字符开头,这种属于解析静态方法,不走上下文
                                            Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
                                            // 4.3.解析出表达式对应的值
                                            Object value = expression.getValue(context);
                                            // 4.4.用值替换锁名称中的SPEL表达式
                                            name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
                                        }
                                        return name;
                                    }
                                    private Method resolveMethod(ProceedingJoinPoint pjp) {
                                        // 1.获取方法签名
                                        MethodSignature signature = (MethodSignature)pjp.getSignature();
                                        // 2.获取字节码
                                        Class clazz = pjp.getTarget().getClass();
                                        // 3.方法名称
                                        String name = signature.getName();
                                        // 4.方法参数列表
                                        Class[] parameterTypes = signature.getMethod().getParameterTypes();
                                        return tryGetDeclaredMethod(clazz, name, parameterTypes);
                                    }
                                    private Method tryGetDeclaredMethod(Class clazz, String name, Class ... parameterTypes){
                                        try {
                                            // 5.反射获取方法
                                            return clazz.getDeclaredMethod(name, parameterTypes);
                                        } catch (NoSuchMethodException e) {
                                            Class superClass = clazz.getSuperclass();
                                            if (superClass != null) {
                                                // 尝试从父类寻找
                                                return tryGetDeclaredMethod(superClass, name, parameterTypes);
                                            }
                                        }
                                        return null;
                                    }
                                    

                                    2.8 完整代码

                                    MyLockAspect 经过一步步修改与最开始在文章中出现有差异这里给出完整版。

                                    import com.common.utils.StringUtils;
                                    import com.promotion.anno.MyLock;
                                    import lombok.RequiredArgsConstructor;
                                    import org.aspectj.lang.ProceedingJoinPoint;
                                    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.springframework.context.expression.MethodBasedEvaluationContext;
                                    import org.springframework.core.DefaultParameterNameDiscoverer;
                                    import org.springframework.core.Ordered;
                                    import org.springframework.core.ParameterNameDiscoverer;
                                    import org.springframework.expression.EvaluationContext;
                                    import org.springframework.expression.Expression;
                                    import org.springframework.expression.ExpressionParser;
                                    import org.springframework.expression.TypedValue;
                                    import org.springframework.expression.spel.standard.SpelExpressionParser;
                                    import org.springframework.stereotype.Component;
                                    import org.springframework.util.ObjectUtils;
                                    import java.lang.reflect.Method;
                                    import java.util.regex.Matcher;
                                    import java.util.regex.Pattern;
                                    @Component
                                    @Aspect
                                    @RequiredArgsConstructor
                                    public class MyLockAspect implements Ordered {
                                        //    private final RedissonClient redissonClient;
                                        private final MyLockFactory myLockFactory;
                                        @Around("@annotation(myLock)")
                                        public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
                                        	if (!myLock.autoUnlock() && myLock.leaseTime() 
                                                // 不手动释放锁时,必须指定leaseTime时间
                                                throw new BizIllegalException("leaseTime不能为空");
                                            }
                                            // 1.创建锁对象
                                            //RLock lock = redissonClient.getLock(myLock.name());//获取可重入锁
                                            String lockName = getLockName(myLock.name(), pjp);
                                            RLock lock = myLockFactory.getLock(myLock.lockType(),lockName);
                                            // 2.尝试获取锁
                                    //        boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
                                            //使用策略模式获取锁
                                            boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);
                                            // 3.判断是否成功
                                            if (!isLock) {
                                                // 3.1.失败,快速结束(使用策略模式后内部会自己抛异常)
                                                return null;
                                            }
                                            try {
                                                // 3.2.成功,执行业务
                                                return pjp.proceed();
                                            } finally {
                                                // 4.释放锁
                                                if (myLock.autoUnlock()) {
                                                    lock.unlock();
                                                }
                                            }
                                        }
                                        /**
                                         * 指定切面注解的优先执行顺序
                                         * 这里设置要高于其他注解
                                         * @return
                                         */
                                        @Override
                                        public int getOrder() {
                                            return 0;
                                        }
                                        /**
                                         * SPEL的正则规则
                                         */
                                        private static final Pattern pattern = Pattern.compile("\#\{([^\}]*)\}");
                                        /**
                                         * 方法参数解析器
                                         */
                                        private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
                                        /**
                                         * 解析锁名称
                                         * @param name 原始锁名称
                                         * @param pjp 切入点
                                         * @return 解析后的锁名称
                                         */
                                        private String getLockName(String name, ProceedingJoinPoint pjp) {
                                            // 1.判断是否存在spel表达式
                                            if (StringUtils.isBlank(name) || !name.contains("#")) {
                                                // 不存在,直接返回
                                                return name;
                                            }
                                            // 2.构建context,也就是SPEL表达式获取参数的上下文环境,这里上下文就是切入点的参数列表
                                            EvaluationContext context = new MethodBasedEvaluationContext(
                                                    TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
                                            // 3.构建SPEL解析器
                                            ExpressionParser parser = new SpelExpressionParser();
                                            // 4.循环处理,因为表达式中可以包含多个表达式
                                            Matcher matcher = pattern.matcher(name);
                                            while (matcher.find()) {
                                                // 4.1.获取表达式
                                                String tmp = matcher.group();
                                                String group = matcher.group(1);
                                                // 4.2.这里要判断表达式是否以 T字符开头,这种属于解析静态方法,不走上下文
                                                Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
                                                // 4.3.解析出表达式对应的值
                                                Object value = expression.getValue(context);
                                                // 4.4.用值替换锁名称中的SPEL表达式
                                                name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
                                            }
                                            return name;
                                        }
                                        private Method resolveMethod(ProceedingJoinPoint pjp) {
                                            // 1.获取方法签名
                                            MethodSignature signature = (MethodSignature)pjp.getSignature();
                                            // 2.获取字节码
                                            Class[] parameterTypes = signature.getMethod().getParameterTypes();
                                            return tryGetDeclaredMethod(clazz, name, parameterTypes);
                                        }
                                        private Method tryGetDeclaredMethod(Class clazz, String name, Class ... parameterTypes){
                                            try {
                                                // 5.反射获取方法
                                                return clazz.getDeclaredMethod(name, parameterTypes);
                                            } catch (NoSuchMethodException e) {
                                                Class superClass = clazz.getSuperclass();
                                                if (superClass != null) {
                                                    // 尝试从父类寻找
                                                    return tryGetDeclaredMethod(superClass, name, parameterTypes);
                                                }
                                            }
                                            return null;
                                        }
                                    }
                                    

免责声明
本网站所收集的部分公开资料来源于AI生成和互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,1504人围观)

还没有评论,来说两句吧...

目录[+]