Springboot集成Klock简单实现分布式锁

        一个注解就能实现分布式锁?听起来是不是很让人觉得心动呢?又是springboot注解流,傻瓜式实现?等等,简单归简单,还是要看看它是怎么实现的,再看看是否适合我们的业务。

        @Klock也是基于Redisson实现的,先写出依赖:

<dependency>
    <groupId>cn.keking</groupId>
    <artifactId>spring-boot-klock-starter</artifactId>
</dependency>

加载依赖之后,可以看看源码:

@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Klock {
    /**
     * 锁的名称
     * @return name
     */
    String name() default "";
    /**
     * 锁类型,默认可重入锁
     * @return lockType
     */
    LockType lockType() default LockType.Reentrant;
    /**
     * 尝试加锁,最多等待时间
     * @return waitTime
     */
    long waitTime() default Long.MIN_VALUE;
    /**
     *上锁以后xxx秒自动解锁
     * @return leaseTime
     */
    long leaseTime() default Long.MIN_VALUE;

    /**
     * 自定义业务key
     * @return keys
     */
     String [] keys() default {};

     /**
     * 加锁超时的处理策略
     * @return lockTimeoutStrategy
     */
     LockTimeoutStrategy lockTimeoutStrategy() default LockTimeoutStrategy.NO_OPERATION;

    /**
     * 自定义加锁超时的处理策略
     * @return customLockTimeoutStrategy
     */
     String customLockTimeoutStrategy() default "";

     /**
     * 释放锁时已超时的处理策略
     * @return releaseTimeoutStrategy
     */
     ReleaseTimeoutStrategy releaseTimeoutStrategy() default ReleaseTimeoutStrategy.NO_OPERATION;

    /**
     * 自定义释放锁时已超时的处理策略
     * @return customReleaseTimeoutStrategy
     */
     String customReleaseTimeoutStrategy() default "";

}

可以看到有4个常用参数:

name:lock的name。是redis key的重要组成部分,默认为空。 可根据业务指定name, 一般为方法名。防止方法名重名,最好前面加上包名。

lockType:锁的类型,目前支持(可重入锁,公平锁,读写锁)。默认为:可重入锁

waitTime:获取锁最长等待时间。

leaseTime:获得锁后,自动释放锁的时间。

另外还有几个用策略模式写的超时策略。

OK,看下加锁逻辑

核心KlockAspectHandler的主逻辑,AOP切面,拦截注解的方法,织入加锁的逻辑

@Around(value = "@annotation(klock)")
public Object around(ProceedingJoinPoint joinPoint, Klock klock) throws Throwable {
    LockInfo lockInfo = lockInfoProvider.get(joinPoint,klock);
    String curentLock = this.getCurrentLockId(joinPoint,klock);
    currentThreadLock.put(curentLock,new LockRes(lockInfo, false));
    Lock lock = lockFactory.getLock(lockInfo);
    boolean lockRes = lock.acquire();
    //如果获取锁失败了,则进入失败的处理逻辑
    if(!lockRes) {
        if(logger.isWarnEnabled()) {
            logger.warn("Timeout while acquiring Lock({})", lockInfo.getName());
        }
        //如果自定义了获取锁失败的处理策略,则执行自定义的降级处理策略
        if(!StringUtils.isEmpty(klock.customLockTimeoutStrategy())) {
            return handleCustomLockTimeout(klock.customLockTimeoutStrategy(), joinPoint);
        } else {
            //否则执行预定义的执行策略
            //注意:如果没有指定预定义的策略,默认的策略为静默啥不做处理
            klock.lockTimeoutStrategy().handle(lockInfo, lock, joinPoint);
        }
    }
    currentThreadLock.get(curentLock).setLock(lock);
    currentThreadLock.get(curentLock).setRes(true);
    return joinPoint.proceed();
}

LockFacotry看看他获取的锁是个什么锁?

public Lock getLock(LockInfo lockInfo){
    // 还是基于redisson的
    switch (lockInfo.getType()) {
        case Reentrant://@Klock注解的默认的锁类型是ReentrantLock
            return new ReentrantLock(redissonClient, lockInfo);
        case Fair:
            return new FairLock(redissonClient, lockInfo);
        case Read:
            return new ReadLock(redissonClient, lockInfo);
        case Write:
            return new WriteLock(redissonClient, lockInfo);
        default:
            return new ReentrantLock(redissonClient, lockInfo);
    }
}

看看ReentrantLock 的acquire()

public boolean acquire() {
    try {
        rLock = redissonClient.getLock(lockInfo.getName());
        return rLock.tryLock(lockInfo.getWaitTime(), lockInfo.getLeaseTime(), TimeUnit.SECONDS);
    } catch (InterruptedException e) {
        return false;
    }
}

那么key是来自 lockInfo的name;而lockInfo的name来自@Klock的name和keys

LockInfo get(JoinPoint joinPoint, Klock klock) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    LockType type= klock.lockType();
    
    String businessKeyName=businessKeyProvider.getKeyName(joinPoint,klock);
    //锁的名字,锁的粒度就是这里控制的 
    String lockName = LOCK_NAME_PREFIX + LOCK_NAME_SEPARATOR + getName(klock.name(), signature) + businessKeyName;
    long waitTime = getWaitTime(klock);
    long leaseTime = getLeaseTime(klock);
    //如果占用锁的时间设计不合理,则打印相应的警告提示
    if(leaseTime == -1 && logger.isWarnEnabled()) {
        logger.warn("Trying to acquire Lock({}) with no expiration, " +
                    "Klock will keep prolong the lock expiration while the lock is still holding by current thread. " +
                    "This may cause dead lock in some circumstances.", lockName);
    }
    return new LockInfo(type,lockName,waitTime,leaseTime);
}

重点在 String businessKeyName=businessKeyProvider.getKeyName(joinPoint,klock);

public String getKeyName(JoinPoint joinPoint, Klock klock) {
    List<String> keyList = new ArrayList<>();
    Method method = getMethod(joinPoint);
    List<String> definitionKeys = getSpelDefinitionKey(klock.keys(), method, joinPoint.getArgs());
    keyList.addAll(definitionKeys);
    List<String> parameterKeys = getParameterKey(method.getParameters(), joinPoint.getArgs());
    keyList.addAll(parameterKeys);
    return StringUtils.collectionToDelimitedString(keyList,"","-","");
}

        看来不管他lockName的来源花样多么的多,最终还是将注解名和keys拼接起来形成的一个单独key

        虽然确实极为简单的实现了分布式锁,同时也带来了不少的问题,不管key是单个还是多个,都失去了这些key的保护性,只能作为一个非常有局限性的分布式锁实现。

        比如,单独的key形成的lockKey,不同业务方法同一个key也不会争抢锁,造成不必要的阻塞。同一个方法,不同参数,来修改一个hash结构缓存的不同field,可以将field作为二级key,那么不同的field就有不同的lockKey,不会互相影响的业务,就可以不用阻塞了。

        但是如果是订单中,同一个订单中有不同的品名ID的商品,这些品名对应不同的key,在下单的时候,防止超卖,每个品命的库存都需要被保护的,即使其他的业务方法,想修改多个key中的其中的一个key,都不行,那么@Klock的注解实现方式就不可行。

总结:

优点:简单容易实现,还可以自己定义一些超时策略

缺点:对于分布式常见的脑裂(主节点Master挂了,但是数据还未同步到slave)引起的多个客户端都获取到相同的分布式锁数据不一致的问题,毫无防范。

如果你觉得我的博客对你有帮助,请关注我的公众号:砥砺code

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值