一个注解就能实现分布式锁?听起来是不是很让人觉得心动呢?又是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