Redis并发问题解决方案

news2024/11/13 15:01:20

目录

前言

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.coloradmin.cn/o/1245338.html

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

相关文章

Banana Pi [BPi-R3-Mini] 回顾和主线 ImmortalWrt 固件支持

BananaPi BPi-R3 Mini 采用 MediaTek 830(4 个 A53,最高 2.0 GHz),具有 2 个 2.5 GbE、AX4200 2.4G/5G 无线和 USB 2.0 端口。它还具有两个 M.2 连接器,可用于 NVMe SSD 和 5G 模块(板上包含 Nano SIM 插槽…

绝地求生:PGC 2023 赛事直播期间最高可获:2000万G-Coins,你还不来吗?

今年PGC直播期间将有最高2000万G-Coin掉落,究竟花落谁家咱们拭目以待 公告原文:Watch PGC 2023 Live And Earn G-Coins! 如何赚取高额G-Coin? Throughout the PGC 2023, an astounding 20,000,000 G-Coins will be up for grabs as part of …

常见树种(贵州省):013桉树、米槠、栲类

摘要:本专栏树种介绍图片来源于PPBC中国植物图像库(下附网址),本文整理仅做交流学习使用,同时便于查找,如有侵权请联系删除。 图片网址:PPBC中国植物图像库——最大的植物分类图片库 一、桉树 …

京东数据分析(京东大数据):2023年10月京东手机行业品牌销售排行榜

鲸参谋监测的京东平台10月份手机市场销售数据已出炉! 根据鲸参谋平台的数据显示,今年10月份,京东平台手机行业的销量约340万,环比增长约11%,同比则下滑约2%;销售额为108亿,环比增长约17%&#x…

c#数据库:vs2022 加入mysql数据源

网上有VS2019连接MySQL数据库的,那么VS2022,VS2023如果和连接到mysql数据库呢,这里总结一下我的经历: 1、首先下载ODBC驱动安装包 当前下载地址:https://dev.mysql.com/downloads/connector/odbc/ 2、ODBC安装 下载完…

小程序可拖拽按钮

你有没有遇到过在页面中有一个固定在某个位置的按钮,永远的挡住了你想要看的区域? 在小程序的列表页面中,常常会有一个提报的入口固定在右下角,如果这个按钮不可拖动的话,可能会挡住下面的事件,让用户操作起…

PyTorch包

进入PyTorch的官网: pytorch GitHub 点击GitHub: 进入PyTorch的主目录: 进入Vision reference: detection: 这就是我们在训练过程中会使用到的文件了:

如何使用springboot服务端接口公网远程调试——实现HTTP服务监听

🌈个人主页:聆风吟 🔥系列专栏:网络奇遇记、数据结构、算法模板 🔖少年有梦不应止于心动,更要付诸行动。 文章目录 📋前言一. 本地环境搭建1.1 环境参数1.2 搭建springboot服务项目 二. 内网穿透…

机器学习笔记 - 复杂任务的CNN组合

基础CNN架构可通过多种方式进行组合和扩展,从而解决更多、更复杂的任务。 1. 分类和定位 在分类和定位任务中,你不仅需要说出在图像中找到的物体的类别,而且还需指出物体显现在图像中的边界框坐标。这类任务假设在图像中只有一个物体实例。 这个任务可通过在典型的分类网络…

多数据库使用django-apscheduler时,migrate后并不能生成django_apscheduler_djangojob表的问题

先说一下django-apscheduler定时器的使用过程: django-apscheduler基本使用 1.安装django-apscheduler代码如下(示例): pip install django-apscheduler 2.配置settings.py的INSTALLED_APPS代码如下(示例&#xff09…

B033-Servlet交互 JSP

目录 ServletServlet的三大职责跳转:请求转发和重定向请求转发重定向汇总请求转发与重定向的区别用请求转发和重定向完善登录 JSP第一个JSP概述注释设置创建JSP文件默认字符编码集 JSP的java代码书写JSP的原理三大指令九大内置对象改造动态web工程进行示例内置对象名…

关于elementui和ant design vue无法禁止浏览器自动填充问题

以and design vue 为例&#xff1a; 图标用来显隐账号密码 html&#xff1a; <a-form-model-item label"账号密码:" prop"password"><a-input v-if"passwordTab" ref"passwordInput" v-model"form.password" typ…

如何进行MySQL的主从复制(MySQL5.7)

背景&#xff1a;在一些Web服务器开发中&#xff0c;系统用户在进行数据访问时&#xff0c;基本都是直接操作数据库MySQL进行访问&#xff0c;而这种情况下&#xff0c;若只有一台MySQL服务器&#xff0c;可能会存在如下问题 数据的读和写的所有压力都会由一台数据库独…

利用STM32CubeMX解读时钟树

1&#xff0c;低速时钟 LSE是外部晶振作时钟源&#xff0c;主要提供给实时时钟模块&#xff0c;所以一般采用32.768KHz。LSI是由内部RC振荡器产生&#xff0c;也主要提供给实时时钟模块&#xff0c;频率大约为40KHz。(LSE和LSI)只是提供给芯片中的RTC(实时时钟)及IWDG(独立看门…

【大数据】Docker部署HMS(Hive Metastore Service)并使用Trino访问Minio

本文参考链接置顶&#xff1a; Presto使用Docker独立运行Hive Standalone Metastore管理MinIO&#xff08;S3&#xff09;_hive minio_BigDataToAI的博客-CSDN博客 一. 背景 团队要升级大数据架构&#xff0c;需要摒弃hadoop&#xff0c;底层使用Minio做存储&#xff0c;应用…

ebpf实战(一)-------监控udp延迟

问题背景: 为了分析udp数据通信中端到端的延迟,我们需要对整个通信链路的每个阶段进行监控,找出延迟最长的阶段. udp接收端有2个主要路径 1.数据包到达本机后&#xff0c;由软中断处理程序将数据包接收并放入udp socket的接收缓冲区 数据接收流程 2. 应用程序调用recvmsg等a…

MySQL数据库_01

Web后端开发_02 数据库介绍 什么是数据库&#xff1f; 数据库&#xff1a;DataBase&#xff08;DB&#xff09;&#xff0c;是存储和管理数据的仓库 数据库管理系统&#xff1a;DataBase Management System (DBMS)&#xff0c;操纵和管理数据库的大型软件。SQL&#xff1a;St…

Node.js入门指南(二)

目录 http模块 创建http服务端 浏览器查看 HTTP 报文 获取 HTTP 请求报文 设置响应报文 网页资源的基本加载过程 静态资源服务 hello,大家好&#xff01;上一篇文章我们对Node.js进行了初步的了解&#xff0c;并介绍了Node.js的Buffer、fs模块以及path模块。这一篇文章主…

51单片机应用从零开始(八)·循环语句(for循环、while 语句、do‐while 语句)

51单片机应用从零开始&#xff08;七&#xff09;循环语句&#xff08;if语句&#xff0c;swtich语句&#xff09;-CSDN博客 目录 1. 用for 语句控制蜂鸣器鸣笛次数 2. 用while 语句控制 LED 3. 用 do‐while 语句控制 P0 口 8 位 LED 流水点亮 1. 用for 语句控制蜂鸣器鸣笛…

6.3.WebRTC中的SDP类的结构

在上节课中呢&#xff0c;我向你介绍了sdp协议&#xff0c; 那这节课呢&#xff0c;我们再来看看web rtc中。是如何存储sdp的&#xff1f;也就是sdp的类结构&#xff0c;那在此之前呢&#xff1f;我们先对sdp的内容啊&#xff0c;做一下分类。因为在上节课中呢&#xff0c;虽然…