2023.1.31 关于 Redis 分布式锁详解

目录

引言

分布式锁

引入分布式锁

引入 set nx

引入过期时间

引入校验机制

引入 lua 脚本

引入过期时间续约(看门狗)

引入 redlock 算法

结语


引言

  • 在一个分布式系统中,可能会涉及到多个节点访问同一个公共资源的情况
  • 此时就需要通过锁来进行互斥控制,从而避免出现类似于 线程安全 的问题
  • 而像 Java 的 synchronized 这样的锁都是只能在当前进程中生效,在分布式系统的多个进程多个主机的场景下就无能为力了
  • 此时就需要用到分布式锁

通俗理解:

  • 多个线程并发执行时,执行的先后顺序是不确定的,具有随机性
  • 从而我们可以引入 Java 的 synchronized 来解决多个线程并发执行的随机性
  • 但是 synchronized 本质上只能在一个进程内部生效
  • 在分布式系统中,具有很多进程,即每个服务器均为一个独立进程
  • 因此 synchronized 难以对 分布式系统中的多个进程之间产生制约
  • 再加之分布式系统中,多个进程之间的执行顺序也是不确定的, 具有随机性
  • 为了保证程序在任意执行顺序下,其执行逻辑都是正确的
  • 从而我们引入了 分布式锁

分布式锁

实例引入

  • 此处模拟一个购买车票的场景
  • 买票逻辑:先查询余票,如果余票 > 0,则设置余票 -= 1

  • 假设此时客户端A 先执行查询 车次1 的余票,发现仅剩余 1 张
  • 在 买票服务器A 即将执行剩余票数 -= 1  过程之前
  • 此时客户端B 也执行查询 车次1 的余票,发现也仅剩余 1 张
  • 此时 买票服务器B 也会执行剩余票数 -= 1  的过程

注意:

  • 上述过程就属于 超卖,即 1张票卖给了 2个人!

引入分布式锁

  • 所谓分布式锁,本质上是使用 一个或一组 单独的服务器程序,通过使用一个键值对来标识锁的状态,来给其他的服务器提供 加锁 这样的服务

注意:

  • Redis 是一种典型的可以用来实现分布式锁的方案,但是不是唯一的一种
  • 业界可能也会使用 MySQL / zookeeper 这样的组件来实现分布式锁的效果

  • 如上图所示,买票服务器在执行 剩余票数 -= 1 操作的过程中,就需要先进行加锁
  • 即往 Redis 上设置一个特殊的 key-value 来完成上述买票操作,再将这个 key-value 删除掉
  • 当其他服务器也想买票时,也会去 Redis 上尝试设置 key-value
  • 如果发现 key-value 已经存在,就认为加锁失败(加锁失败后具体是放弃 还是阻塞,就得看具体的实现策略了)
  • 此时便可以保证 第一个服务器执行 "查询 ——> 更新" 的过程中,第二个服务器不会执行 "查询" ,也就解决了上述 超卖 问题!

问题:

  • 刚才买票场景,也可使用 MySQL 的事务来批量执行 查询 + 修改 操作
  • 将事务级别修改为 串行化执行 即可 

回答:

  • 首先,在分布式系统中,要访问的共享资源不一定就是 MySQL,也可能是其他的存储介质,且这些存储介质可能没有事务
  • 其次,在某些情况下,可能需要通过同一台服务器来执行一段特定的操作,以确保操作的原子性

引入 set nx

  • set key value nx 命令:如果 key 不存在则进行设置,如果 key 存在则不设置

注意:

  • 使用 set nx 确实可以实现 加锁 效果
  • 针对解锁,我们便可使用 del 命令来完成

问题:

  • 某个服务器 加锁成功了,即 set nx 命令执行成功
  • 如果在执行后续逻辑过程中,程序崩溃了,此时将无法执行解锁操作

对于进程内的锁:

  • 其一,为了保证解锁操作能够执行到,可将解锁操作放到 finally 中
  • 即 Java 中的 try - catch - finally,try 里面的逻辑无论是否出现异常,最后都会执行finally 中的代码
  • 其二,如果进程直接异常退出,锁也会跟着销毁
  • 上述两种做法或情况仅针对进程内的锁有效,针对分布式锁无效!

对于分布式锁:

  • 服务器直接掉电、 进程直接异常终止,这样的情况将直接导致 Redis 上设置的 key 无人删除也就导致其他服务器无法获取到锁
  • 因为 Redis 服务器 和 买票服务器 属于两个不同的服务器,买票服务器挂了,Redis 服务器上设置的 key 自然就无人删除了!

引入过期时间

  • 针对上述 "还没来得及解锁,服务器便宕机" 的情况,我们可以给 key 设置过期时间
  • 通过 set ex nx 这样的命令来完成设置,时间一到 key 便会被自动删除

具体理解:

  • 比如设置 key 的过期时间为 1000ms ,那么意味着即使该服务器出现了极端情况挂掉了,无法释放掉锁,这个锁最多保持 1000ms ,也就自动释放了

注意:

  • set nx + expire 这种设置方式是不行的!务必需要使用 set ex nx 这样的方式来设置
  • 因为 Redis 上多个命令之间,无法保证着多个命令执行的原子性!
  • 此时就可能出现这两个命令,一个成功,一个失败的情况
  • 相比之下,直接使用一条命令设置,便显得更加稳妥!

问题:

  • 所谓的加锁,就是给 Redis 上设置一个 key-value
  • 所谓的解锁,就是给 Redis 上的这个 key-value 给删除掉
  • 是否可能会出现,服务器A 执行了加锁,服务器B 执行了解锁呢?

回答:

  • 这种情况是可以存在的!
  • 正常来说,服务器2 肯定不是故意的,但是代码总会有 bug,从而不小心就执行到了解锁操作,因此就可能进一步的给整个系统带来更严重的问题(比如像超卖)

引入校验机制

  • 针对上述 "服务器A 执行加锁,服务器B 执行解锁" 的情况,我们可以引入校验机制

具体思路:

  1. 给服务器编号,每个服务器均有一个自己的身份标识
  2. 进行加锁时,设置 key-value 对应着要针对哪个资源加锁(比如车次),value 便可以存储用来存储 服务器编号,标识出当前这个锁是哪个服务器加上的
  3. 后续在解锁时,先查询一下这个锁对应的服务器编号,然后判定一下这个编号是否就是当前执行解锁的服务器编号,如果是,则真正执行 del,如果不是,就失败

注意点一:

  • 上述的校验操作为 服务器 需要完成的逻辑
  • 通过上述校验,便可以有效避免 "误解锁"

注意点二:

  • 上述服务器都是我们自己写的代码
  • 这些代码的初心,当然是为了避免出现上述问题,而不会说服务器故意搞破坏

引入 lua 脚本

  • 在解锁时,先查询判定,再进行 del
  • 此时这两步操作就可能会出现问题,因为这两步操作不是原子的
  • 一个服务器内部,是可以同时处理多个请求的
  • 此时就可能出现同一个服务器,两个线程均在执行上述解锁操作

  • 如上图所示,看起来重复执行 DEL 好像问题不大
  • 因为使用 DEL 命令删除一个不存在的 key 时,会直接返回 0,表示没有删除任何键
  • 是 Redis 的一种正常行为,不会引发错误

  • 如上图所示,此时引入一个新服务器,要来执行加锁,就可能出现问题了
  • 在线程A 执行完 DEL 之后,线程B 执行 DEL 之前
  • 服务器2 的线程C 正好要执行加锁操作(set nx ex)
  • 此时由于线程A 已经将锁给释放掉了,线程C 的加锁是能够执行成功的!
  • 但是紧接着,线程B 的 DEL 就到来了,直接将刚刚服务器2 的加锁操作给解锁了!

问题:

  • 为啥上文引入的校验机制没起作用呢?

回答:

  • 上述场景中,服务器1 的线程B 已经执行完 get 操作后,即已经判定完 Redis 上的 key 就是服务器1 所设置的,可以执行 del 操作
  • 此时服务器2 的线程C 便穿插在线程B 的 get 和 del 命令之间,往 Redis 中设置 key-value,进行加锁
  • 从而紧接着服务器1 的线程B 直接执行 del 操作,将服务器2 线程C 在 Redis 中设置的 key 给直接删掉了
  • 归根结底,都是因为 get 和 del 不是原子的所产生的问题

注意:

  • 使用事务,可以解决上述问题,虽然 Redis 的事务比较弱,但还是能够避免插队的
  • 然而在实践中,往往使用更好的方案,即 lua 脚本

具体理解:

  • lua 是一个编程语言,作为 Redis 内嵌的脚本
  • MySQL8 支持 Javascript 作为内嵌语言
  • Vim 支持使用 vumscript / python 作为内嵌语言
  • 但是 lua 语言特别轻量,即实现一个 lua 解释器,其消耗的体积是非常小的
  • 我们可以使用 lua 编写一些逻辑,并将该脚本上传到 Redis 服务器
  • 然后就可以让客户端来控制 Redis 执行上述脚本了

注意:

  • Redis 执行 lua 脚本的过程也是原子的,相当于执行一条命令一样
  • Redis 官方文档也明确说,lua 就属于是 事务 的替代方案
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) 
else return 0 
end;
  • 通过上方 lua 脚本,便能使得 get 和 del 命令执行的原子性
  • ARGV[1]:表示调用脚本给定的参数,此处需传入一个服务器的 id
  • 如果 id 和 get 到的参数能够匹配相等,则进行删除操作

引入过期时间续约(看门狗)

  • 当某一服务器进行加锁时,我们应该给 key 设置多长的过期时间呢?
  • 如果设置的太短,就可能业务逻辑还没执行完,就把锁给释放了
  • 如果设置的太长,就可能导致 锁释放不及时 问题
  • 所以此处我们引入 动态续约

具体理解:

  • 初始情况下,设置一个过期时间(比如设置 1s)就提前在还剩 300s 时,且如果当前任务还没执行完,就把过期时间再续上 1s
  • 等到时间又快到了,如果任务还没执行完,就再续

注意点一:

  • 也不一定就是提前 300ms,此处的数值可灵活调整

注意点二:

  • 如果服务器中途崩溃了,自然就没人负责续约了
  • 此时,锁便能在较短的时间内被自动释放!

注意点三:

  • 这种动态续约 往往需要服务器这边有一个专门的线程来负责
  • 而这个负责的线程就叫做看门狗
  • 看门狗 也是一个比较广义的概念,很多场景均会涉及到这种针对过期时间的操作,从而引入 看门狗

问题:

  • 使用 Redis 作为分布式锁,有没有可能 Redis 本身自己挂掉了呢?

回答:

  • 是可能存在的该情况的
  • 为了确保 Redis 的高可用性,便需要制定一系列的预案和应急措施,通过预案演习,可以在发生问题时更快地进行故障切换和恢复

注意:

  • 在使用 Redis 作为分布式锁的场景中,通常仅涉及到少量的数据,因为锁的目的是控制对共享资源的访问,而不是存储大量数据
  • 因此,备份和恢复 Redis 中的锁相关数据就相对较为轻量

具体理解:

  • 服务器进行加锁,就是将 key 设置到主节点上
  • 如果主节点挂了,就会有哨兵自动将从节点升级为主节点,进一步的保证刚才的锁仍然可用
  • 但是主节点和从节点之间的数据同步存在一定的延时
  • 可能主节点收到了 set 请求,还没来得及同步给从节点,主节点就先挂了
  • 即使从节点升级成了主节点,但是刚才的加锁对应的数据也不存在

引入 redlock 算法

  • 这是 Redis 作者给出的一个方案,用来解决 Redis 节点挂掉所引发的问题

  • 此处加锁,就是按照一定的顺序,针对多个独立的 Redis 节点都进行加锁操作!
  • 如果某个节点加不上锁没关系,可能是 Redis 挂掉了,继续给下一个节点加锁即可
  • 如果写入 key 成功的节点个数超过总数的一半,就视为加锁成功
  • 同理进行解锁的时候,也就会把上述节点都设置一遍解锁

注意:

  • 此处需跟 Redis 集群给区分开来
  • Redis 集群主要是用来解决存储空间不足问题,即拓展存储空间
  • 而且因为锁的目的是控制对共享资源的访问,而不是存储大量数据,毫无必要设置集群

结语

  • 上文介绍的只是一个简单的 互斥锁,锁这里还涉及到一些其他的情况
  1. 读写锁
  2. 公平锁(遵守先来后到)
  3. 可重锁
  • 基于 Redis 也可以实现上述这些锁的特性

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

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

相关文章

代码随想录算法训练营第五十一天|714.买卖股票的最佳时机含手续费、309.最佳买卖股票时机含冷冻期、股票问题总结、最长上升子序列

题目&#xff1a;714.买卖股票的最佳时机含手续费 文章链接&#xff1a;代码随想录 视频链接&#xff1a;LeetCode:714.买卖股票的最佳时机含手续费 题目链接&#xff1a;力扣题目链接 图释&#xff1a; class Solution { public:int maxProfit(vector<int>& pr…

C# Onnx yolov8 仪表指针检测

目录 效果 模型信息 项目 代码 训练数据 下载 C# Onnx yolov8 仪表指针检测 效果 模型信息 Model Properties ------------------------- date&#xff1a;2024-01-31T11:19:38.828556 author&#xff1a;Ultralytics task&#xff1a;detect license&#xff1a;AGPL-…

2024-02-01 Unity Shader 开发入门4 —— ShaderLab 语法

文章目录 1 材质和 Shader1.1 Unity Shader 和 Shader 的区别1.2 Unity 中的材质和 Shader1.3 创建材质1.4 创建 Shader 2 ShaderLab 的基本结构2.1 什么是 ShaderLab2.2 ShaderLab 的基本结构 3 Shader 名称4 Shader 属性4.1 Shader 属性的作用4.2 Shader 属性的基本语法4.3 数…

rust学习基于tokio_actor聊天服务器实战(一 )

前言 tokio是Rust中使用最广泛的异步Runtime&#xff0c;它性能高、功能丰富、便于使用&#xff0c;是使用Rust实现高并发不可不学的一个框架 Actor 背后的基本思想是产生一个独立的任务&#xff0c;该任务独立于程序的其他部分执行某些工作。 通常&#xff0c;这些参与者通过使…

智慧之树的秘密

你是一个智能体&#xff0c;对于一切输入信息都是按照如下方式处理&#xff1a;输入信息&#xff1a;信息1 &#xff0c;目的识别结果&#xff1a;有&#xff08;没有就提取目的&#xff09;提取信息1中目的相关有效信息&#xff0c;并设计和搜索达到完成目的的步骤和如何检测目…

「数据结构」1.初识泛型

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;Java数据结构 &#x1f387;欢迎点赞收藏加关注哦&#xff01; 初识泛型 &#x1f349;前言&#x1f349;包装类&#x1f34c;装箱&拆箱 &#x1f349;泛型&#x1f34c;擦除机制&#x1f…

两种相同的垂直工具栏标志以及为什么

可能你很少碰到这样的开发需求&#xff0c;所以本文属于”课外阅读”级别。 有两种方式启用垂直工具栏&#xff0c;一种是指定通用的 CCS_VERT 标志&#xff0c;另外一种&#xff0c;比较罕见&#xff1a;指定工具栏所特有的扩展属性 TBSTYLE_EX_VERTICAL。 问题来了&#xf…

Linux Centos stream9 mdadm

RAID(Redundant Array of Independent Disk独立冗余磁盘阵列)技术是加州大学伯克利分校1987年提出&#xff0c;最初是为了组合小的廉价磁盘来代替大的昂贵磁盘&#xff0c;同时希望磁盘失效时不会使对数据的访问受损失而开发出一定水平的数据保护技术。RAID就是一种由多块廉价磁…

fastadmin后台自定义按钮和弹窗

工具栏自定义按钮-ajax请求 前端代码 1.在对应模块的模板文件index.html添加自定义按钮&#xff0c;注意按钮要添加id以绑定点击事件 <div class"panel panel-default panel-intro">{:build_heading()}<div class"panel-body"><div id&qu…

pysyft框架中WebsocketClientWorker与WebsocketServerWorker的消息传输

引言 pysyft是基于pytorch的一个联邦学习框架&#xff08;虽然用起来很难受&#xff09;&#xff0c;通过内存管理实现联邦学习的模拟。 在pysyft中&#xff0c;WebsocketServerWorker充当数据的提供方&#xff08;数据存储方&#xff09;&#xff0c;而WebsocketClientWorker…

专业的韩语导游翻译需要具备哪些能力

近年来&#xff0c;随着中韩关系的友好发展&#xff0c;两国之间的旅游交流呈现出爆发式的增长。这一趋势不仅深化了中韩民众之间的交流与理解&#xff0c;也对韩语导游翻译人才的需求产生了显著的影响。那么&#xff0c;为了做好韩语翻译工作&#xff0c;我们需要具备哪些专业…

页面通过Vue进行整体页面不同语言切换 i18n库

目录 引入 如何做到 下载i18n库 构建整体翻译文件结构 语言包文件 i18n配置文件 把i18n挂载到vue实例上 添加按钮点击事件切换语言 引入 我们现在有这样一个要求,我们想要对我们开发的网页进行国际化操作,也就是我们不仅要有中文,还要有英文等。用户可以随时进行不同语言…

代码随想录day16--二叉树的应用4

LeetCode513.找树左下角的值 题目描述&#xff1a; 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1示例 2: 输入: [1,2,3,4,null,5,6,null,null,7] 输出: 7 解题思…

C++ //练习 3.21 请使用迭代器重做3.3.3节(第94页)的第一个练习。

C Primer&#xff08;第5版&#xff09; 练习 3.21 练习 3.21 请使用迭代器重做3.3.3节&#xff08;第94页&#xff09;的第一个练习。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /*************************************…

【重磅】中国隐私计算平台市场,摩斯第一

摩斯市场份额遥遥领先 10月11日&#xff0c;全球领先的IT市场研究和咨询公司IDC发布了《中国隐私计算平台厂商市场份额&#xff0c;2022》报告。蚂蚁集团凭借商用隐私计算平台摩斯&#xff08;MORSE&#xff09;&#xff0c;以 36.9%的市场份额排名第一。…

Git系列---远程操作

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 引用 1.理解分布式版本控制…

java+springboot校园体育场地预约预订使用系统vue+ssm

研究内容和研究方法 1.研究内容 网站主要包括管理员和用户两个部分&#xff0c;用户可以登录与注册自己的基本信息、查询哪些场地可以使用、提前预约场地、取消预约的场地、使用完场地后进行缴费。管理员可以审批用户的注册信息、对用户信息进行增删改查、查询场地的使用情况、…

JUnit

前言&#xff1a;自动化就是selenium脚本来实现的&#xff0c;JUnit是java的单元测试工具&#xff0c;只不过我们在实现自动化的时候需要借助一下JUnit库里面提供的一些方法。 1、Test Test &#xff1a;表示方法是测试方法&#xff0c;执行当前这个类的时候&#xff0c;会自动…

微服务知识

1、概念 大型单体应用拆分成多个独立部署运行的微服务&#xff08;解决并发问题&#xff09;​​​​​​​ 2、特点 3、技术栈 4、微服务带来的问题 ​​​​​​​ 5、微服务的注册中心 服务注册与发现&#xff1a;微服务实例在启动时会向注册中心注册自己的信息&#xf…

Centos慢慢长大(一)

1、写在前面 这将是一个系列性的文章。可能更多的是记录我在学习的过程中的一些感悟吧。我想强调的是在这一系列文章里我会从最小化的安装开始&#xff0c;然后逐渐的增加需要安装的软件。就象一个婴儿的诞生&#xff0c;慢慢的学走路、学说话、学使用筷子。。。。。。 这将是一…