通用分布式锁组件

通用分布式锁组件

  • 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 快速了解

首先引入依赖:

<!--redisson-->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId>
</dependency>

然后是配置:

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

最后是基本用法:

 @Autowiredprivate RedissonClient redissonClient;@Testvoid 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@ConditionalOnMissingBeanpublic LockAspect lockAspect(RedissonClient redissonClient){return new LockAspect(redissonClient);}@Bean@ConditionalOnMissingBeanpublic 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<String> nodesObject) {List<String> 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() <= 0) {// 不手动释放锁时,必须指定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*/@Overridepublic int getOrder() {return 0;}
}

2.4 使用锁

定义好了锁注解和切面,接下来使用直接加上注解就行了:

在这里插入图片描述

可以看到,业务中无需手动编写加锁、释放锁的逻辑了,没有任何业务侵入,使用起来也非常优雅。

不过呢,现在还存在几个问题:

  • Redisson中锁的种类有很多,目前的代码中把锁的类型写死了
  • Redisson中获取锁的逻辑有多种,比如获取锁失败的重试策略,目前都没有设置
  • 锁的名称目前是写死的,并不能根据方法参数动态变化

所以呢,我们接下来还要对锁的实现进行优化,注意解决上述问题。

2.5.工厂模式切换锁类型

Redisson中锁的类型有多种,例如:

在这里插入图片描述

因此,我们不能在切面中把锁的类型写死,而是交给用户自己选择锁类型。

那么问题来了,如何让用户选择锁类型呢?
锁的类型虽然有多种,但类型是有限的几种,完全可以通过枚举定义出来。然后把这个枚举作为MyLock注解的参数,交给用户去选择自己要用的类型。

而在切面中,我们则需要根据用户选择的锁类型,创建对应的锁对象即可。但是这个逻辑不能通过if-else来实现,太low了。
这里我们的需求是根据用户选择的锁类型,创建不同的锁对象。有一种设计模式刚好可以解决这个问题:简单工厂模式

2.5.1 锁类型枚举

我们首先定义一个锁类型枚举:

public enum MyLockType {RE_ENTRANT_LOCK, // 可重入锁FAIR_LOCK, // 公平锁READ_LOCK, // 读锁WRITE_LOCK, // 写锁;
}

然后在自定义注解中添加锁类型这个参数:

	/*** 使用的锁类型,默认可重入锁* @return*/MyLockType lockType() default MyLockType.RE_ENTRANT_LOCK;

在这里插入图片描述

2.5.2 锁对象工厂

然后定义一个锁工厂,用于根据锁类型创建锁对象:

import com.xxx.enums.MyLockType;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;import java.util.EnumMap;
import java.util.Map;
import java.util.function.Function;import static com.tianji.promotion.enums.MyLockType.*;@Component
public class MyLockFactory {//封装的是方法引用private final Map<MyLockType, Function<String, RLock>> lockHandlers;public MyLockFactory(RedissonClient redissonClient) {this.lockHandlers = new EnumMap<>(MyLockType.class);this.lockHandlers.put(RE_ENTRANT_LOCK, redissonClient::getLock);this.lockHandlers.put(FAIR_LOCK, redissonClient::getFairLock);this.lockHandlers.put(READ_LOCK, name -> 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(){@Overridepublic boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {return lock.tryLock(0, prop.leaseTime(), prop.unit());}},FAIL_FAST(){@Overridepublic 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(){@Overridepublic boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {lock.lock( prop.leaseTime(), prop.unit());return true;}},SKIP_AFTER_RETRY_TIMEOUT(){@Overridepublic boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {return lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());}},FAIL_AFTER_RETRY_TIMEOUT(){@Overridepublic 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() <= 0) {// 不手动释放锁时,必须指定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*/@Overridepublic 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<?> 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;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/799614.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STL之string模拟实现

面试题&#xff1a;简易版string(深拷贝与浅拷贝的问题) 如果要实现简易版的string 无需涉及增容问题&#xff0c;成员变量可以不用存储容量和元素个数 构造函数 错误示范 class string {string(): _str(nullptr){}string(const char* str): _str(str){}char& operator[](s…

Redis数据库:高可用(主从复制、哨兵模式、cluster集群)

目录 前言 一、Redis数据库高可用 二、Redis 主从复制 1、Redis主从复制概述 1.1 Redis主从复制概念 1.2 Redis主从复制的作用 1.3 Redis主从复制的流程 2、搭建Redis主从复制 2.1 环境部署 2.2 主服务器修改配置文件 2.3 从服务器修改配置文件 2.4 测试主从复制效…

负氧离子监测站解析

TH-FZ4防腐木负氧离子监测站&#xff0c;作为一种独特的空气质量监测设备&#xff0c;以其独特的优势在生态环保领域发挥着日益重要的作用。这种监测站不仅具备防腐木材质带来的天然美感与耐久性&#xff0c;更结合了先进的负氧离子监测技术&#xff0c;为环境保护和生态旅游等…

[开源]基于SVM的时间序列预测python代码

整理了SVM的时间序列预测python代码分享给大家。记得点赞哦 #!/usr/bin/env python # coding: utf-8import numpy as np import matplotlib.pyplot as plt import pandas as pd from sklearn import preprocessing from sklearn.metrics import mean_squared_error from math i…

短剧小程序系统开发,让短剧观看与创作更加便捷。短剧系统源码搭建

一、目前短剧发展趋势 1. 市场规模&#xff1a;根据数据来看&#xff0c;2023年中国微短剧市场规模达到了373.9亿元&#xff0c;同比上升了267.65%。预计2024年市场规模将超过500亿元。这一市场规模的增长速度非常显著&#xff0c;显示出短剧行业的巨大潜力和发展前景。 2. 投…

蓝桥杯考前复习三

1.约数个数 由乘法原理可以得出&#xff1a; import java.util.*; public class Main{static int mod (int)1e9 7;public static void main(String[] args){Map<Integer,Integer> map new HashMap<>(); //创建一个哈希表Scanner scan new Scanner(System.in);i…

【会议】Oracle自动化运维峰会

2023年7月21日&#xff0c;杭州。我组织了Oracle自动化运维峰会&#xff0c;大约有20人左右参加会议。以下是会议主题&#xff1a; Oracle自动化运维能力是Oracle 19c自动化运维体系中非常重要的一环&#xff0c;自动化索引、自动化SQL优化、资源隔离等技术能够非常好的提升运维…

Java基于微信小程序的校园外卖平台系统,附源码

博主介绍&#xff1a;✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3…

C++ vector顺序表模拟实现

目录 前言&#xff1a; 模拟实现&#xff1a; 构造函数&#xff1a; 析构函数&#xff1a; 容量调整&#xff08;reserve&#xff09;&#xff1a; resize函数&#xff1a; 尾插&#xff08;push_back&#xff09;: 尾删&#xff08;pop_back&#xff09;: 插入&#xff…

C++ | Leetcode C++题解之第8题字符串转换整数atoi

题目&#xff1a; 题解&#xff1a; class Automaton {string state "start";unordered_map<string, vector<string>> table {{"start", {"start", "signed", "in_number", "end"}},{"signed…

如何判断超级充电测试的性能

超级充电测试是电动汽车充电设备性能评估的重要环节&#xff0c;其性能的好坏直接影响到电动汽车的充电效率和使用寿命。以下是判断超级充电测试性能的几个关键因素&#xff1a;这是衡量超级充电测试性能的最直接指标&#xff0c;充电速度快意味着电动汽车可以在更短的时间内完…

商业分析思维与实践:用数据分析解决商业问题

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【工具大全】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

日期时间相关的类

分界线jdk8 jdk8之前和之后分别提供了一些日期和时间的类&#xff0c;推荐使用jdk8之后的日期和时间类 Date类型 这是一个jdk8之前的类型&#xff0c;其中有很多方法已经过时了&#xff0c;选取了一些没有过时的API //jdk1.8之前的日期 Date Date date new Date(); // 从1970年…

区块链的网络架构有哪些?

区块链技术的兴起正在深刻地改变着互联网的格局。它不仅提供了去中心化、数据透明、难以篡改等优势&#xff0c;还为各种应用场景提供了新的可能性。为了更好地理解区块链&#xff0c;我们需要深入探讨其网络架构。 区块链网络架构主要由以下几个部分组成&#xff1a; 1. 区块…

Web 前端性能优化之五:构建优化

4、构建优化 资源的合并与压缩所涉及的优化点包括两方面&#xff1a;一方面是减少HTTP的请求数量&#xff0c;另一方面是减少HTTP请求资源的大小。 1、HTML 压缩 1、什么是 HTML 压缩 百度首页部分 HTML 源代码 谷歌首页部分 HTML 源代码 虽然这些格式化的字符能带来很好的代…

SpringBoot及其特性

0.前言 Spring 框架提供了很多现成的功能。那么什么是 Spring Boot&#xff1f;使用 Spring 框架&#xff0c;我们可以避免编写基础框架并快速开发应用程序。为了让 Spring 框架提供基础框架&#xff0c;我们需要向 Spring 框架描述有关我们的应用程序及其组件的信息。 不只是…

OpenAI Sora:浅析文生视频模型Sora以及技术原理简介

一、Sora是什么&#xff1f; Sora官方链接&#xff1a;https://openai.com/sora 视频模型领头羊Runway Gen 2、Pika等AI视频工具&#xff0c;都还在突破几秒内的连贯性&#xff0c;而OpenAI&#xff0c;已经达到了史诗级的纪录。 OpenAI&#xff0c;永远快别人一步&#xff0…

C语言面试题之判定字符是否唯一

判定字符是否唯一 实例要求 实现一个算法&#xff0c;确定一个字符串 s 的所有字符是否全都不同 实例分析 1、使用一个大小为 256 的bool数组 charSet 来记录字符是否出现过&#xff1b;2、遍历字符串时&#xff0c;如果字符已经在数组中标记过&#xff0c;则返回 false&a…

Golang 开发实战day08 - Multiple Return values

Golang 教程08 - Multiple Return values 1. Multiple return values 1.1 如何理解多个返回值&#xff1f; Go语言中的多返回值&#xff0c;就像你听了一首歌曲yellow&#xff0c;可以从歌曲里反馈出忧郁和害羞&#xff01;Goland的多个返回值就类似于如此&#xff0c;设定一…

LangChain - OpenGPTs

文章目录 MessageGraph 消息图认知架构AssistantsRAGChatBot 持久化配置新模型新工具astream_events总结 关键链接&#xff1a; OpenGPT GitHub 存储库YouTube 上的 OpenGPT 演练LangGraph&#xff1a;Python、JS 两个多月前&#xff0c;在 OpenAI 开发日之后&#xff0c;我们…