Redis分布式锁和RedissionLock可重入分布式锁源码解读
Redis分布式锁和RedissionLock可重入分布式锁源码解读
本文主要讲三个部分
1,分布式锁的基本特性
2,设计一个可用的redis分布式锁及会遇到的重要问题和解决办法
3,RedissionLock的lock和unlock的源码分析
分布式锁
在分布式模式下,对一份临界资源需要跨主机跨进程跨线程互斥访问的时候,需要用分布式锁,来保证多进程有序操作
分布式特点
1,互斥性:只能有一个线程拥有该锁
2,锁超时避免死锁:当该线程发生异常,能让其他线程获取
3,容错性,高可用
4,高性能
5,可重入:即当前获取该锁的线程,可以继续获取执行lock获取该锁,每次lock可重入计数+1,unlock可重入计数-1,直到这个值为0,视为彻底释放该锁,参照JUC下ReentrantLock
6,具备阻塞和非阻塞性:当获取不到的线程,能够被及时唤醒
分布式类型:
1,基于数据库的悲观锁:X锁
2,基于数据库的乐观锁:基于版本号
3,基于redis的分布式锁
4,基于zookeeper的分布式锁
注:这篇文章主要讲redis分布式锁,以及具体实现RedissionLock可重入分布式锁源码解读
可重入分布式锁的设计
1,要保证互斥性和可重入性:需要 记录获取该锁的线程信息,例如线程id,以及记录可重入计数(即lock的次数)
2,锁超时避免死锁:需要对redis key设置超时淘汰,但是多长时间淘汰,是个问题,后面讨论
3,容错性,高可用,高性能:基于redis集群,则暂时不用考虑
4,获取不到锁的阻塞线程,需要被及时唤醒:可以基于redis发布订阅,用来通知等待锁的线程
如上设计需要解决的两个问题
1,用记录获取锁的线程id,来实现互斥以及判断可重入,但是在linux下线程id,是不保证唯一的,更何况还要跨主机进程,这个也是需要好好思考的问题
2,避免死锁,需要设置锁超时,那设置多长时间合适?以及设置超时并获取锁之后,在java语言下,还存在因为gc,工作线程会暂定运行,即会存在STW (stop the world)问题导致多线程同时获取锁的异常情况的解决办法
java gc STW (stop the word)导致的锁过期问题
1,工作线程1,获取锁,并设置了超时淘汰时长
2,jvm gc垃圾回收时,会暂停工作线程,即STW
3,当工作线程1恢复工作的时候,由于STW的时长稍长,可能锁已经超时淘汰了,但是该线程还不知道,此时工作线程2去获取,也是能获取到的,导致出现多个线程获取同一个锁的异常问题,如下图所示
大概的解决方案,有:
1: 模拟CAS乐观锁的方式,增加版本号,如下图
2: watch dog自动延期机制,在后面介绍RedissionLock时会介绍
注意:单机版的watch dog 并不能解决 STW的过期问题, 需要分布式版本的 watch dog, 独立的看门狗服务。因为单机版,仍然受gc STW影响。这个问题特别容易被忽略,尽管gc STW时间通常不会太长,至少理论上需要考虑。 锁删除之后, 取消看门狗服务的对应的key记录, 当然,这就使得系统变得复杂, 还要保证看门狗服务的高并发、高可用、数据一致性的问题。
RedissionLock 大致框架逻辑图
锁实现的主要数据结构
1,基于hash数据结构,hash key=lockName,保存了具有唯一性的threadId(例如uuid:threadId结构)和可重入计数,该hash下有且只有一个子元素,hincrby lockName uniqueThreadId,1
2,对lockName设置超时淘汰,pexpire lockName expireTime ,一般30秒超时淘汰,或者自己unlock及时释放
3,基于发布订阅实现watch dog:pulish redisson_lock__channel:{lockName} LockPubSub.UNLOCK_MESSAGE
RedissionLock.lock
1,锁不存在,则设置锁,线程id,和超时淘汰
2,锁存在,可重入,则可重入计数增1
3,获取了锁,则添加watch dog定时线程定时刷新超时淘汰时间,默认每10秒执行一次
4,没获取到锁,则订阅锁释放通知,等待通知
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {//是当前线程获取到锁了,并且会重新设置超时淘汰时间和自增可重入次数
return;
}
//加锁操作失败,订阅消息,利用 redis 的 pubsub 提供一个通知机制来减少不断的重试,避免发生活锁。
//活锁:是指线程 1 可以使用资源,但它很礼貌,让其他线程先使用资源,线程 2 也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源
//在lock中执行订阅,unlock-> unlockInnerAsync 则发布publish redisson_lock__channel:{lockName} LockPubSub.UNLOCK_MESSAGE=0 通知等待的线程去获取锁
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
//当前线程没有拿到锁,先在上面订阅了channel key=redisson_lock__channel:{lockName}
//在如下则,等待在拿到锁的线程unlock之后可重入计数=0时,
//会pulish redisson_lock__channel:{lockName} LockPubSub.UNLOCK_MESSAGE 一个信息,则唤醒等待获取锁的线程去tryAcquire
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
unsubscribe(future, threadId);
}
}
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,//key超时淘汰 30秒=30*1000
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
scheduleExpirationRenewal(threadId);//添加watch dog
}
}
});
return ttlRemainingFuture;
}
加锁lua脚本
1,逻辑调用链:lock->tryAcquire->tryLockInnerAsync
/**
* 1,如果key(lockName)不存在,(key=getLock中参数name),则 hincrby key hash-key,hash-value
* 2,设置key毫秒超时淘汰
*
* 3,如果key存在,并且包含元素hash-key,执行如上1和2,hash-key=commandExecutor.getConnectionManager().getId()+":"+threadId
*
* 4,如果key存在,并且不包含元素hash-key,则获取超时淘汰时间长
* @param waitTime
* @param leaseTime
* @param unit
* @param threadId
* @param command
* @param <T>
* @return
*/
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
lua脚本保证原子性,lua字段解释
1,KEYS[1]:表示你加锁的那个key,比如说RLock lock = redisson.getLock(“myLock”);这里你自己设置了加锁的那个锁key就是“myLock”。
2,ARGV[1]:表示锁的有效期,默认30s
3,ARGV[2]:表示表示加锁的线程ID,类似于:8743c9c0-0795-4907-87fd-6c719a6b4586:1,大致是uuid:threadId,用来避免threadId重复,又可以实现互斥和可重入判断
RedissionLock.unlock
1,可重入计数-1,当可重入计数=0时,则发布一条消息用于通知等待的线程
2,删除watch dog
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<>();
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
cancelExpirationRenewal(threadId);//删除watch dog
if (e != null) {
result.tryFailure(e);
return;
}
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
result.trySuccess(null);
});
return result;
}
lua解锁脚本
/**
* 1,如果不存在key 和hash-key,则直接退出
* 2,存在key和hash-key,则可重入计数减一
* 3,当可重入计数还大于0,则更新一下淘汰时长,还需要继续被同一个线程解锁,因为同一个线程下可重入锁被多次lock
* 4,如果可重入计数等于0,则可重入可以彻底释放了,则删除这个key,并发布订阅,让之前等待该锁的线程进入
* @param threadId
* @return
*/
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " + ////在lock中有订阅这个可以,unlock 则publish redisson_lock__channel:{lockName} LockPubSub.UNLOCK_MESSAGE=0
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
watch dog 逻辑
1,watchDog添加逻辑:lock->tryAcquire->tryAcquireAsync->获取锁之后执行scheduleExpirationRenewal(创建一个定时任务,并把这个定时任务加入EXPIRATION_RENEWAL_MAP)->{lua脚本:存在,则重试淘汰时长,默认30秒}
2,创建HashedWheelTimer.HashedWheelTimeout,internalLockLeaseTime / 3间隔循环执行,默认是30/3=10秒,去刷新超时淘汰时间
3,watchDog删除逻辑:unlock->unlockAsync-> cancelExpirationRenewal(threadId)
注意:单机版的watch dog 并不能解决 STW的过期问题, 需要分布式版本的 watch dog, 独立的看门狗服务。因为单机版,仍然受gc STW影响。这个问题特别容易被忽略,尽管gc STW时间通常不会太长,至少理论上需要考虑
protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
renewExpiration();
}
}
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// reschedule itself
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
设计结构上最大的问题:
1,异步复制,master和slave不一致,master宕机,slave则没有该锁记录,导致多端获取该锁:就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会 异步复制 给对应的master slave实例。但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。此时就会导致多个客户端对一个分布式锁完成了加锁。这时系统在业务上一定会出现问题,导致脏数据的产生。所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁
2,需要设计分布式的watch dog机制以避免GC STW问题,这个问题,在redission中,目前待后面阅读补充