Redisson&AOP自定义分布式锁组件
在我们开发业务的时候 难以避免使用使用Redisson
实现分布式锁,Redisson
的分布式锁使用并不复杂,基本步骤包括:
- 1)创建锁对象
- 2)尝试获取锁
- 3)处理业务
- 4)释放锁
但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多:
可以发现,只有红框部分是业务功能,业务前、后都是固定的锁操作。既然如此,我们完全可以基于AOP
的思想,将业务部分作为切入点,将业务前后的锁操作作为环绕增强。
不是每一个service方法都需要加锁,因此我们不能直接基于类来确定切入点;另外,需要加锁的方法可能也较多,我们不能基于方法名作为切入点,这样太麻烦。因此,最好的办法是把加锁的方法给标记出来,利用标记来确定切入点。如何标记呢?
最常见的办法就是基于注解来标记了。同时,加锁时还有一些参数,比如:锁的key名称、锁的waitTime
、releaseTime
等等,都可以基于注解来传参。
实现的流程
-
1.定义注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyLock { String name(); long waitTime() default 1; long leaseTime() default -1; TimeUnit unit() default TimeUnit.SECONDS; }
-
2.定义切面
@Component @Aspect @RequiredArgsConstructor public class MyLockAspect implements Ordered{ private final RedissonClient redissonClient; @Around("@annotation(myLock)") public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable { // 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.释放锁 lock.unlock(); } } @Override public int getOrder() { return 0; } }
注意 Spring中的AOP切面有很多,会按照Order排序,按照Order值从小到大依次执行。Spring事务AOP的order值是Integer.MAX_VALUE,优先级最低。 我们的分布式锁一定要先于事务执行,因此,我们的切面一定要实现Ordered接口,指定order值小于Integer.MAX_VALUE即可。
-
3.定义好了锁注解和切面,接下来就可以改造业务了 下面是没有经过改造的业务代码
String key = "lock:coupon:userId"; RLock lock = redissonClient.getLock(key); try { boolean isLock = lock.tryLock(); if (!isLock){ throw new BizIllegalException("请求频繁"); } IUserCouponService userCouponServiceProxy =( IUserCouponService ) AopContext.currentProxy(); userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null); } finally { lock.unlock(); } ---------------------------------------------------------------- @Transactional //这个注解的事务执行是靠后的 @Override public void checkAndCreateUserCoupon(Long userId, Coupon coupon ,Long serialId)
经过改造以后的代码
//使用redisson + aop 实现 分布式锁的通用组件 IUserCouponService userCouponServiceProxy =( IUserCouponService ) AopContext.currentProxy(); userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null); //这中调用的是代理对象 ---------------------------------------------------------------- @Transactional //这个注解的事务执行是靠后的 @Override @MyLock(name = "lock:coupon:userId") public void checkAndCreateUserCoupon(Long userId, Coupon coupon ,Long serialId){}
经过上面的改造,业务中无需手动编写加锁、释放锁的逻辑了,没有任何业务侵入,使用起来也非常优雅。
不过呢,现在还存在几个问题:
- Redisson中锁的种类有很多,目前的代码中把锁的类型写死了
- Redisson中获取锁的逻辑有多种,比如获取锁失败的重试策略,目前都没有设置
- 锁的名称目前是写死的,并不能根据方法参数动态变化
所以呢,我们接下来还要对锁的实现进行优化,注意解决上述问题。
业务的升级
工厂模式切换锁类型
Redisson中锁的类型有多种,例如:
因此,我们不能在切面中把锁的类型写死,而是交给用户自己选择锁类型。
那么问题来了,如何让用户选择锁类型呢?
锁的类型虽然有多种,但类型是有限的几种,完全可以通过枚举定义出来。然后把这个枚举作为MyLock
注解的参数,交给用户去选择自己要用的类型。
而在切面中,我们则需要根据用户选择的锁类型,创建对应的锁对象即可。但是这个逻辑不能通过if-else
来实现,太low了。
这里我们的需求是根据用户选择的锁类型,创建不同的锁对象。有一种设计模式刚好可以解决这个问题:简单工厂模式。
实现
- 1.我们首先定义一个锁类型枚举
public enum MyLockType {
RE_ENTRANT_LOCK, // 可重入锁
FAIR_LOCK, // 公平锁
READ_LOCK, // 读锁
WRITE_LOCK, // 写锁
;
}
- 2.然后在自定义注解中添加锁类型这个参数
-
3.然后定义一个锁工厂,用于根据锁类型创建锁对象:
@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){ return lockHandlers.get(lockType).apply(name); } }
说明:
MyLockFactory
内部持有了一个Map,key是锁类型枚举,值是创建锁对象的Function。注意这里不是存锁对象,因为锁对象必须是多例的,不同业务用不同锁对象;同一个业务用相同锁对象。MyLockFactory
内部的Map采用了EnumMap
。只有当Key是枚举类型时可以使用EnumMap
,其底层不是hash表,而是简单的数组。由于枚举项数量固定,因此这个数组长度就等于枚举项个数,然后按照枚举项序号作为角标依次存入数组。这样就能根据枚举项序号作为角标快速定位到数组中的数据。
-
4.改造切面类的代码
此时,在业务中,就能通过注解来指定自己要用的锁类型了
锁失败策略
多线程争抢锁,大部分线程会获取锁失败,而失败后的处理方案和策略是多种多样的。目前,我们获取锁失败后就是直接抛出异常,没有其它策略,这与实际需求不一定相符。
- 获取锁失败是否要重试?有三种策略:
- 不重试,对应API:
lock.tryLock(0, 10, SECONDS)
,也就是waitTime小于等于0 - 有限次数重试:对应API:
lock.tryLock(5, 10, SECONDS)
,也就是waitTime大于0,重试一定waitTime时间后结束 - 无限重试:对应API
lock.lock(10, SECONDS)
, lock就是无限重试
- 不重试,对应API:
- 重试失败后怎么处理?有两种策略:
- 直接结束
- 抛出异常
对应的API和策略名如下:
重试策略 + 失败策略组合,总共以下几种情况:
如何用代码来表示这些失败策略,并让用户自由选择呢?
一种设计模式:策略模式。同时,我们还需要定义一个失败策略的**枚举。**在MyLock
注解中定义这个枚举类型的参数,供用户选择。
实现
- 1.定义一个失败策略枚举
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;
}
- 2.在
MyLock
注解中添加枚举参数
- 3.修改切面代码,基于用户选择的策略来处理
我们就可以在使用锁的时候自由选择锁类型、锁策略了: