通用分布式锁组件
Redisson的分布式锁使用并不复杂,基本步骤包括:
-
1)创建锁对象
-
2)尝试获取锁
-
3)处理业务
-
4)释放锁
但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多:
可以发现,非业务代码格式固定,每次获取锁总是在重复编码。我们可不可以对这部分代码进行抽取和简化呢?
我们计划利用注解来标记切入点,传递锁参数。同时利用AOP环绕增强来实现加锁、释放锁等操作。
定义注解
注解本身起到标记作用,同时还要带上锁参数:
-
锁名称
-
锁等待时间
-
锁超时时间
-
时间单位
同样定义在util包:
package com.tianji.promotion.utils;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 {String name();long waitTime() default 1;long leaseTime() default -1;TimeUnit unit() default TimeUnit.SECONDS;
}
定义切面
接下来,我们定义一个环绕增强的切面,实现加锁、释放锁:
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 {// 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();}}@Overridepublic int getOrder() {return 0;}
}
使用锁
定义好了锁注解和切面,接下来就可以改造业务了:
可以看到,业务中无需手动编写加锁、释放锁的逻辑了,没有任何业务侵入,使用起来也非常优雅。
不过呢,现在还存在几个问题:
-
Redisson中锁的种类有很多,目前的代码中把锁的类型写死了
-
Redisson中获取锁的逻辑有多种,比如获取锁失败的重试策略,目前都没有设置
-
锁的名称目前是写死的,并不能根据方法参数动态变化
所以呢,我们接下来还要对锁的实现进行优化,注意解决上述问题。
工厂模式切换锁类型
Redisson中锁的类型有多种,例如:
因此,我们不能在切面中把锁的类型写死,而是交给用户自己选择锁类型。
那么问题来了,如何让用户选择锁类型呢?
锁的类型虽然有多种,但类型是有限的几种,完全可以通过枚举定义出来。然后把这个枚举作为MyLock
注解的参数,交给用户去选择自己要用的类型。
而在切面中,我们则需要根据用户选择的锁类型,创建对应的锁对象即可。但是这个逻辑不能通过if-else
来实现,太low了。
这里我们的需求是根据用户选择的锁类型,创建不同的锁对象。有一种设计模式刚好可以解决这个问题:简单工厂模式。
锁类型枚举
我们首先定义一个锁类型枚举:
public enum MyLockType {RE_ENTRANT_LOCK, // 可重入锁FAIR_LOCK, // 公平锁READ_LOCK, // 读锁WRITE_LOCK, // 写锁;
}
然后在自定义注解中添加锁类型这个参数:
锁对象工厂
然后定义一个锁工厂,用于根据锁类型创建锁对象:
package com.tianji.promotion.utils;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.utils.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){return lockHandlers.get(lockType).apply(name);}
}
改造切面代码
我们将锁对象工厂注入MyLockAspect,然后就可以利用工厂来获取锁对象了:
此时,在业务中,就能通过注解来指定自己要用的锁类型了:
锁失败策略
多线程争抢锁,大部分线程会获取锁失败,而失败后的处理方案和策略是多种多样的。目前,我们获取锁失败后就是直接抛出异常,没有其它策略,这与实际需求不一定相符。
策略分析
接下来,我们就分析一下锁失败的处理策略有哪些。
大的方面来说,获取锁失败要从两方面来考虑:
-
获取锁失败是否要重试?有三种策略:
-
不重试,对应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注解中定义这个枚举类型的参数,供用户选择。
注:
一般的策略模式大概是这样:
-
定义策略接口
-
定义不同策略实现类
-
提供策略工厂,便于根据策略枚举获取不同策略实现
而在策略比较简单的情况下,我们完全可以用枚举代替策略工厂,简化策略模式。
综上,我们可以定义一个基于枚举的策略模式,简化开发。
策略实现
我们定义一个失败策略枚举:
package com.tianji.promotion.utils;import com.tianji.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注解中添加枚举参数:
最后,修改切面代码,基于用户选择的策略来处理:
这个时候,我们就可以在使用锁的时候自由选择锁类型、锁策略了: