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 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,就认为客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个实例发生故障,因为锁变量在其它实例上也有保存,所以客户端仍然可以正常地进行锁操作,锁变量并不会丢失。
加锁过程:
- 客户端获取当前时间;
- 客户端按顺序依次向 N 个 Redis 实例执行加锁操作。同样使用 SETNX 命令,并设置超时时间。如果请求加锁一直超时,则视为加锁失败,向下一个实例执行加锁操作。
- 客户端完成所有加锁操作后,计算整个加锁过程的总耗时。
客户端只有在满足下面的这两个条件时,才能认为是加锁成功:
- 从超过半数(大于等于 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.执行步骤
- 开启事务:使用 MULTI 命令开启事务;
- 入队:接收到命令后并不会立即执行,而是放到等待执行的事务队列里;
- 执行:由 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 脚本是将一系列操作放在一个脚本中原子执行。分布式锁是通过共享的锁变量来限制客户端的并发访问。事务是将一系列操作放到执行队列中,再按顺序原子执行。
相关文章:

Redis并发问题解决方案
目录 前言 1.分布式锁 1.基于单个节点 2.基于多个节点 3.watch(乐观锁) 2.原子操作 1.单命令操作 2.Lua 脚本(多命令操作) 3.事务 1.执行步骤 2.错误处理 3.崩溃处理 总结 前言 在多个客户端并发访问Redis的时候,虽然Redis是单线程执行指令ÿ…...
读取两个文件夹里不同名的文件,处理映射不对应的文件
解决方案:读取两个文件夹里不同名的文件,处理映射不对应的文件 # -*- coding: utf-8 -*- import ospath1 r/home/ubuntu/data/yoloData/images/train2017 path2 r/home/ubuntu/data/yoloData/labels/train2017def read_all_file_name():file_path ./t…...

SpringCloud原理-OpenFeign篇(四、请求原理)
文章目录 前言正文一、书接上回,从代理对象入手二、ReflectiveFeign.FeignInvocationHandler#invoke()三、SynchronousMethodHandler#invoke(...) 的实现原理3.1 invoke(...)源码3.2 executeAndDecode(...) 执行请求并解码 四、如何更换client 的实现 附录附1&#…...

什么是工业物联网(IOT)?这样的IOT平台你需要吗?——青创智通
物联网(IOT)是指在互联网上为传输和共享数据而嵌入传感器和软件的互联设备的广泛性网络。这允许将从物理对象收集的信息(数据)存储在专用服务器或云中。通过分析这些积累的信息,通过提供最优的设备控制和方法,可以实现一个更安全、更方便的社会。在智能家…...
MTK Pump Express 快速充电原理分析
1 MTK PE 1.1 原理 在讲正文之前,我们先看一个例子。 对于一块电池,我们假设它的容量是6000mAh,并且标称电压是3.7V,换算成Wh(瓦时)为单位的值是22.3Wh(6000mAh*3.7V);普通的充电器输出电压电流是5V2A(10W),…...
leetcode刷题记录——1991. 找到数组的中间位置
找到数组的中间位置 给你一个下标从 0 开始的整数数组 nums ,请你找到 最左边 的中间位置 middleIndex (也就是所有可能中间位置下标最小的一个)。 中间位置 middleIndex 是满足 nums[0] nums[1] … nums[middleIndex-1] nums[middleInd…...

跨域攻击分析和防御(上)
点击星标,即时接收最新推文 跨域攻击 在大型公司或大型跨国企业中都拥有自己的内网,跨国公司都有各个子公司一般以建立域林进行共享资源。根据不同职能区分的部门,从逻辑上以主域和子域进行划分以方便统一管理。在物理层使用防火墙将各个子公…...

GEE:梯度提升树(Gradient Boosting Tree)分类教程(样本制作、特征添加、训练、精度、参数优化、贡献度、统计面积)
作者:CSDN @ _养乐多_ 本文将介绍在Google Earth Engine (GEE)平台上进行梯度提升树(Gradient Boosting Tree)分类的方法和代码,其中包括制作样本点教程(本地、在线和本地在线混合制作样本点,合并样本点等),加入特征变量(各种指数、纹理特征、时间序列特征、物候特征…...

ubuntu22.04 arrch64版在线安装redis
脚本 apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5 apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 echo "deb http://archive.ubuntu.com/ubuntu/ trusty main universe restricted multiverse" >…...

篮桥云课-摆玩具
思维好题 一开始掉进了二分的陷阱,发现看看逐个位置的差,我们要分成k段就是要取消k-1个最大的逐差 然后将剩余的加起来就可以了 因为本体保证是从小到大给出的 这一点保证了答案的正确性,自己没想出来 还是太菜了 #include<bits/stdc.h&…...
【python】python进阶知识点
eval函数用法 函数参数:字符串 # 计算表达式 print(eval("12*3")) # 把字符串当成python程序运行 print(eval("random.random()")) # 字符串转成列表 print(type(eval("[1,2,3,4]"))) # 字符串转成字典 print(type(eval("{name…...
LeetCode算法题解(动态规划)|LeetCode322. 零钱兑换、LeetCode279. 完全平方数
一、LeetCode322. 零钱兑换 题目链接:322. 零钱兑换 题目描述: 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一…...

Python Web开发基础知识篇
一,基础知识篇 本片文章会简单地说一些python开发web中所必须的一些基础知识。主要包括HTML和css基础、JavaScript基础、网络编程基础、MySQL数据库基础、Web框架基础等知识。 1,Web简介 Web,全称为World Wide Web,也就是WWW,万…...

企业计算机服务器中了360勒索病毒怎么办,360勒索病毒解密文件恢复
计算机技术的不断发展,为企业的生产运营提供了极大便利,不仅提升了办公效率,还促进了企业的发展。企业计算机在日常工作中一定加以防护,减少网络威胁事件的产生,确保企业的生产生产运营。最近,网络上的360后…...
LeetCode无重复字符的最长字符串的Java实现
题目 给定一个字符串 s ,请你找出其中不含有重复字符的 最长连续子字符串 的长度。 示例 1: 输入: s "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子字符串是 "abc",所以其长度为 3。示例 2: 输入: s "bbbbb" 输…...

opencv-图像平滑
高斯平滑 高斯平滑即采用高斯卷积核对图像矩阵进行卷积操作。高斯卷积核是一个近似服从高斯分布的矩阵,随着距离中心点的距离增加,其值变小。这样进行平滑处理时,图像矩阵中锚点处像素值权重大,边缘处像素值权重小。 import cv2 …...

【开源】基于Vue.js的天然气工程运维系统的设计和实现
项目编号: S 022 ,文末获取源码。 \color{red}{项目编号:S022,文末获取源码。} 项目编号:S022,文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统角色分类2.2 核心功能2.2.1 流程…...

数据丢失抢救神器之TOP10 Android 数据恢复榜单
在快节奏的数字时代,我们的生活越来越与智能手机交织在一起,使它们成为重要数据和珍贵记忆的存储库。由于意外删除、软件故障或硬件故障而丢失数据可能是一种痛苦的经历。值得庆幸的是,技术领域提供了 Android 数据恢复软件形式的解决方案。这…...

梨花声音教育,动作电影中配音也能带来听见“冲击力”
在为动作电影提供配音服务时,配音员家需要通过声音的力量来增强画面上的动作张力、传递角色的活力,以及呈现出电影中的紧迫感。动作片充斥着快节奏的场景交替、紧张的格斗和惊险的逃逸等元素,配音需要精确、生动且充满动力。为动作电影配音应…...
Elaticsearch学习
Elaticsearch 索引 1、索引创建 PUT /index_v1 {"settings": {"number_of_shards": 3,"number_of_replicas": 1},"mappings": {"properties": {"aaa": {"type": "keyword","store&qu…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...
laravel8+vue3.0+element-plus搭建方法
创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...

【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...

Ubuntu系统复制(U盘-电脑硬盘)
所需环境 电脑自带硬盘:1块 (1T) U盘1:Ubuntu系统引导盘(用于“U盘2”复制到“电脑自带硬盘”) U盘2:Ubuntu系统盘(1T,用于被复制) !!!建议“电脑…...

【Veristand】Veristand环境安装教程-Linux RT / Windows
首先声明,此教程是针对Simulink编译模型并导入Veristand中编写的,同时需要注意的是老用户编译可能用的是Veristand Model Framework,那个是历史版本,且NI不会再维护,新版本编译支持为VeriStand Model Generation Suppo…...
【安全篇】金刚不坏之身:整合 Spring Security + JWT 实现无状态认证与授权
摘要 本文是《Spring Boot 实战派》系列的第四篇。我们将直面所有 Web 应用都无法回避的核心问题:安全。文章将详细阐述认证(Authentication) 与授权(Authorization的核心概念,对比传统 Session-Cookie 与现代 JWT(JS…...