java 篇: 1.基础地基 2.设计原理 3.项目实战
分布式锁集群下的琐失效问题:
开启多个实例,模拟集群环境

记得在虚拟机选项当中把端口改了,把 local 去掉

然后三个服务启动

全部启动后,就注册到 Nacos 当中了,负载均衡,每刷新一次,一个服务后台就有日志了,轮着来


虚拟机地址,端口号是网关,最后记得加上服务的前缀

网关接收的是一个 token,不再是用户 id 了

复制粘贴一下这个 TOKEN

理论上是这样的结果

但是我 jmter 测试通过,但是数据库当中没有数据,不知道为何,先过。

分布式锁简单分布式锁原理:




如果加锁后,服务宕机了,那其他线程也拿不到锁了,所以需要加过期时间,并且加锁和过期时间得是原子性的。


setnx 不能设置过期时间,所以使用 set lock t1 nx ex 20

代码实现:
先建个类

实现获取锁和释放锁两个功能


好了,测试一下,这里我就不测了。
分布式锁的问题及 Redisson 简介:








不需要给 redis 和 Redisson 各配一套地址,只需要配置 Redis 就行了,Redisson 会按照 Redis 的方式读取,并完成自己的配置。
@ConditionalOnClass({RedissonClient.class, Redisson.class})
只有当 RedissonClient 和 Redisson 两个类同时存在于类路径(classpath)中时,被标注的配置类或 Bean 才会生效。

waitTime 重试时间(在这时间内不停尝试获取锁),leaseTime 锁释放时间
在 common 包的 pom.xml 下,已经有 redisson 的依赖

但是加了一个 scope,里面是 provided,意思是这个依赖,将来在打包的时候不会打包进去,将来是需要调用方提供的,谁引用了 common,谁需要提供。那这里引入,是因为让 RedissonConfig 这个自动装配的类不报错,它需要 redisson 相关东西,这样代码编译可以通过。
但因为加了 scope,打包的时候没有,那下面这句的条件就是不成立的,所以需要手动引入 Redisson 依赖,才能触发这个条件。

所以我们需要在调用它的 promotion 模块的 pom.xml 文件种手动引入依赖

改造代码:



参数支持无参、两参、三参,无参 l1 默认为 30s
这里采用无参


这里还有可以改进的地方,你看它无论有木有获取锁,最终都会释放锁。

现在好了
后面可以进行测试了
Redisson 的 lock.unlock(),内部已经用 LUA 脚本做了很多复杂的判断。并且自带看门狗机制,测试一下

用 sleep 模拟业务超时。开一个实例,用一个线程测就行了,

这里就显示阻塞卡住了

每过 10s(30/3)重置有效期为 30s
还支持可重入,存储结构为哈希,key(UUID+ 线程 id)就是线程标识,value 重入的次数,将来同一线程多次来获取,就 +1,释放锁就-1,到 0 就删除了
分布式锁基于自定义注解改造分布式锁:


① 自定义注解:


重试时间设置为 1s,过期设置为-1 默认,用看门狗。
@Retention()是用来标记注解的作用时间的,是运行期间呢,还是源码期间呢,还是字节码期间,这里是运行期间,@Target()标记作用目标,这里标记某个方法
②:定义环绕增强通知:
传入参数,一个切入点,一个切入点标记

别忘了加上

③:配置生效


当然也可以直接在 myLockAspect 类当中加 @Component 注解
改下业务代码:

这样其实还不能解决问题,因为事务注解先执行,然后锁再执行,那锁先释放,然后事务再提交,就又出现之前的问题了。所以得让锁注解先执行。


默认最大。值越小,越先执行。这里设置为 0,就行

分布式锁简单工厂模式改造分布式锁:
这里就固定了 reetrantLock 锁,但还有其他锁,为了通用性,需要进行改造。

先定义一个枚举类


reentrantLock、fairLock(公平锁)、readLock(读锁)、writeLock(写锁)

注解当中加入,之后对切面代码改造一下,

这里可以用工厂模式


目前就是把刚才的代码,换了个地方写
一个类型,对应一个行为,即对应一个操作,即对应一个方法,即对应一个函数。这个函数入参是字符串,返回值是 RLock


对切面代码进行改造:

把 config 里面注释掉,不再用 RedssonClient 了,用 MyLockFactory 了,上面已经加了 Component 了

分布式锁策略模式改造分布式锁:
用 if-else 或者 swith 来判断,从执行上来讲是个比较简单的逻辑,那现在呢,是基于 map 去 get 一个具体的 key,需要对 key 进行哈希运算,然后对数组取模,找到数组脚标。这样执行的流程就变复杂,性能就下降了

这里换成 EnumMap,不需要传入大小,只需要传入 MyLockType.class,它会根据传入的枚举类当中的枚举项确定大小的,也不用动态扩容了。而且枚举项是固定,那是不是可以对其进行编号,拿编号找对应的脚标就行
工厂改造完成后完整代码:

由于场景不同,处理逻辑也不同,有些是没获取到锁,就直接 return 了,有些是不停尝试,直到成功。

获取锁有多种方式,失败处理也有多种方式。组合起来就是获取锁的处理策略。

传统策略模式写法:
先去定义一个接口,里面声明获取锁的方法,然后根据五个策略,写五个实现类,每个实现类按照刚才分析的思路编写对应的逻辑,那还需要五个对象提前创建出来,跟锁工厂一样,整到一个 Map 当中。接着定义一个策略对应的枚举,封装到自定义注解当中,由用户选择具体哪个枚举,也就是对应的策略。然后再从工厂里取出对应的策略的实现类调用里面的方法。实现复杂
代码实现,在枚举类当中,实现相应的业务:


抽象接口,waitTime、leaseTime 从注解当中拿,prop 为 properity 简写

类似于匿名内部类
完整代码:
package com.tianji.promotion.utils;
import com.tianji.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.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;
}
改为 public ,方便调用
注解当中添加,默认是抛异常

那这样用户就可以自己选策略,当然不选,就用默认的

切面改造完成:

本来切面就是把锁逻辑和业务代码分开,然后锁类型又用工厂模式,又和切面类分开,现在锁的重试逻辑又分开了,每个类只干自己的事情,里面不能存在其他无关逻辑
分布式锁 SPEL 表达式动态锁名称:
用 SPEL 表达式实现动态锁名称:


得有个办法动态解析表达式,并且从参数列表当中寻找,替换
这个在 common 包当中已经写好了

这里换成 common 包当中的 Lock 就行了,开始测试

异步领券优化思路:


那返回什么消息给用户呢,所以我们必须把领券资格的判断放在发消息之前

那放到前面,执行一大串,又得考虑性能问题,所以得用 Redis 缓存




下面进行相关代码改造:




对于组织数据,有两种方案,第一种直接用 BeanUtil 将 coupon 转 map,因为 coupon 数据很多,但是我们这只要四个就够了,所以这里采用手动比较合适

这里不能直接 toString,转过去的时间结构可能不是我们想要的,这里采用把它转成毫秒值,就是从 1970 年开始的那个毫秒值,有两个好处,精度更准确,占用空间小。用到 DateUtils
最后别忘了加上事务


进行测试:





缓存没了
异步领券基于 Redis 的领券和消息发送:

导入 dto 类:


mq 常量

添加交换机:

添加路由键:

接下来进行改造代码:

.increment 增加以及返回增加后的结果,原子性操作
下面在 Promotion 模块下搞监听器

导入依赖








下面就可以进行测试了,这里就不测了。
练习 1 兑换码异步兑换的思路分析:



这里其实不需要保存所有兑换码 id,因为自增长是连续的,只需要记录每一个优惠券的起始范围


在这过程中多次向 Redis 发送请求,多次网络交互,性能会受到网络的影响,所以可以用 LUA 脚本。
代码改造:








如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥