Long userId = UserContext.getUser();
String key="lock:coupon:uid:"+userId;
RLock lock = redissonClient.getLock(key);
//尝试获取锁
boolean success = lock.tryLock();
//获取锁失败直接抛异常
if (!success){
throw new BadRequestException("请求频繁!");
}
try {
//使用代理对象来调用此方法,避免事务失效
IUserCouponService userCouponService =(IUserCouponService)AopContext.currentProxy();
//检验是否超领,并且生成用户券
userCouponService.checkAndCreateCoupon(coupon, userId);
} finally {
lock.unlock();
@Transactional
@Override
public void checkAndCreateCoupon(Coupon coupon,Long userId){
//4.是否超出限领数量
Integer count = lambdaQuery()
.eq(UserCoupon::getUserId,userId)
.eq(UserCoupon::getCouponId, coupon.getId()).count();
if (count!=null&&count>=coupon.getUserLimit()){
throw new BadRequestException("用户超领!");
}
//优惠券已发放数量+1
int num = couponMapper.incrIssueNum(coupon.getId());
if (num==0){
throw new BadRequestException("优惠券库存不足!");
}
//生成用户券
UserCoupon userCoupon=new UserCoupon();
userCoupon.setUserId(userId);
userCoupon.setCouponId(coupon.getId());
userCoupon.setStatus(UserCouponStatus.UNUSED);
LocalDateTime issueBeginTime = coupon.getIssueBeginTime();
LocalDateTime issueEndTime = coupon.getIssueEndTime();
//判断是否是以天数为有效期
if (issueBeginTime==null){
LocalDateTime now = LocalDateTime.now();
issueBeginTime= now;
issueEndTime=issueBeginTime.plusDays(coupon.getTermDays());
}
userCoupon.setTermBeginTime(issueBeginTime);
userCoupon.setTermEndTime(issueEndTime);
save(userCoupon);
}
序言
在上述代码中,我们使用redisson构建了简单分布式锁,勉强解决了在集群环境用户抢券发生的一系列并发问题,但是我们并不能动态获取锁。
Redisson的分布式锁使用并不复杂,基本步骤包括:
-
1)创建锁对象
-
2)尝试获取锁
-
3)处理业务
-
4)释放锁
但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多:
可以发现,非业务代码格式固定,每次获取锁总是在重复编码。我们可不可以对这部分代码进行抽取和简化呢?
所以,我们可以采用aop切面编程模式来抽取与业务代码无关并且可能出现重复的代码,
可是,我们需要一个切入点,最常见的办法,就是使用注解的方式
于是
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;
//锁类型
MyLockType lockType() default MyLockType.RE_ENTRANT_LOCK;
//失败策略
MyLockStrategy myLockStrategy() default MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT;
}
2.锁类型-枚举
public enum MyLockType {
RE_ENTRANT_LOCK,
FAIR_LOCK,
READ_Lock,
WRITE_LOCK;
}
3.自定义切面
@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered {
private final MyLockFactory myLockFactory;
@Around("@annotation(myLock)")
public Object tryLock(ProceedingJoinPoint pjp,MyLock myLock) throws Throwable {
//创建锁对象
RLock lock = myLockFactory.getLock(myLock.lockType(), myLock.name());
//尝试获取锁
boolean isLock = myLock.myLockStrategy().tryLock(lock,myLock);
//判断锁是否获取成功
if (!isLock){
return null;
}
try {
//执行业务
return pjp.proceed();
} finally {
//释放锁
lock.unlock();
}
}
//排序,防止使我们的切面比事务的优先级高,保证在事务提交之后,再释放锁,避免锁失效问题
@Override
public int getOrder() {
return 0;
}
}
除此之外,创建锁对象使,我们采用工厂模式,保证我们在打上注解时,可以随意指定哪种模式的锁;
例如redisson中的锁大概分为:
1.可重入锁
2.公平锁
3.读写锁
所以
4.创建锁-工厂模式
@Component
public class MyLockFactory {
private final Map<MyLockType,Function<String,RLock>> lockHandler;
public MyLockFactory(RedissonClient redissonClient) {
this.lockHandler=new EnumMap<>(MyLockType.class);
lockHandler.put(RE_ENTRANT_LOCK, redissonClient::getLock);
lockHandler.put(FAIR_LOCK,redissonClient::getFairLock);
lockHandler.put(READ_Lock,name->redissonClient.getReadWriteLock(name).readLock());
lockHandler.put(WRITE_LOCK,name->redissonClient.getReadWriteLock(name).writeLock());
}
public RLock getLock(MyLockType myLock,String name){
return lockHandler.get(myLock).apply(name);
}
}
接下来就是获取锁,常规获取锁需要3个参数:
-
waitTime:获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
-
leaseTime:锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效。
-
TimeUnit:时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
常规情况下,我们还需要写获取锁成功或者失败的逻辑,而我们可以将这些东西封装成一个失败策略,于是:
5.获取锁-策略模式
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 b = lock.tryLock(0, prop.leaseTime(), prop.unit());
if (!b){
throw new BizIllegalException("操作频繁!");
}
return true;
}
},
KEEP_TRYING(){
//无限重试
@Override
public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
boolean b = lock.tryLock( 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 b = lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
if (!b){
throw new BizIllegalException("操作频繁!");
}
return true;
}
} ;
public abstract boolean tryLock(RLock lock, MyLock prop) throws InterruptedException;
}
最后,在我们的切入点打上注解(可以指定失败策略,锁类型)