Redis并发问题解决方案

目录

前言

1.分布式锁

1.基于单个节点

2.基于多个节点

3.watch(乐观锁) 

2.原子操作

1.单命令操作

2.Lua 脚本(多命令操作)

3.事务

1.执行步骤

2.错误处理

3.崩溃处理

总结


前言

在多个客户端并发访问Redis的时候,虽然Redis是单线程执行指令,但是由于客户端指令达到Redis的时序无法保证,所以可能出现如下的情况,导致并发问题。

2个客户端都执行 get, set指令,期望将key的值设置为3,结果因为并发问题,导致结果为2
client1 get x => 1
client2 get x => 1
client1 set x => 2
client2 set x => 2

本文介绍 Redis 并发方面的解决方案。

Redis 的单个命令是原子的,但是一个业务操作可能包含多条命令,比如以下场景:客户端查询值,并递增,在高并发场景下就可能出现并发问题,导致数据不一致。

为了保证并发访问的正确性,Redis 提供了三种方法,原子操作、分布式锁、事务。

1.分布式锁

与分布式锁相对的是本地锁,假如只有一个服务实例,就可以直接在该单应用本地使用锁变量来控制多个客户端的访问。

如果使用的是多实例的分布式系统,就需要使用分布式锁,即将锁保存在一个第三方的共享存储系统中,可以被多个客户端共享访问和获取。通常将一个 Redis 实例作为分布式锁的存储系统。

实现分布式锁的关键在于:

  • 保证每个加锁、释放锁操作都是原子的;
  • 保证共享存储系统的可靠性,即锁的可靠性;
分布式锁相较于 Lua 脚本,更简单易用,但是可能存在死锁问题。分布式锁的性能不如 Lua 脚本。

1.基于单个节点

Redis 提供了 SETNX 命令(在 SET 命令后加上 NX 选项也能达到同样的效果),保证了加锁操作的原子性。

同时,为了避免客户端加锁后不释放,应该给锁变量设置过期时间(set NX EX),且在过期释放锁时,判断业务代码是否执行完成,如果未完成则给锁续期。如果多次续期后,业务仍然未完成,再释放锁。(仍然存在风险)

为了区分不同客户端的操作,应该将锁变量设置为随机值或唯一值,在释放锁时进行验证。

释放锁的逻辑包含了读取锁变量、判断值、删除锁变量的多个操作,所以应该使用 Lua 脚本来保证互斥执行。

单个节点可以实现分布式锁的功能,但是无法保证可靠性

2.基于多个节点

为了避免 Redis 实例故障而导致的锁无法工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,就认为客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个实例发生故障,因为锁变量在其它实例上也有保存,所以客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

加锁过程:

  1. 客户端获取当前时间;
  2. 客户端按顺序依次向 N 个 Redis 实例执行加锁操作。同样使用 SETNX 命令,并设置超时时间。如果请求加锁一直超时,则视为加锁失败,向下一个实例执行加锁操作。
  3. 客户端完成所有加锁操作后,计算整个加锁过程的总耗时。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功:

  • 从超过半数(大于等于 N/2+1)的实例上成功获取到了锁;
  • 获取锁的总耗时没有超过锁的有效时间。

在满足了这两个条件后,还需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
如果没能同时满足这两个条件,则视为加锁失败,执行释放锁的过程:客户端会向所有节点发起释放锁的操作,执行释放锁的 Lua 脚本。

判断是否加锁时,需要查询所有节点,以半数以上节点的锁状态来判断整个分布式锁的状态。在释放锁之前,需要先判断分布式锁的状态。

为了避免 Redis 节点发生崩溃重启后造成锁丢失,从而影响锁的安全性,antirez 还提出了延时重启的概念,即一个节点崩溃后不要立即重启,而是等待一段时间后再进行重启,这段时间应该大于锁的有效时间。优点是保证了锁不会被多个客户端获取;缺点是延长了重启时间,可能对系统造成影响。

性能和一致性是冲突的,如果为了分布式锁的高可用性,可以开启持久化,但是会有额外的性能开销,需要根据实际场景进行选择。

3.watch(乐观锁) 

watch通常跟redis事务配合使用,watch某个key在操作过程中有没有被其他指令改变,进而做出相应的处理。底层利用了CAS操作,后面讲Redis事务会讲到。

2.原子操作

为了实现并发控制要求的临界区代码互斥执行,Redis 的原子操作采用了两种方法:单命令操作和 Lua 脚本。

1.单命令操作

Redis 的每个操作都是原子性的。

Redis 是使用单线程来串行处理客户端的请求操作命令的,所以,当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于单个操作是原子的。虽然 Redis 的单个操作是原子的,但是通常修改数据是包含多个操作的,至少包括读数据、修改数据、写回数据这三个操作,此时仍然可能出现并发问题。

针对常用的修改数据场景,Redis 提供了 INCR/DECR 命令,可以对数据进行简单的递增/递减操作,它们本身就是单个命令操作,在执行时,具有互斥性。但是如果要执行更复杂的操作,Redis 的单命令操作就无法保证互斥执行了。

2.Lua 脚本(多命令操作)

Redis 可以将多个操作写在 Lua 脚本中,然后把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。

为什么是 Lua 脚本,而不是其他语言的脚本?
Lua 是一种高效的轻量级 脚本语言,用标准 C 语言编写并以源代码形式开放。其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 脚本可以在服务器端执行,不需要将数据传输到客户端再进行处理,可以减少网络传输的开销,因此性能较高。

使用 Lua 脚本不仅可以实现将多个操作原子执行,还能够复用 Lua 脚本。但是使用 Lua 脚本需要额外的语言学习成本,还有调试困难、可读性较差的问题。

如果把很多操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的时间增加,同样也会降低 Redis 的并发性能。所以,在编写 Lua 脚本时,要避免把不需要做并发控制的操作写入脚本中。

在 Lua 脚本执行过程中崩溃怎么办?
Redis 会在内部维护一个已经加载脚本的 哈希表,记录了每个脚本的 SHA1 值和对应的 Lua 脚本代码。当 Redis 服务器重启时,Redis 会自动重新加载这个哈希表中记录的所有脚本,再重新执行,此时可能导致部分修改被应用。所以 Lua 脚本并不能严格保证原子性。如果对数据一致性非常严格,可以使用 Lua 脚本+事务 WATCH 的办法。

3.事务

Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

Redis 提供了实现事务的几个命令:

MULTI :开启事务,redis 会将后续的命令逐个放入队列中,然后使用 EXEC 命令来原子化执行这个命令系列。

EXEC:执行事务中的所有操作命令。

DISCARD:取消事务,放弃执行事务块中的所有命令。

WATCH:在开启事务之前监视一个或多个 key,如果事务在执行前,这个 key (或多个 key)被其他命令修改,则事务被中断,不会执行事务中的任何命令(一般需要在 EXEC 执行失败后重新执行整个函数)。

UNWATCH:取消 WATCH 对所有 key 的监视。

为什么 WATCH 是中断事务,而不是阻塞其他进程?这样不会导致并发量高的时候,被 WATCH 的事务一直得不到执行吗?
这种机制称为 乐观锁,因为在大多数情况下,碰撞的概率很小,所以选用了更容易实现的方式(且影响不大)。

在使用事务时,可以配合 Pipeline 使用:一次性将所有命令打包好,再全部发送到服务端。
相比于事务的入队,同样是一次性执行,这样不仅能减少网络 IO,还能保证在开启 WATCH 时不会被其他操作打断。

1.执行步骤

  1. 开启事务:使用 MULTI 命令开启事务;
  2. 入队:接收到命令后并不会立即执行,而是放到等待执行的事务队列里;
  3. 执行:由 EXEC 命令触发事务执行。

当客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:

  • 如果客户端发送的命令为 EXEC 、DISCARD、WATCH、MULTI 四个命令的其中一个, 那么服务器立即执行这个命令;
  • 如果是其他命令, 那么服务器并不立即执行命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复;

2.错误处理

在事务执行过程中可能遇到两种不同类型的错误,会有不同的应对方案:

  • 编译器错误:命令在编译时出错,会导致整个事务提交失败,即所有命令执行不成功;
  • 运行时错误:命令在运行时检测到错误,最终会导致事务提交失败,但是事务并不会回滚,而是跳过错误命令继续执行并保留结果;
为什么 Redis 不支持 事务回滚?
Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

3.崩溃处理

Redis 在执行事务时会使用一个单独的内存空间来保存事务中的所有修改操作,只有当事务成功提交时,这些修改操作才会被应用到 Redis 中。因此,如果事务被中止,所有的修改操作也都会被撤销,从而保证了数据的一致性。

如果开启了 AOF 持久化,会先将事务中的所有命令写入 AOF 缓冲区,然后执行事务中的命令,再将 AOF 缓冲区中的数据写入到 AOF 文件。

如果在写入 AOF 文件前崩溃,则持久化失败,相当于事务没有发生,不会出现数据不一致。

另外,RDB 快照不会在事务执行途中进行。

总结

本文介绍了 Redis 应对并发问题的三种方案,Redis 中的单条命令都是原子操作,而且还有 INCR/DECR 来应对简单的场景。对于复杂的场景,Redis 可以使用 Lua 脚本、分布式锁、事务来实现操作的原子性。Lua 脚本是将一系列操作放在一个脚本中原子执行。分布式锁是通过共享的锁变量来限制客户端的并发访问。事务是将一系列操作放到执行队列中,再按顺序原子执行。

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

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

相关文章

基于IDEA+HTML+SpringBoot前后端分离电子商城

基于springboot的电子商城 项目介绍💁🏻 •B2C 商家对客户 •C2B2C 客户对商家对客户 1.1.1 B2C 平台运营方即商品的卖家 小米商城 •商品 •用户 1.1.2 C2B2C 平台运营方不卖商品(也可以卖) 卖家是平台的用户 买家也是平台用户 •…

【算法】经典算法题

文章目录 专题一:双指针1. 移动零2. 复写零3. 快乐数4. 盛最多水的容器5. 有效三角形的个数6. 查找总价格为目标值的两个商品7. 三数之和8. 四数之和 专题二:滑动窗口1. 长度最小的子数组2. 无重复字符的最长字串3. 最大连续1的个数 III4. 将 x 减到 0 的…

请你说一下Vue中v-if和v-for的优先级谁更高

v-if 与 v-for简介 v-ifv-forv-if & v-for使用 v-if 与 v-for优先级比较 vue2 中,v-for的优先级高于v-if 例子进行分析 vue3 v-if 具有比 v-for 更高的优先级 例子进行分析 总结 在vue2中,v-for的优先级高于v-if在vue3中,v-if的优先级高…

61 权限提升-RedisPostgre令牌窃取进程注入

目录 演示案例:Redis数据库权限提升-计划任务PostgreSQL数据库权限提升Windows2008&7令牌窃取提升-本地Windows2003&10进程注入提升-本地pinjector进程注入工具针对-win2008以前操作系统pexec64 32进程注入工具针对-win2008及后操作系统- (佛系) 涉及资源: postgersql是…

2023亚太杯数学建模APMCM竞赛C题思路讲解:基于ARIMA与机理模型进行预测

本文针对6大问题,从多角度分析了我国新能源电动汽车发展形势与前景。文中针对不同问题,采用了层次分析法、时间序列模型、机理模型、回归模型等数学方法。并结合实例数据,对相关模型进行求解,以量化预测了新能源电动汽车在政策驱动、市场竞争、温室气体减排等多个方面的潜在贡献…

OpenCV快速入门:图像分析——图像分割和图像修复

文章目录 前言一、图像分割1.1 漫水填充法1.1.1 漫水填充法原理1.1.2 漫水填充法实现步骤1.1.3 代码实现 1.2 分水岭法1.2.1 分水岭法原理1.2.2 分水岭法实现步骤1.2.3 代码实现 1.3 GrabCut法1.3.1 GrabCut法原理1.3.2 GrabCut法实现步骤1.3.3 代码实现 1.4 Mean-Shift法1.4.1…

【分布式】小白看Ring算法 - 03

相关系列 【分布式】NCCL部署与测试 - 01 【分布式】入门级NCCL多机并行实践 - 02 【分布式】小白看Ring算法 - 03 【分布式】大模型分布式训练入门与实践 - 04 概述 NCCL(NVIDIA Collective Communications Library)是由NVIDIA开发的一种用于多GPU间…

Navicat 技术指引 | 适用于 GaussDB 的数据迁移工具

Navicat Premium(16.2.8 Windows版或以上) 已支持对 GaussDB 主备版的管理和开发功能。它不仅具备轻松、便捷的可视化数据查看和编辑功能,还提供强大的高阶功能(如模型、结构同步、协同合作、数据迁移等),这…

基于element-ui后台模板,日常唠嗑

后面会补充github地址 文章目录 目录 文章目录 案例说明 1.引入库 2.创建布局组件 3.创建布局组件 4.菜单效果展示 5.创建顶部组件 5.创建顶部面包屑组件 6.创建内容区域组件 7.效果总览 7.布丁(实现一些小细节) 前言一、pandas是什么?二、使…

Android Studio记录一个错误:Execution failed for task ‘:app:lintVitalRelease‘.

Android出现Execution failed for task :app:lintVitalRelease.> Lint found fatal errors while assembling a release target. Execution failed for task :app:lintVitalRelease解决方法 Execution failed for task ‘:app:lintVitalRelease’ build project 可以正常执…

计算机网络之网络层

一、概述 主要任务是实现网络互连,进而实现数据包在各网络之间的传输 1.1网络引入的目的 从7层结构上看,网络层下是数据链路层 从4层结构上看,网络层下面是网络接口层 至少我们看到的网络层下面是以太网 以太网解决了什么问题? 答…

python中一个文件(A.py)怎么调用另一个文件(B.py)中定义的类AA详解和示例

本文主要讲解python文件中怎么调用另外一个py文件中定义的类,将通过代码和示例解读,帮助大家理解和使用。 目录 代码B.pyA.py 调用过程 代码 B.py 如在文件B.py,定义了类别Bottleneck,其包含卷积层、正则化和激活函数层,主要对…

微信小程序实现【点击 滑动 评分 评星(5星)】功能

wxml文件&#xff1a; <view class"wxpl_xing"><view class"manyidu">{{scoreContent}}</view><view><block wx:for{{scoreArray}} wx:for-item"item"><view classstarLen bindtapchangeScore data-sy"{{…

什么是LLC电路?

LLC电路是由2个电感和1个电容构成的谐振电路&#xff0c;故称之为LLC&#xff1b; LLC电路主要由三个元件组成&#xff1a;两个电感分别为变压器一次侧漏感(Lr)和励磁电感(Lm)&#xff0c;电容为变压器一次侧谐振电容(Cr)。这些元件构成了一个谐振回路&#xff0c;其中输入电感…

程序员进阶高管指南,看懂工资最少加5k

从象牙塔毕业跨入社会大染缸&#xff0c;很多人都跟我谈过他们的职业困惑&#xff0c;其中有一些刚刚毕业&#xff0c;有些人已经工作超过10年。基本上是围绕着怎样持续提升&#xff0c;怎样晋升为高级管理者。那么这篇文章&#xff0c;我就来谈一谈程序员到高管的跃升之路。 …

程序环境和预处理(详解版)

我们已经学到这里&#xff0c;这就是关于C语言的最后一个集中的知识点了&#xff0c;虽然它比较抽象&#xff0c;但是了解这部分知识&#xff0c;可以让我们对C代码有更深层次的理解&#xff0c;知道代码在每一个阶段发生什么样的变化。让我们开始学习吧! 目录 1.程序的翻译环…

5个免费在线工具推荐

NSDT 三维场景建模工具GLTF/GLB在线编辑器Three.js AI自动纹理化开发包YOLO 虚幻合成数据生成器3D模型在线转换 1、NSDT 三维场景建模 访问地址&#xff1a;NSDT 编辑器 2、GLTF/GLB在线编辑器 访问地址&#xff1a;GLTF 编辑器 3、Three.js AI自动纹理化开发包 图一为原始模…

C++类与对象(4)—日期类的实现

目录 一、类的创建和方法声明 二 、输出&运算符重载 三、检查合法性 1、获取对应年月的天数 2、初始化 四、实现加等和加操作 1、先写再写 2、先写再写 3、两种方式对比 五、实现自增和--自减 1、自增 2、自减 六、 实现减等和减操作 1、减等天数 2、加负数…

【数据结构/C++】线性表_双链表基本操作

#include <iostream> using namespace std; typedef int ElemType; // 3. 双链表 typedef struct DNode {ElemType data;struct DNode *prior, *next; } DNode, *DLinkList; // 初始化带头结点 bool InitDNodeList(DLinkList &L) {L (DNode *)malloc(sizeof(DNode))…

motionlayout的简单使用

MotionLayout 什么是motionLayout&#xff1f; MotionLayout 是 Android 中的一个强大工具&#xff0c;用于创建复杂的布局动画和过渡效果。它是 ConstraintLayout 的一个子类&#xff0c;继承了 ConstraintLayout 的布局功能&#xff0c;同时添加了动画和过渡的支持。Motion…