redisson分布式锁原理剖析

​ 相信使用过redis的,或者正在做分布式开发的童鞋都知道redisson组件,它的功能很多,但我们使用最频繁的应该还是它的分布式锁功能,少量的代码,却实现了加锁、锁续命(看门狗)、锁订阅、解锁、锁等待(自旋)等功能,我们来看看都是如何实现的。

加锁

//获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
//加分布式锁
redissonLock.lock();

根据redissonLock.lock()方法跟踪到具体的private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId)方法,真正获取加锁的逻辑是在tryAcquireAsync该方法中调用的tryLockInnerAsync()方法,看看这个方法是怎么实现的?

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime); return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 判断是否存在分布式锁,getName()也就是KEYS[1],也就是锁key名
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 加锁,执行hset 锁key名 1
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
// 设置过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 这个分支是redisson的重入锁逻辑,锁还在,锁计数+1,重新设置过期时长
"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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

发现底层是结合lua脚本实现了加锁逻辑。

为什么底层结合了Lua脚本?

Redis是在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到redis执行。使用脚本的好处如下:

1、减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑,可以一次性放到redis中执行,较少了网络往返时延。这点跟管道有点类似

2、原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过

redis的批量操作命令(类似mset)是原子的

也就意味着虽然脚本中有多条redis指令,那即使有多条线程并发执行,在同一时刻也只有一个线程能够执行这段逻辑,等这段逻辑执行完,分布式锁也就获取到了,其它线程再进来就获取不到分布式锁了。

锁续命(自旋)

​ 大家都听过锁续命,肯定也知道这里涉及到看门狗的概念。在调用tryLockInnerAsync()方法时,第一个参数是commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()也就是默认的看门狗过期时间是private long lockWatchdogTimeout = 30 * 1000毫秒。

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// 添加监听器,判断获取锁是否成功,成功的话,添加定时任务:定期更新锁过期时间
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
// 根据tryLockInnerAsync方法,加锁成功,return nil 也就是null
Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
// 添加定时任务:定期更新锁过期时间
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}

​ 当线程获取到锁后,会进入if (ttlRemaining == null)分支,调用定期更新锁过期时间scheduleExpirationRenewal方法,我们看看该方法实现:

private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
} Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception { RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 检测KEYS[1]锁是否还在,在的话再次设置过期时间
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}
// 通过上面lua脚本执行后会返回1,也就true,再次调用更新过期时间进行续期
if (future.getNow()) {
// reschedule itself
scheduleExpirationRenewal(threadId);
}
}
});
}
// 延迟 internalLockLeaseTime / 3再执行续命
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
task.cancel();
}
}

​ 发现scheduleExpirationRenewal方法只是用了Timeout作为任务,并没有使用java的Timer()之类的定时器,而是在Timeout任务run()方法中定义了RFuture对象,通过给RFuture对象设置listener,在listener中通过Lua脚本执行结果进行判断是否还需要进行续期。通过这样的方式来给分布式锁进行续期。

​ 这种方式实现定时更新确实很巧妙,定期时间很灵活。

锁订阅及锁等待

​ 锁订阅是针对那些没有获取到分布式锁的线程而言的。来看看整个获取锁的方法:

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired,获取到锁,直接退出
if (ttl == null) {
return;
}
// 没有获取到锁,进行订阅
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future); try {
while (true) {
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
} // waiting for message
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}

​ 当第一个线程获取到锁后,会在if (ttl == null)分支进行返回,第二个及以后的线程进来在没获取到锁时,只能接着走下面的逻辑,进行锁的订阅。

​ 接着进入到一个while循环,首先还是会进行一次尝试获取锁(万一此时第一个线程已经释放锁了呢),通过tryAcquire(leaseTime, unit, threadId)方法,如果没有获取到锁的话,会返回锁的剩余过期时间,如果剩余过期时间大于0,则当前线程通过Semaphore信号号,将当前线程阻塞,底层执行LockSupport.parkNanos(this, nanosTimeout)线程挂起剩余过期时间后,会自动进行唤醒,再次执行tryAcquire尝试获取锁。所有没有获取到锁的线程都会执行这个流程。

一定要等待剩余过期时间后才唤醒吗?

​ 假设线程一获取到锁,过期时间默认为30s,当前执行业务逻辑已经过了5s,那其他线程走到这里,则需要等待25s后才行进行唤醒,那万一线程一执行业务逻辑只要10s,那其他线程还需要等待20s吗?这样岂不是导致效率很低?

​ 答案是否定的,详细看解锁逻辑。

解锁

​ 解锁:redissonLock.unlock();

​ 我们来看看具体的解锁逻辑:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 锁不存在,发布unlockMessage解锁消息,通知其他等待线程
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 不存在该锁,异常捕捉
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// redisson可重入锁计数-1,依旧>0,则重新设置过期时间
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
// redis删除锁,发布unlockMessage解锁消息,通知其他等待线程
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId)); }

​ 发现解锁逻辑底层也是用了一个lua脚本实现。具体的说明可以看代码注释,删除锁后,并发布解锁消息,通知到其它线程,也就意味着不会其它等待的线程一直等待。

Semophore信号量的订阅中有个onMessage方法,

protected void onMessage(RedissonLockEntry value, Long message) {
// 唤醒线程
value.getLatch().release(message.intValue()); while (true) {
Runnable runnableToExecute = null;
synchronized (value) {
Runnable runnable = value.getListeners().poll();
if (runnable != null) {
if (value.getLatch().tryAcquire()) {
runnableToExecute = runnable;
} else {
value.addListener(runnable);
}
}
} if (runnableToExecute != null) {
runnableToExecute.run();
} else {
return;
}
}
}

解锁后通过if (opStatus)分支取消锁续期逻辑。

总结:

​ 总的来说,可以借助一张图加深理解:

​ 分布式锁的整体实现很巧妙,借助lua脚本的原子性,实现了很多功能,当然redisson还有其它很多功能,比如为了解决主从集群中的异步复制会导致锁丢失问题,引入了redlock机制,还有分布式下的可重入锁等。

redisson分布式锁原理剖析的更多相关文章

  1. 又长又细,万字长文带你解读Redisson分布式锁的源码

    前言 上一篇文章写了Redis分布式锁的原理和缺陷,觉得有些不过瘾,只是简单的介绍了下Redisson这个框架,具体的原理什么的还没说过呢.趁年前项目忙的差不多了,反正闲着也是闲着,不如把Rediss ...

  2. redisson实现分布式锁原理

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

  3. Redisson 实现分布式锁原理分析

    Redisson 实现分布式锁原理分析   写在前面 在了解分布式锁具体实现方案之前,我们应该先思考一下使用分布式锁必须要考虑的一些问题.​ 互斥性:在任意时刻,只能有一个进程持有锁. 防死锁:即使有 ...

  4. Java进阶专题(二十五) 分布式锁原理与实现

    前言 ​ 现如今很多系统都会基于分布式或微服务思想完成对系统的架构设计.那么在这一个系统中,就会存在若干个微服务,而且服务间也会产生相互通信调用.那么既然产生了服务调用,就必然会存在服务调用延迟或失败 ...

  5. Redisson分布式锁实现

    转: Redisson分布式锁实现 2018年09月07日 15:30:32 校长我错了 阅读数:3303   转:分布式锁和Redisson实现 概述 分布式系统有一个著名的理论CAP,指在一个分布 ...

  6. Redis分布式锁原理

    1. Redis分布式锁原理 1.1. Redisson 现在最流行的redis分布式锁就是Redisson了,来看看它的底层原理就了解redis是如何使用分布式锁的了 1.2. 原理分析 分布式锁要 ...

  7. Redisson 分布式锁实战与 watch dog 机制解读

    Redisson 分布式锁实战与 watch dog 机制解读 目录 Redisson 分布式锁实战与 watch dog 机制解读 背景 普通的 Redis 分布式锁的缺陷 Redisson 提供的 ...

  8. Redisson 分布式锁实现之前置篇 → Redis 的发布/订阅 与 Lua

    开心一刻 我找了个女朋友,挺丑的那一种,她也知道自己丑,平常都不好意思和我一块出门 昨晚,我带她逛超市,听到有两个人在我们背后小声嘀咕:"看咱前面,想不到这么丑都有人要." 女朋友 ...

  9. Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端

    开心一刻 一男人站在楼顶准备跳楼,楼下有个劝解员拿个喇叭准备劝解 劝解员:兄弟,别跳 跳楼人:我不想活了 劝解员:你想想你媳妇 跳楼人:媳妇跟人跑了 劝解员:你还有兄弟 跳楼人:就是跟我兄弟跑的 劝解 ...

  10. 利用多写Redis实现分布式锁原理与实现分析(转)

    利用多写Redis实现分布式锁原理与实现分析   一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子:场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能 ...

随机推荐

  1. 为开源提 PR

    PR 可让你在 GitHub 上向他人告知你已经推送到存储库中分支的更改. 在 PR 打开后,你可以与协作者讨论并审查潜在更改,在更改合并到基本分支之前添加跟进提交. 为什么 PR 使用 PR 的主要 ...

  2. day40-网络编程02

    Java网络编程02 4.TCP网络通信编程 基本介绍 基于客户端--服务端的网络通信 底层使用的是TCP/IP协议 应用场景举例:客户端发送数据,服务端接收并显示控制台 基于Scoket的TCP编程 ...

  3. 【gRPC】C++异步服务端优化版,多服务接口样例

    官方的C++异步服务端API样例可读性并不好,理解起来非常的费劲,各种状态机也并不明了,整个运行过程也容易读不懂,因此此处参考网上的博客进行了重写,以求顺利读懂. C++异步服务端实例,详细注释版 g ...

  4. es,logstash各版本对应要求的JDK版本,操作系统对应示意图

    官网地址:https://www.elastic.co/cn/support/matrix

  5. 在CentOS 8服务器上搭建FastDFS环境

    什么是FastDFS? 这里,我就摘录下百度百科上对于FastDFS的描述. FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储.文件同步.文件访问(文件上传.文件下 ...

  6. C/C++ 宏定义

    宏定义(无参宏定义和带参宏定义) 宏定义是比较常用的预处理指令,即使用"标识符"来表示"替换列表"中的内容.标识符称为宏名,在预处理过程中,预处理器会把源程序中 ...

  7. 魔改xxl-job,彻底告别手动配置任务!

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 哈喽大家好啊,我是Hydra. xxl-job是一款非常优秀的任务调度中间件,轻量级.使用简单.支持分布式等优点,让它广泛应用在我们的项目中,解 ...

  8. TensorFlow搭建模型方式总结

    引言 TensorFlow提供了多种API,使得入门者和专家可以根据自己的需求选择不同的API搭建模型. 基于Keras Sequential API搭建模型 Sequential适用于线性堆叠的方式 ...

  9. BigDecimal的运算——加减乘除

    BigDecimal的运算--加减乘除 1.初始化(尽量用字符串的形式初始化) BigDecimal num12 = new BigDecimal("0.005"); BigDec ...

  10. 如何用Virtualbox搭建一个虚拟机

    序言 各位好啊,我是会编程的蜗牛,作为java开发者,我们肯定会接触Linux服务器,除了使用云服务搭建Linux服务器外,我们一般也可以在自己的电脑上安装虚拟机来搭建Linux服务器用于各种功能的验 ...