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 脚本是将一系列操作放在一个脚本中原子执行。分布式锁是通过共享的锁变量来限制客户端的并发访问。事务是将一系列操作放到执行队列中,再按顺序原子执行。

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

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

相关文章

【间歇振荡器2片555时基仿真】2022-9-24

缘由multisim出现这个应该怎么解决吖,急需解决-嵌入式-CSDN问答 输出一定要有电阻分压才能前后连接控制否则一定报错。

Python自动化生成漂亮的测试报告

📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢交流讨论:欢迎加入我们一起学习!📢资源分享:耗时200小时精选的「软件测试」资…

五种多目标优化算法(MOJS、NSGA3、MOGWO、NSWOA、MOPSO)求解微电网多目标优化调度(MATLAB代码)

一、多目标优化算法简介 (1)多目标水母搜索算法MOJS 多目标优化算法:多目标水母搜索算法MOJS(提供MATLAB代码)_水母算法-CSDN博客 (2)NSGA3 NSGA-III求解微电网多目标优化调度(M…

acwing算法基础之数学知识--求卡特兰数

目录 1 基础知识2 模板3 工程化 1 基础知识 题目:给定n个0和n个1,它们将按照某种顺序排成长度为2n的序列,求它们能排成的所有序列中,能够满足任意前缀序列中0的个数都不少于1的个数的序列有多少个? 输出的答案对 1 0 …

【云原生 Prometheus篇】Prometheus的动态服务发现机制与认证配置

目录 一、Prometheus服务发现的方式1.1 基于文件的服务发现1.2 基于consul的服务发现1.3 基于 Kubernetes API 的服务发现1.3.1 简介1.3.2 基于Kurbernetes发现机制的部分配置参数 二、实例一:部署基于文件的服务发现2.1 创建用于服务发现的文件2.2 修改Prometheus的…

yo!这里是c++11重点新增特性介绍

目录 前言 列表初始化 { }初始化 initializer_list类 类型推导 auto decltype 范围for 右值引用与移动语义 左值引用和右值引用 移动语义 1.移动构造 2.移动赋值 3.stl容器相关更新 右值引用和万能引用 完美转发 关键字 default delete final和override …

西米支付:简单介绍一下支付公司的分账功能体系

随着互联网的普及和电子商务的快速发展,支付已经成为人们日常生活的重要组成部分。支付公司作为第三方支付平台,为消费者和商家提供了便捷、安全的支付方式。而在支付领域中,分账功能是一个非常重要的功能,它可以帮助企业实现资金…

C语言——从终端(键盘)将 5 个整数输入到数组 a 中,然后将 a 逆序复制到数组 b 中,并输出 b 中 各元素的值。

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h> int main() {int i;int a[5];int b[5];printf("输入5个整数&#xff1a;\n");for(i0;i<5;i){scanf("%d",&a[i]);}printf("数组b的元素值为&#xff1a;\n");for(i4;i>0;i--…

Javascript每天一道算法题(十五)——轮转数组_中等(一行解决轮转数组)

文章目录 1、问题2、示例3、解决方法&#xff08;1&#xff09;方法1——while遍历&#xff08;较为复杂&#xff0c;不推荐&#xff09;&#xff08;2&#xff09;方法2&#xff08;直接截取后插入&#xff0c;推荐&#xff09;&#xff08;3&#xff09;方法3——优化方法2&a…

jQuery_03 dom对象和jQuery对象的互相转换

dom对象和jQuery对象 dom对象 jQuery对象 在一个文件中同时存在两种对象 dom对象: 通过js中的document对象获取的对象 或者创建的对象 jQuery对象: 通过jQuery中的函数获取的对象。 为什么使用dom或jQuery对象呢&#xff1f; 目的是 要使用dom对象的函数或者属性 以及呢 要…

python -opencv 轮廓检测(多边形,外接矩形,外接圆)

python -opencv 轮廓检测(多边形&#xff0c;外接矩形&#xff0c;外接圆) 边缘检测步骤: 第一步&#xff1a;读取图像为灰度图 第二步&#xff1a;进行二值化处理 第三步&#xff1a;使用cv2.findContours对二值化图像提取轮廓 第三步&#xff1a;将轮廓绘制到图中 代码如下…

Hibernate的三种状态

1.瞬时状态(Transient) 通过new创建对象后&#xff0c;对象并没有立刻持久化&#xff0c;他并未对数据库中的数据有任何的关联&#xff0c;此时java对象的状态为瞬时状态&#xff0c;Session对于瞬时状态的java对象是一无所知的&#xff0c;当对象不再被其他对象引用时&#xf…

【TL431+场效应管组成过压保护电路】2022-3-22

缘由这个稳压三极管是构成的电路是起到保护的作用吗&#xff1f;-硬件开发-CSDN问答

HTML5+ API 爬坑记录

背景: 有个比较早些使用5开发的项目, 最近两天反馈了一些问题, 解决过程在此记录; 坑1: plus.gallery.pick 选择图片没有进入回调 HTML5 API Reference 在 联想小新 平板电脑上选择相册图片进行上传时, 打开相册瞬间 应用会自动重启, 相册倒是有打开, 不过应用重启了, 导…

[原创](免改BIOS)使用Clover升级旧电脑-(高阶玩法)让固态硬盘内置Win11 PE启动系统

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XXQQ: 643439947 个人网站: 80x86汇编小站 https://www.x86asm.org 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、Delphi…

【算法专题】滑动窗口—无重复字符的最长子串

力扣题目链接&#xff1a;无重复字符的最长子串 一、题目解析 二、算法原理 解法一&#xff1a;暴力解法&#xff08;时间复杂度最坏&#xff1a;O(N)&#xff09; 从每一个位置开始往后枚举&#xff0c;在往后寻找无重复最长子串时&#xff0c;可以利用哈希表来统计字符出现…

手机APP-MCP走蓝牙无线遥控智能安全帽~执法记录仪~拍照录像,并可做基础的配置,例如修改服务器IP以及配置WiFi等

手机APP-MCP走蓝牙无线遥控智能安全帽~执法记录仪~拍照录像,并可做基础的配置,例如修改服务器IP以及配置WiFi等 手机APP-MCP走蓝牙无线遥控智能安全帽~执法记录仪~拍照录像,并可做基础的配置,例如修改服务器IP以及配置WiFi等&#xff0c; AIoT万物智联&#xff0c;智能安全帽…

JVM之jvisualvm多合一故障处理工具

jvisualvm多合一故障处理工具 1、visualvm介绍 VisualVM是一款免费的&#xff0c;集成了多个 JDK 命令行工具的可视化工具&#xff0c;它能为您提供强大的分析能力&#xff0c;对 Java 应 用程序做性能分析和调优。这些功能包括生成和分析海量数据、跟踪内存泄漏、监控垃圾回…

SpringBoot:异步任务基础与源码剖析

官网文档&#xff1a;How To Do Async in Spring | Baeldung。 Async注解 Spring框架基于Async注解提供了对异步执行流程的支持。 最简单的例子是&#xff1a;使用Async注解修饰一个方法&#xff0c;那么这个方法将在一个单独的线程中被执行&#xff0c;即&#xff1a;从同步执…

系列六、Spring整合单元测试

一、概述 Spring中获取bean最常见的方式是通过ClassPathXmlApplicationContext 或者 AnnotationConfigApplicationContext的getBean()方式获取bean&#xff0c;那么在Spring中如何像在SpringBoot中直接一个类上添加个SpringBootTest注解&#xff0c;即可在类中注入自己想要测试…