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 也可以实现上述这些锁的特性

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JAVA Web 学习(二)ServLet

二、动态web 资源开发技术——Servlet Servlet(小服务程序)是一个与协议无关的、跨平台的Web组件,由Servlet容器所管理。运行在服务器端,可以动态地扩展服务器的功能,并采用“请求一响应”模式提供Web服务。 Servlet的…

【JavaScript】JS实用案例分享:DOM节点转JSON数据 | 标签输入框

🖥️ NodeJS专栏:Node.js从入门到精通 🖥️ 博主的前端之路(源创征文一等奖作品):前端之行,任重道远(来自大三学长的万字自述) 🖥️ TypeScript知识总结&…

对称和非对称加密算法

对称加密算法 对称加密算法依赖于一个共享的加密密钥,该密钥会被分发给所有参与通信 的对象。所有通信对象都使用这个密钥对消息数据进行加密和解密。当使用越长 的密钥对消息进行加密时,密文数据越难被破解。对称加密算法主要应用于批量 加密的数据&…

【开源】SpringBoot框架开发海南旅游景点推荐系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户端2.2 管理员端 三、系统展示四、核心代码4.1 随机景点推荐4.2 景点评价4.3 协同推荐算法4.4 网站登录4.5 查询景点美食 五、免责说明 一、摘要 1.1 项目介绍 基于VueSpringBootMySQL的海南旅游推荐系统&#xff…

力扣之2619.数组原型对象的最后一个元素-JS

Array.prototype.last function () {const a this.length;if (a 0) {return -1;}return this[a - 1]; };const nums [null, {}, 3]; console.log(nums.last());说明: 在 JavaScript 中,Array.prototype 是每个数组对象的原型。通过在 Array.prototy…

探索智慧文旅:科技如何提升游客体验

随着科技的迅猛发展,智慧文旅已成为旅游业的重要发展方向。通过运用先进的信息技术,智慧文旅不仅改变了传统旅游业的运营模式,更在提升游客体验方面取得了显著成效。本文将深入探讨科技如何助力智慧文旅提升游客体验。 一、智慧文旅的兴起与…

认识Spring 中的日志

这篇文章你将了解到Spring生态中日志框架是如何演化集成的 Spring Boot 日志 众说周知,Spring Boot 统一了日志框架,统一使用Logback进行日志输出,不管内部依赖框架使用的何种日志,最终都以Logback输出,他为什么需要统…

FCIS 2023:洞悉网络安全新前沿,引领未来安全创新狂潮

在数字化浪潮席卷全球的今天,网络安全问题愈发凸显其重要性。 FCIS 2023网络安全创新大会作为业界瞩目的盛会,不仅汇聚了国际顶尖的网络安全专家,更展示了最前沿的安全技术与研究成果。那么,参与这场大会,我们究竟能学…

05 MyBatis之表关系的声明+事务+SqlSession三件套的作用域

MyBatis 支持一对一,一对多,多对多查询。XML 文件和注解都能实现关系的操作。多对多实质就是一对多 1. 表关系的维护 1.1 One一对一 一对一查询和多表(两表)查询很相似, 都能查询两表的全部属性 区别是一对一可以在对象中嵌套对象, 呈现包含关系; 多表…

ele-h5项目使用vue3+vite开发:第一节、页面头部实现

实现页面 确认需求 顶部提示栏搜索框搜索提示 normalize.css:处理不同浏览器的默认样式 安装 npm i normalize.css 使用 src\App.vue<style scoped> import normalize.css;#app {/** 让字体抗锯齿&#xff0c;看起来更清晰 */-webkit-font-smoothing: antialiased;-moz-o…

python打造光斑处理系统4:裁切光斑感兴趣区域

文章目录 图像裁切给定坐标裁切手动阈值裁切 光斑处理&#xff1a;python处理高斯光束的图像 光斑处理系统&#xff1a;程序框架&#x1f31f;打开图像&#x1f31f;参数对话框/伪彩映射 图像裁切 一般来说&#xff0c;光斑只占图像很小一部分&#xff0c;为了更好的观感和更…

python实现贪吃蛇小游戏(附源码)

文章目录 导入所需的模块坐标主游戏循环模块得分 贪吃蛇小游戏&#xff0c;那个曾经陪伴着00后和90后度过无数欢笑时光的熟悉身影&#xff0c;仿佛是一把打开时光之门的钥匙。它不仅是游戏世界的经典之一&#xff0c;更是我们童年岁月中不可或缺的一部分&#xff0c;一个承载回…

《区块链简易速速上手小册》第6章:区块链在金融服务领域的应用(2024 最新版)

文章目录 6.1 金融服务中的区块链6.1.1 金融服务中区块链的基础6.1.2 主要案例&#xff1a;跨境支付6.1.3 拓展案例 1&#xff1a;去中心化金融&#xff08;DeFi&#xff09;6.1.4 拓展案例 2&#xff1a;代币化资产 6.2 区块链在支付系统中的作用6.2.1 支付系统中区块链的基础…

LRU 缓存置换策略:提升系统效率的秘密武器(上)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

Android进阶之路 - ViewPager2 比 ViewPager 强在哪?

我记得前年&#xff08;2022&#xff09;面试的时候有被问到 ViewPager 和 ViewPager2 有什么区别&#xff1f;当时因为之前工作一直在开发售货机相关的项目&#xff0c;使用的技术要求并不高&#xff0c;所以一直没去了解过 ViewPager2~ 去年的时候正好有相关的功能需求&#…

[Java 并发基础]多线程编程

文章参考&#xff1a; https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html https://juejin.cn/post/6970558076642394142 文章目录 线程的创建方式继承 Thread实现 Runnable 接口实现 Callable 接口使用 Lambda使用线程池 线程创建相关的 jdk源码Thr…

TCP四次握手

TCP 协议在关闭连接时&#xff0c;需要进行四次挥手的过程&#xff0c;主要是为了确保客户端和服务器都能正确地关闭连接。 # 执行流程 四次挥手的具体流程如下&#xff1a; 客户端发送 FIN 包&#xff1a;客户端发送一个 FIN 包&#xff0c;其中 FIN 标识位为 1&#xff0c…

【项目管理】立项管理

一、前言 对于甲方的立项&#xff1a;需求调研二编写项目申请书一可行性研究&#xff08;机会、初步、详细&#xff09;一项目论证一项目评估一评审获得批准一发布招标文件&#xff01;对于乙方的立项&#xff1a;看到招标文件一进行项目识别一可行性研究&#xff08;机会、初…

【Java 数据结构】优先级队列(堆)

优先级队列&#xff08;堆&#xff09; 1. 优先级队列1.1 概念 2. 优先级队列的模拟实现2.1 堆的概念2.2 堆的存储方式2.3 堆的创建2.3.1 堆向下调整2.3.2 堆的创建2.3.3 建堆的时间复杂度 2.4 堆的插入与删除2.4.1 堆的插入2.4.2 堆的删除 2.5 用堆模拟实现优先级队列 3.常用…

JDBC - 结构优化1

JDBC - 结构优化1 文章目录 JDBC - 结构优化1三层架构1 什么是三层架构2 三层架构项目搭建 结构优化1 - 学生信息管理1 封装工具类2 ORM3 DAO 三层架构 1 什么是三层架构 **三层架构&#xff1a;**将程序划分为表示层, 业务逻辑层, 数据访问层三层&#xff0c;各层之间采用接…