Bootstrap

Redisson&AOP自定义分布式锁组件

Redisson&AOP自定义分布式锁组件

在我们开发业务的时候 难以避免使用使用Redisson实现分布式锁,Redisson的分布式锁使用并不复杂,基本步骤包括:

  • 1)创建锁对象
  • 2)尝试获取锁
  • 3)处理业务
  • 4)释放锁

但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多:

img

可以发现,只有红框部分是业务功能,业务前、后都是固定的锁操作。既然如此,我们完全可以基于AOP的思想,将业务部分作为切入点,将业务前后的锁操作作为环绕增强

不是每一个service方法都需要加锁,因此我们不能直接基于类来确定切入点;另外,需要加锁的方法可能也较多,我们不能基于方法名作为切入点,这样太麻烦。因此,最好的办法是把加锁的方法给标记出来,利用标记来确定切入点。如何标记呢?

最常见的办法就是基于注解来标记了。同时,加锁时还有一些参数,比如:锁的key名称、锁的waitTimereleaseTime等等,都可以基于注解来传参。

实现的流程

  • 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中锁的类型有多种,例如:

image-20240723171809350

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

那么问题来了,如何让用户选择锁类型呢?

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

而在切面中,我们则需要根据用户选择的锁类型,创建对应的锁对象即可。但是这个逻辑不能通过if-else来实现,太low了。

这里我们的需求是根据用户选择的锁类型,创建不同的锁对象。有一种设计模式刚好可以解决这个问题:简单工厂模式

实现
  • 1.我们首先定义一个锁类型枚举
public enum MyLockType {
    RE_ENTRANT_LOCK, // 可重入锁
    FAIR_LOCK, // 公平锁
    READ_LOCK, // 读锁
    WRITE_LOCK, // 写锁
    ;
}
  • 2.然后在自定义注解中添加锁类型这个参数

image-20240723165546401

  • 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.改造切面类的代码

image-20240723165811449

此时,在业务中,就能通过注解来指定自己要用的锁类型了

image-20240723165858431

锁失败策略

多线程争抢锁,大部分线程会获取锁失败,而失败后的处理方案和策略是多种多样的。目前,我们获取锁失败后就是直接抛出异常,没有其它策略,这与实际需求不一定相符。


  • 获取锁失败是否要重试?有三种策略:
    • 不重试,对应API:lock.tryLock(0, 10, SECONDS),也就是waitTime小于等于0
    • 有限次数重试:对应API:lock.tryLock(5, 10, SECONDS),也就是waitTime大于0,重试一定waitTime时间后结束
    • 无限重试:对应API lock.lock(10, SECONDS) , lock就是无限重试
  • 重试失败后怎么处理?有两种策略:
    • 直接结束
    • 抛出异常

对应的API和策略名如下:

dffbd78d-c4d1-47ba-a3ae-dff00b145bc9

重试策略 + 失败策略组合,总共以下几种情况:

c2138c80-5cde-4503-8fe0-3f7692d01941

如何用代码来表示这些失败策略,并让用户自由选择呢?

一种设计模式:策略模式。同时,我们还需要定义一个失败策略的**枚举。**在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注解中添加枚举参数

image-20240723170520528

  • 3.修改切面代码,基于用户选择的策略来处理

image-20240723170614355

我们就可以在使用锁的时候自由选择锁类型、锁策略了:

image-20240723170701386

;