Redisson分布锁原理分析及源码解读

news2024/11/18 13:28:59

本文源码解读基于Redisson 3.18.0 版本

Redisson分布锁实现原理

Redisson锁实现基本原理大致如下图所示:

客户端执行Lua脚本去获取锁,如果获取失败,则订阅解锁消息,并挂起线程。

客户端解锁时执行一段Lua脚本,删除锁的同时往解锁消息通道发送解锁指令,Redis会广播解锁消息到所有订阅的客户端。

当客户端收到解锁消息或者线程挂起时间超过锁超时时间(leaseTime)时,客户端会重新尝试获取分布式锁,如果仍然获不到,则线程再度进入阻塞状态,等待解锁消息到达或者锁超时。

如果等待时间超出了最大可等待时间(waitTime),会直接返回锁获取失败。

无论成功还是失败,当前客户端线程最后都会取消订阅解锁消息。

Redisson客户端加锁流程

1.尝试获取分布式锁

以tryLock(long waitTime, long leaseTime, TimeUnit unit)方法为例:

  • waitTime 获取锁时最大等待时间
  • leaseTime 获得锁后,设置的锁超时时间

tryLock方法为尝试获取锁的方法入口,返回ttl表示锁剩余超时时间,如果返回null,表示无人占有锁,当前线程获取锁成功,如果获取锁成功,客户端会启动一个看门狗线程,来自动为锁续期(后面会讲到看门狗作用)。tryLock方法过程时序图如下:

核心为图中红色部份的tryLockInnerAsync方法,他通过evalWriteAsync执行了一段lua脚本如下.

  • KEYS[1]:锁名称
  • ARGV[1]:锁过期时间
  • ARGV[2]:客户端唯一标识(Client UUID+threadId).
# 如果锁不存在 
if (redis.call('exists', KEYS[1]) == 0) then
    # hash结构,锁名称为key,线程唯一标识为itemKey,itemValue为一个计数器。支持相同客户端线程可重入,每次加锁计数器+1.
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    # 设置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    # 成功获取锁返回null
    return nil;
end ;
#如果是当前线程占有分布式锁,允许重入锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    # 将锁重入计数器自增1.
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    # 设置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    # 成功获取锁返回null
    return nil;
end ;
#如果获取不到锁,返回锁剩余过期时间
return redis.call('pttl', KEYS[1]);
复制代码

2.获取锁失败,订阅解锁消息channel

图1处:判断获取锁是否等待(waitTime)超时,如果等待超时则直接返回获取锁失败。

图2处:如果等待未超时,则尝试订阅解锁channel。

图3处:获取ReissonLockEntry(获取成功表示订阅成功),超时时长设置为当前剩余的等待时间(waitTime)。 如果获取ReissonLockEntry超时,终止并取消解锁消息channel订阅,获取锁失败。

3.收到解锁通知,重新竞争锁

此处的代码紧接上面,因为图片太大故切成两块了:

这里主要分析红色圆圈处的while代码块。这里的while代码是一个循环尝试,直到获取锁成功或者超时失败。 解锁消息是广播给所有锁竞争的客户端的,收到解锁消息后,所有的客户端进程都会有一个线程去重新竞争锁。

图1处:和第一步获取锁的代码一模一样,尝试执行lua脚本获取锁

图2处:此处使用信号量处理一个客户端进程中有多个线程竞争分布式锁的场景。

当有解锁消息到达时,不需要所有挂起线程都恢复,一起去竞争分布式锁,只需要唤醒一个线程去抢夺就可以了。Redisson使用信号量控制,每次收到解锁消息仅释放一个信号,只允许一个线程解除阻塞状态,去竞争锁(参考LockPubSub类的onMessage方法),当然,如果线程等待超时(超过锁过期时间)也会重新加入锁竞争行列。

由此代码分析可知,即时解锁消息通知失败。客户端也能在锁超时后重新尝试获取锁。实际上在比较旧一些的版本(3.13.1之前,参考 github.com/redisson/re… )中,如果解锁消息因为网络原因而丢失,客户端总会因等待超时而失败。

Redisson客户端解锁流程

解锁代码比较简单,其核心逻辑在RedissonLock.unlockInnerAsync(long threadId)方法里

unlockInnerAsync方法lua脚本代码说明如下:

  • KEYS[1]:锁名称
  • KEYS[2]:解锁消息通道名称
  • ARGV[1]:解锁消息
  • ARGV[2]:锁续租时间(看门狗超时时间,默认为30秒)
  • ARGV[3]:客户端唯一标识
# 判断锁是否为自己持有,不为自己持有则不允许解锁。
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil;
end ;
# 由于支持可重入,所以这里需要判断是否完全解锁,每解一次锁重入计数器减1.
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]);
    return 1;
end ;
return nil;
复制代码

Redisson看门狗机制

如果锁中的业务处理时间比较长,那么可能一种异常情况:即业务还未处理完毕,锁就提前过期了。Redisson针对这个问题的解决办法,是提供一个守护线程,定时检查锁状态。如果锁快要过期了,客户端还占有锁,那么就自动给锁续期,延长锁的过期时间。

守护线程轮询周期为:internalLockLeaseTime/3。internalLockLeaseTime的默认值由lockWatchdogTimeout来配置。默认值为30秒。也就是说默认情况下,守护线程每10秒检查续期。

续期靠执行下面这段lua脚本实现,每次续期时间由lockWatchdogTimeout配置项决定,默认30秒。

无PubSub锁-RedissonSpinLock

由于Redis PubSub的不可靠性,消息丢失几率是相对较高的。所以在RedissonLock的实现中,客户端等待解锁消息时都会设置超时时间,一旦超时客户端线程也会解除阻塞状态,重新进入锁竞争状态。

其实仔细想想,这里即使没有PubSub,锁的获取通过不断的自旋(重试)一样可以保证分布式锁的可靠性。只不过如果每次阻塞挂起的时间都设置为锁超时时间,会影响性能,因为大多数场景下业务处理时间要远远快于锁的超时时间。那么我们能不能考虑像RocketMQ消费者重试机制一样,一开始重试间隔时间很短,后续逐步增加重试间隔了?

其实官方已经有了对应的实现了,它就是RedissonSpinLock。来看看RedissonSpinLock是如何实现加锁的。

这代码看着可清爽多了...

RedissonSpinLock的核心就是红框处的代码,每次自旋竞争锁前先休眠一段时间,这个时间间隔由LockOptions.BackOffPolicy来指定。

官方目前提供了两种BackOffPolicy的实现:

  • ConstantBackOffPolicy 指定一个常量间隔,每次自旋休眠固定时长
  • ExponentialBackOffPolicy

每次按倍数multiplier增长,同时加个失败次数fails为种子的随机整数。最大值不能超过指定值-maxDelay 默认值:

  • initialDelay=1
  • multiplier = 2
  • maxDelay = 128

不要觉得自旋多重试了几次就会对性能有多大影响,要知道Redis每秒QPS可是能达到10W+级别的,区区几次重试完全可以忽略不计。不信可以瞅瞅官方的基准测试 redis.io/docs/manage…

关于红锁

有一种极端场景,客户端A尝试在Redis Master节点上锁,客户端A成功获得锁的瞬间,锁数据还没有同步至Slave节点。这时Master挂了,于是发生主从切换,其它客户端连接到Slave节点尝试抢占锁,由于Slave没有客户端A的上锁信息。自然又会有一个新的客户端B抢到锁,此时就会出现两个客户端同时拥有分布式锁的奇葩现像。

针对上述问题,Redis作者曾经提出了Redlock方案。Redisson中也有相应的实现,不过现在最新的版本已经不再建议使用。

因为现在基础的加锁操作,会广播到所有从节点,等所有从节点同步了才算加锁成功。

参考CommandBatchService.executeAsync方法代码,底层实现是通过Redis Wait命令,实现Master-Slave同步复制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/29810.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]计算机毕业设计JAVA课程资源系统管理

[附源码]计算机毕业设计JAVA课程资源系统管理 项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybati…

PowerJob 定时从SFTP下载文件拆的坑

一. 业务需求 SFTP上有多个目录, 每小时要下载一次文件, 每个目录的下载任务都是一个独立的工作流任务. 二.问题描述 手动执行每个任务可以正常执行, 但是当所有任务都开启定定时任务执行时(每小时执行一次),任务实例就会报错. 三.问题分析 查看服务端和worker端的日志, …

数据采集-“消防知识网上答题挑战赛”题库

为普及消防法律法规和消防安全知识,提升全员消防安全意识,提高抗御火灾、自防自救和组织疏散能力,集团公司近日部署11月份集中开展消防宣传月活动。 为“全民消防”营造浓厚氛围,集团公司以消防知识为主要内容,整理形…

关于如何快速学好,学懂Linux内核。内含学习路线

学习linux内核,这个可不像学一门语言,c或者java一个月或者3月你就能精通掌握。学习linux内核是需要一步一步循序渐进,掌握正确的linux内核学习路线对学习至关重要,本篇文章就来分享学习linux内核的一些建议吧。 1. 了解操作系统基…

nginx(六十一)proxy模块(二)修改发往上游的请求

一 修改发往上游的请求 重点: 利用指令更改转发给上游服务器的HTTP报文的内容1) 请求行 --> proxy_method、url、proxy_http_version2)请求头 --> proxy_set_header、proxy_pass_request_headers3)请求体 --> proxy_pass_request_b…

SpringBoot入口深入

这里写目录标题1.run()1.1 程序运行监听器 SpringApplicationRunListeners1.2 应用参数 ApplicationArguments启动加载顺序1.run() run()方法是一个SpringBoot程序的入口 SpringApplication.run(Application.class, args);看看方…

OVIS数据集代码解析

OVIS数据集代码解析 OVIS数据集格式整体和COCO类似,但是是以video的形式存储的,对应的解析代码见:https://github.com/qjy981010/cocoapi/blob/main/PythonAPI/pycocotools/ovis.py。 由于OVIS仅train提供了标注,因此&#xff0…

前端埋点实现方案

前言 领导今天又来活了😣,要记录每个页面的停留时间,以及页面的操作,是由哪个页面跳转过来的,给每个页面生成GUID上报给服务端,并且需要携带设备型号和设备唯一标识🙄 名称解释 UV&#xff0…

蓝牙运动耳机排行榜,目前排名最好的运动耳机推荐

对于运动人士来说,运动过程中佩戴着耳机听歌,既能让运动变得更加激情有动力,同时还能释放压力,放松心情。在选择运动耳机方面的要求也高,运动耳机不仅需要佩戴稳固舒适,它的防水性能也不能差,当…

Java并发常见面试题(一)

进程和线程 进程 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。 在Java中,当我们启动main函数其实就是启动了一个JVM进程,而main函数所在的线…

封装自己的 jquery 框架

说到jquery,那就给大家先简单的介绍一下什么是jquery jquery是JavaScript函数的一种封装。jQuery封装了JavaScript常用的功能代码(函数),提供一种简便的JavaScript设计模式,优化HTML文档操作、事件处理、动画设计和Aja…

机器人运动学【2】

目录1.刚体状态的表达2.顺向运动学及DH表3.逆向运动学1.刚体状态的表达 我们前面已经学习了刚体移动和转动的表达,那么怎么将两者在数学上结合呢?这里我们开始构造如下矩阵,记作: 下面我们来看一下只有移动情况下的刚体的描述: …

使用canal解决Mysql和Redis数据同步(TCP)

使用canal解决Mysql和Redis数据同步(TCP) 工作原理分析 我们在面试的时候常常听面试官问这么一个问题:你们的Mysql和Redis怎么做数据同步的,根据不同的业务场景又很多方案,你可能会说先写库再删缓存,或者延迟双删或其他方案。今…

springboot集成flowable简单实例入门

此案例是demo。功能有创建流程、完成审批、生成流程图。适合有java基础的人员看。 第一步.画流程图 resources资源包下,新建processes包,新建一个文件,我命名他apply-rest.bpmmn20.xml。bpmn20.xml后缀文件是流程图配置文件。idea的右下角的…

ImageMol

ai圈也是卷的狠啊~~ 又发现一个全球首个分子图像自监督学习框架来也 分子是维持物质化学稳定性的最小单位。对分子的研究,是药学、材料学、生物学、化学等众多科学领域的基础性问题。 分子的表征学习(Molecular Representation Learning)是…

实践 | 大型基金管理公司数据脱敏体系建设

金融行业数据脱敏安全管理要求高、数据类型复杂、脱敏数据规模庞大。作为业内领先,且支持信创环境的数据安全产品,美创数据脱敏系统在金融行业应用广泛,可满足各类复杂环境下的数据脱敏需求: 台州银行—分布式大数据平台下的及时脱…

SQL Server数据库理论篇(进行时......)

SQL Server数据库理论篇 一. 数据库的关系分类 1.1.0 关系型数据库和非关系数据库区别? 结论:两种数据库的本质在于存储数据的形式不同 1.关系型数据库概念 关系型数据库最大的特征就是表,我们将对象的属性映射为表的一个个列名&#xff…

(九)centos7案例实战——redis一主二从三哨兵高可用服务搭建

前言 本节内容是使用centos服务器搭建一套高可用的redis服务,采用的是一主二从三哨兵的模式。 需要注意的是搭建集群的过程中,我们要保证集群服务器之间可以相互访问,并且redis所需要访问的端口是开放的。我们从redis的下载,源码…

ffmpeg 安装教程

官网:Download FFmpeg window 转:ffmpeg安装教程_moon son的博客-CSDN博客_ffmpeg安装 然后解压,配置全局变量环境。点击“系统属性->高级系统设置->环境变量->系统变量”,选择“Path”条目,点击“编辑->…

将项目部署到Linux系统上

目的是让我们的项目在linux上也能运行起来 有两种部署方式,手工部署或者是通过shell脚本自动部署 手工部署 准备工作:使用ifconfig指令查出服务器的ip地址:192.168.58.130 1.在本地Idea中开发一个springboot项目,并且打包成ja…