如何使用 Redis 快速实现分布式锁?

news2024/9/30 13:22:36

本文我们来讨论如何使用 Redis 快速实现分布式锁。

分布式锁有很多种解决方案,前面简单介绍过,Redis 可以通过 set key 方式来实现分布式锁,但实际情况要更加复杂,比如如何确保临界资源的串行执行,如何及时释放,都是需要额外考虑的。

本文要讲的是一个完备的分布式锁应该具备哪些特性,以及如何使用 Redis 来一步步优化实现。
分布式锁需要具有哪些特点
先来看一下,一个完备的分布式锁,需要支持哪些特性?

图片1(2).png

一般来说,生产环境可用的分布式锁需要满足以下几点:

  • 互斥性,互斥是锁的基本特征,同一时刻只能有一个线程持有锁,执行临界操作;
  • 超时释放,超时释放是锁的另一个必备特性,可以对比 MySQL InnoDB 引擎中的 innodb_lock_wait_timeout 配置,通过超时释放,防止不必要的线程等待和资源浪费;
  • 可重入性,在分布式环境下,同一个节点上的同一个线程如果获取了锁之后,再次请求还是可以成功;
  • 高性能和高可用,加锁和解锁的开销要尽可能的小,同时也需要保证高可用,防止分布式锁失效;
  • 支持阻塞和非阻塞性,对比 Java 语言中的 wait() 和 notify() 等操作,这个一般是在业务代码中实现,比如在获取锁时通过 while(true) 或者轮询来实现阻塞操作。

可以看到,实现一个相对完备的分布式锁,并不是锁住资源就可以了,还需要满足一些额外的特性,否则会在业务开发中出现各种各样的问题。

下面我们以 Redis 实现分布式锁为例,看一下如何优化分布式锁的具体实现。

使用 setnx 实现分布式锁

Redis 支持 setnx 指令,只在 key 不存在的情况下,将 key 的值设置为 value,若 key 已经存在,则 setnx 命令不做任何动作。使用 setnx 实现分布式锁的方案,获取锁的方法很简单,只要以该锁为 key,设置一个随机的值即可。如果 setnx 返回 1,则说明该进程获得锁;如果 setnx 返回 0,则说明其他进程已经获得了锁,进程不能进入临界区;如果需要阻塞当前进程,可以在一个循环中不断尝试 setnx 操作。

if(setnx(key,value)==1){
     try{
        //业务处理
     }finally{
       //释放锁
       del(key)
     }
}

释放锁时只要删除对应的 key 就可以,为了防止系统业务进程出现异常导致锁无法释放,使用 Java 中的 try-catch-finally 来完成锁的释放。

对比一下上面说的分布式锁特性,使用这种方式实现分布式锁的问题很明显:不支持超时释放锁,如果进程在加锁后宕机,则会导致锁无法删除,其他进程无法获得锁。

使用 setnx 和 expire 实现

在分布式锁的实现中,依赖业务线程进行锁的释放,如果进程宕机,那么就会出现死锁。Redis 在设置一个 key 时,支持设置过期时间,利用这一点,可以在缓存中实现锁的超时释放,解决死锁问题。

在使用 setnx 获取锁之后,通过 expire 给锁加一个过期时间,利用 Redis 的缓存失效策略,进行锁的超时清除。

伪代码如下:

if(setnx(key,value)==1){
     expire(key,expireTime)
     try{
        //业务处理
     }finally{
       //释放锁
       del(key)
     }
}

通过设置过期时间,避免了占锁到释放锁的过程发生异常而导致锁无法释放的问题,但是在 Redis 中,setnx 和 expire 这两条命令不具备原子性。如果一个线程在执行完 setnx 之后突然崩溃,导致锁没有设置过期时间,那么这个锁就会一直存在,无法被其他线程获取。

使用 set 扩展命令实现

为了解决这个问题,在 Redis 2.8 版本中,扩展了 set 命令,支持 set 和 expire 指令组合的原子操作,解决了加锁过程中失败的问题。

set 扩展参数的语法如下:

redis> SET key value expireTime nx

nx 表示仅在键不存在时设置,这样可以在同一时间内完成设置值和设置过期时间这两个操作,防止设置过期时间异常导致的死锁。那么这种方式还存在问题吗?

使用 setex 方式看起来解决了锁超时的问题,但在实际业务中,如果对超时时间设置不合理,存在这样一种可能:在加锁和释放锁之间的业务逻辑执行的太长,以至于超出了锁的超时限制,缓存将对应 key 删除,其他线程可以获取锁,出现对加锁资源的并发操作。

我们来模拟下这种情况:

  • 客户端 A 获取锁的时候设置了 key 的过期时间为 2 秒,客户端 A 在获取到锁之后,业务逻辑方法执行了 3 秒;
  • 客户端 A 获取的锁被 Redis 过期机制自动释放,客户端 B 请求锁成功,出现并发执行;
  • 客户端 A 执行完业务逻辑后,释放锁,删除对应的 key;
  • 对应锁已经被客户端 B 获取到了,客户端A释放的锁实际是客户端B持有的锁。

可以看到,第一个线程的逻辑还没执行完,第二个线程也成功获得了锁,加锁的代码或者资源并没有得到严格的串行操作,同时由于叠加了删除和释放锁操作,导致了加锁的混乱。

如何避免这个问题呢?首先,基于 Redis 的分布式锁一般是用于耗时比较短的瞬时性任务,业务上超时的可能性较小;其次,在获取锁时,可以设置 value 为一个随机数,在释放锁时进行读取和对比,确保释放的是当前线程持有的锁,一般是通过 Redis 结合 Lua 脚本的方案实现;最后,需要添加完备的日志,记录上下游数据链路,当出现超时,则需要检查对应的问题数据,并且进行人工修复。

分布式锁的高可用

上面分布式锁的实现方案中,都是针对单节点 Redis 而言的,在生产环境中,为了保证高可用,避免单点故障,通常会使用 Redis 集群。

集群下分布式锁存在哪些问题

集群环境下,Redis 通过主从复制来实现数据同步,Redis 的主从复制(Replication)是异步的,所以单节点下可用的方案在集群的环境中可能会出现问题,在故障转移(Failover) 过程中丧失锁的安全性。

由于 Redis 集群数据同步是异步的,假设 Master 节点获取到锁后在未完成数据同步的情况下,发生节点崩溃,此时在其他节点依然可以获取到锁,出现多个客户端同时获取到锁的情况。

我们模拟下这个场景,按照下面的顺序执行:

  • 客户端 A 从 Master 节点获取锁;
  • Master 节点宕机,主从复制过程中,对应锁的 key 还没有同步到 Slave 节点上;
  • Slave 升级为 Master 节点,于是集群丢失了锁数据;
  • 其他客户端请求新的 Master 节点,获取到了对应同一个资源的锁;
  • 出现多个客户端同时持有同一个资源的锁,不满足锁的互斥性。

可以看到,单实例场景和集群环境实现分布式锁是不同的,关于集群下如何实现分布式锁,Redis 的作者 Antirez(Salvatore Sanfilippo)提出了 Redlock 算法,我们一起看一下。

Redlock 算法的流程

Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。

假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作:

  • 客户端记录当前系统时间,以毫秒为单位;
  • 依次尝试从 5 个 Redis 实例中,使用相同的 key 获取锁,当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,超时时间应该小于锁的失效时间,避免因为网络故障出现的问题;
  • 客户端使用当前时间减去开始获取锁时间就得到了获取锁使用的时间,当且仅当从半数以上的 Redis 节点获取到锁,并且当使用的时间小于锁失效时间时,锁才算获取成功;
  • 如果获取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,减少超时的几率;
  • 如果获取锁失败,客户端应该在所有的 Redis 实例上进行解锁,即使是上一步操作请求失败的节点,防止因为服务端响应消息丢失,但是实际数据添加成功导致的不一致。

在 Redis 官方推荐的 Java 客户端 Redisson 中,内置了对 RedLock 的实现。下面是官方网站的链接,感兴趣的同学可以去了解一下:
redis-distlock
redisson-wiki

分布式系统设计是实现复杂性和收益的平衡,考虑到集群环境下的一致性问题,也要避免过度设计。在实际业务中,一般使用基于单点的 Redis 实现分布式锁就可以,出现数据不一致,通过人工手段去回补。

总结

今天分享了如何使用 Redis 来逐步优化分布式锁实现的相关内容,包括一个完备的分布式锁应该支持哪些特性,使用 Redis 实现分布式锁的几种不同方式,最后简单介绍了一下 Redis 集群下的 RedLock 算法。

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

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

相关文章

HarmonyOS、ArkTS 备忘录(持续更新中)

Component 、Builder Component封装大的组件Builder自定义构建函数,可以理解为 构建页面的函数;Builder插槽多点,封装一些小的模块 组件状态管理 像素单位 export default 和 export之间的区别

鸿蒙ArkTS Web组件加载空白的问题原因及解决方案

问题症状 初学鸿蒙开发,按照官方文档Web组件文档《使用Web组件加载页面》示例中的代码照抄运行后显示空白,纠结之余多方搜索后扔无解决方法。 运行代码 import web_webview from ohos.web.webviewEntry Component struct Index {controller: web_webv…

企业计算机服务器中了halo勒索病毒怎么解密,勒索病毒解密数据恢复

在网络技术飞速发展的今天,越来越多的企业开始意识到企业数据安全的重要性,很多企业都会依赖数字化办公系统软件,并且通过系统软件将企业的重要数据存储在数据库中,为企业的生产运营提供了极大便利,但网络威胁一直存在…

关于在Java中打印三角形图形的汇总

前面写过一些关于打印三角形图形代码的文章,这里进行了汇总,话不多说,直接上代码: /*** 关于打印三角形的汇总*/ public class Work1 {public static void main(String[] args) {int num 5;/** 打印如下图形:* ** …

基于单片机智能循迹小车仿真设计

**单片机设计介绍,基于单片机智能循迹小车仿真设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的智能循迹小车是一种通过传感器检测地面情况,并根据设定的规则进行动作控制的机器人。它使用…

如何正确使用缓存来提升系统性能

文章目录 引言什么时候适合加缓存?示例1示例2:示例3: 缓存应该怎么配置?数据分布**缓存容量大小:**数据淘汰策略 缓存的副作用总结 引言 在上一篇文章IO密集型服务提升性能的三种方法中,我们提到了三种优化…

LeetCode Hot100 39.组合总数

题目: 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限…

css3实现动态心电图折线

css3实现动态心电图折线 M(moveto):需要两个参数(x轴和y轴坐标,移动到的点的x轴和y轴的坐标L(lineto):需要两个参数(x轴和y轴坐标),它会在当前位置…

04.HTML其他知识

HTML其他知识 1.HTML实体 介绍 在 HTML 中我们可以用一种特殊的形式的内容&#xff0c;来表示某个符号&#xff0c;这种特殊形式的内容&#xff0c;就是 HTML 实体。比如小于号 < 用于定义 HTML 标签的开始。如果我们希望浏览器正确地显示这些字符&#xff0c;我们必须在…

CGAL的3D网格生成

1、介绍 该软件包致力于生成离散三维域的各向同性简化网格。要网格化的域是三维空间的子集&#xff0c;需要有界。域可以连接或由多个组件组成和/或细分为几个子域。 边界曲面和细分曲面是平滑曲面或分段平滑曲面&#xff0c;由平面或曲面面片形成。表面可能表现出一维特征&…

成都工业学院2021级操作系统专周课程设计FCFS,SSTF,SCAN,LOOK算法的实现

运行环境 操作系统&#xff1a;Windows 11 家庭版 运行软件&#xff1a;CLion 2023.2.2 源代码文件 #include <iostream> #include <vector> #include <algorithm> #include <random> using namespace std;// 生成随机数 int generateRandomNumber…

12.13_黑马数据结构与算法笔记Java

目录 098 堆 heapify 3 099 堆 增删替换 100 堆 e01 堆排序 100 堆e02 求数组第k大元素 100 堆e03 求数据流第k大元素 100 堆e04 求数据流中位数1 100 堆e04 求数据流中位数2 100 堆e04 求数据流中位数3 101 二叉树 概述 102 二叉树 深度优先遍历 103 二叉树 前中后…

2024年顶级的9个 Android 数据恢复工具(免费和付费)

不同的事情可能会损坏您的Android手机并导致您丢失数据。但大多数时候&#xff0c;您可以使用取证工具恢复部分或全部文件。 问题可能来自手机的物理损坏、磁盘的逻辑故障、完整的系统擦除&#xff0c;或者只是简单的粗心大意。 但是&#xff0c;无论数据丢失的原因是什么&am…

路由基本原理

目录 一、路由器概述 二、路由器的工作原理 三、路由表的形成 四、路由配置 1.连接设备 2.进入系统模式 3.进入接口模式 4.配置网络 5.下一跳的设置 6.设置浮动路由 7.设置默认路由 一、路由器概述 路由器&#xff08;Router&#xff09;是一种用于连接不同网络或子…

MySQL和Redis有什么区别?

目录 一、什么是MySQL 二、什么是Redis 三、MySQL和Redis的区别 一、什么是MySQL MySQL是一种开源的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它是最流行的数据库之一。MySQL以其高性能、可靠性和易用性而闻名&#xff0c;广泛应用于各种Web应用程序…

单片机的低功耗模式介绍

文章目录 简介一、功耗来源说明1.1、芯片工作模式1.2、静态损耗1.3、I/O额外损耗1.4、动态损耗 二、功耗如何测量三、降低功耗有什么方法3.1、选取合适的芯片工作模式3.2、降低工作频率3.3、关闭不需要使用的外设3.4、 降低静态电流损耗3.5、 周期采集供电3.6、 设置IO口状态 四…

MYSQL各种日志

感谢B站up主的视频分享 黑马程序员 MySQL数据库入门到精通&#xff0c;从mysql安装到mysql高级、mysql优化全囊括_哔哩哔哩_bilibili

FMETP STREAM 2.0

FMETPSTREAM简化了Unity3D中的直播,无需编码。设置和测试仅需5分钟。 "编码器模块"将Unity游戏视图、网络摄像头、桌面、声音和麦克风输入转换为字节数据,使其完美适用于各种流媒体场景。 优化的网络模块支持Server-clients连接类型,并允许您使用单个命令向 Serve…

el-date-picker限制选择7天内禁止内框选择

需求&#xff1a;elementPlus时间段选择框需要满足&#xff1a;①最多选7天时间。②不能手动输入。 <el-date-picker v-model"timeArrange" focus"timeEditable" :editable"false" type"datetimerange" range-separator"至&qu…

六.聚合函数

聚合函数 1.什么是聚合函数1.1AVG和SUM函数1.2MIN和MAX函数1.3COUNT函数 2.GROUP BY2.1基本使用2.2使用多个列分组2.3GROUP BY中使用WITH ROLLUP 3.HAVING3.1基本使用3.2WHERE和HAVING的区别 4.SELECT的执行过程4.1查询的结构4.2SELECT执行顺序4.3SQL执行原理 1.什么是聚合函数…