Redis核心技术与实战【学习笔记】 - 20.Redis原子操作及并发访问

news2025/1/13 7:59:24

概述

使用 Redis 时,不可避免地会遇到并发访问的问题,比如说如果多个用户同时下单,就会对缓存在 Redis 中的商品库存并发更新。一旦有了并发写操作,数据就会被修改,如果我们没有对并发写请求做好控制,就可能导致数据被改错,影响业务的正常使用(例如,库存数据错误,导致下单异常)。

为了保证并发访问的正确性,Redis 提供了两种方法,分别是加锁和原子操作。当一个客户端获得锁后,就会一直持有这把锁,直到客户端完成数据更新,才释放这把锁。

但是用锁会有两个问题:

  • 一个是,如果加锁操作多,会降低系统的并发访问性能。
  • 第二个是,Redis 客户端需要加锁时,需要用到分布式锁,而分布式锁实现复杂,需要用额外的存储系统来提供枷锁解锁操作。

原子操作是另一种提供并发访问控制的方法。原子操作是指执行过程保持原子性的操作,而且原子操作执行时并不需要加锁,实现了无锁操作。这样一来,既能保证并发控制,还能减少对系统并发性能的影响。


1.并发访问中需要对什么进行控制?

并发访问控制是指对多个客户端访问操作同一份数据的过程进行控制,以保证任何一个客户端发送的操作在 Redis 实例上执行具有互斥性。

并发访问控制对应的操作主要是数据修改操作。当客户端需要修改数据时,基本流程分成两步:

  1. 客户端先把数据读取到本地,在本地进行修改
  2. 客户端修改完数据后,再写回 Redis。

这个流程叫做“读取 - 修改 - 写回”操作(Read-Modify-Write,简称为 RMW 操作)。当有多个客户端对同一份数据执行 RMW 操作的话,我们就需要让 RMW 操作 涉及的代码以原子性方式执行。访问同一份数据的 RMW 操作 的代码,就叫做临界区代码。

不过,当有多个客户端并发执行临界区代码时,就会存在一些潜在问题。例如,客户端要对商品库存执行扣减 1 的操作,伪代码如下:

current = GET(id)
current--
SET(id, current)
  1. 可以看到,客户先根据商品 id,从 Redis 中读取商品当前的库存值 current(对应 Read)
  2. 然后,客户端对库存值减 1(对应 Modify)
  3. 再把库存值写回 Redis (对应 Write)

如果,我们对临界区代码的执行没有控制机制,就会出现数据更新错误。在刚才的例子中,假设现在有两个客户端 A 和 B,同时执行刚才的临界区代码,就会出现错误。
在这里插入图片描述

  1. 在客户端 A 在 t1 时读取库存值 10 并扣减 1
  2. 在 t2 时,客户端 A 还没有把扣减值后的库存值 9 写回 Redis。此时,客户端 B 读取到库存值 10,也扣减了 1, B 记录的库存值也为 9 了。
  3. 等到 T3 时刻, A 往 Redis 写回了库存值 9。
  4. T4 时刻,B 也写回了库存值 9。

如果按正确的逻辑处理,客户端 A 和 B 对库存值各做了一次扣减,库存值应该为 8.所以,这里的库存值明显更新错了。

出现这个现象的原因是,临界区代码中的客户端读取、更新、再写回涉及了三个操作,而这三个操作执行时并不具有互斥性,多个客户端基于相同的初始值进行修改,而不是基于前一个客户端修改后的值再修改。

前面,我们已经解释过,虽然加锁保证了互斥性,但是加锁也会导致系统并发性能降低。接下来,我们了解下 Redis 中的原子操作。

2.Redis 的两种原子操作方法

Redis 的原子操作采用了两种方法:

  1. 把多操作在 Redis 中实现成一个操作,也就是单命令操作。
  2. 把多个操作写到 Lua 脚本中,以原子性方式执行单个脚本。

单命令操作

Redis 使用单线程来串行处理客户端的请求命令,所以,当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。

当然,Redis 的快照生成、AOF 重写这些操作可以使用后台线程或子进程执行,也就是和主线程的操作并行执行。不过他们都是只读操作,不会修改数据,所以,不需要对它们进行并发控制。

虽然 Redis 的单个命令可以原子性地执行,但是在实际应用中,数据修改时可能包含多个操作,至少包括读数据、数据增减、写回数据三个操作,这显然不是单个命令。

Redis 提供了 INCR/DECR 命令,把这三个操作变为一个原子操作了。INCR/DECR 命令可以对数据进行增值 / 减值操作,而它们本身就是单个命令操作,Redis 在执行它们时,本身就具有互斥性。

比如说,在刚才的库存扣减例子中,客户端可以使用下面的代码,也不用担心出现库存值扣减错误的问题。

DECR id

所以,如果我们执行的 RMW 操作对数据增减值的话, Redis 提供的原子操作 INCR 和 DECR 可以直接绑我们进行并发控制。

Lua 脚本

如果,我们要执行的操作不是简单地增减数据,而是有更新复杂的判断逻辑或者其他操作,那么 Redis 的单命令操作已经无法保证多个操作的互斥执行了。这个时候,就需要用到 Lua 脚本

Redis 会把整个 Lua 脚本 作为一个整体执行,在执行过程中不会被其他命令打断,从而保证了 Lua 脚本 中操作的原子性。如果,我们有多个操作要执行,但是又无法使用 INCR/DECR 这种命令操作来实现,就可以把这些要执行的命令编写到一个 Lua 脚本 中。然后,我们可以使用 Redis 的 EVAL 命令来执行脚本。这样一来,这些操作在执行的时就具有了互斥性。

我们可以把 访问次数加 1、判断访问次数是否为 1,以及设置过期时间这三个操作写入一个 Lua 脚本,如下所示:

local current
current = redis.call("incr", KEYS[1])
if tonumber(current) == 1 then
	redis.call("expire", KEYS[1], 60)
end
return current

假设编写的脚本的名称为 lua.script,我们就接着使用 Redis 客户端,带上 eval 选项,来执行该脚本。脚本所需要的参数通过以下命令中的 key 和 arg 进行传递。

redis-cli --eval lua.script [key key2 ...] , [arg arg2 ...]

另外还可以在 Redis 客户端内,执行脚本:eval lua.script key-num [key key2 ...] [arg arg2 ...]

  • [key key2 ...]: 表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
  • [arg arg2 ...]: 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

例如上面的 Lua 脚本的实际执行如下(其中, KEYS[1] 为 lua1):

chenjian@DESKTOP-Q24SEP3:~$ vim lua.script
chenjian@DESKTOP-Q24SEP3:~$ ./redis-6.2.12/src/redis-cli --eval lua.script lua1 ,
(integer) 1
chenjian@DESKTOP-Q24SEP3:~$ ./redis-6.2.12/src/redis-cli --eval lua.script lua1 ,
(integer) 2

这样一来,访问次数加 1、判断访问次数是否为 1,以及设置过期时间这三个操作就可以原子性的执行了。即使客户端有多个线程同时执行这个脚本,Redis 也会依次串行的执行脚本代码,避免并发操作带来的数据错误。

不过需要注意的是,如果把很多操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的时间增加,同样也会降低 Redis 。所以,给你一个小建议:在编写 Lua 脚本时,你要避免把不需要做并发控制的操作写入 Lua 脚本中。

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

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

相关文章

机器学习中的有监督学习和无监督学习

有监督学习 简单来说,就是人教会计算机学会做一件事。 给算法一个数据集,其中数据集中包含了正确答案,根据这个数据集,可以对额外的数据希望得到一个正确判断(详见下面的例子) 回归问题 例如现在有一个…

套路化编程 C# winform 自适应缩放布局

本例程实现基本的自适应缩放布局。 在本例程中你将会学习到如何通过鼠标改变界面比例(SplitContainer)、如何使用流布局(FlowLayoutPanel)排列控件,当然首先需要了解如何设置控件随窗口缩放。 目录 创建项目 ​编辑…

【npm】安装全局包,使用时提示:不是内部或外部命令,也不是可运行的程序或批处理文件

问题 如图,明明安装Vue是全局包,但是使用时却提示: 解决办法 使用以下命令任意一种命令查看全局包的配置路径 npm root -g 然后将此路径(不包括node_modules)添加到环境变量中去,这里注意,原…

101 C++内存高级话题 内存池概念,代码实现和详细分析

零 为什么要用内存池? 从前面的知识我们知道,当new 或者 malloc 的时候,假设您想要malloc 10个字节, char * pchar new char[10]; char *pchar1 malloc(10); 实际上编译器为了 记录和管理这些数据,做了不少事情&…

全自动网页生成系统重构版源码

全自动网页生成系统重构版源码分享,所有模板经过精心审核与修改,完美兼容小屏手机大屏手机,以及各种平板端、电脑端和360浏览器、谷歌浏览器、火狐浏览器等等各大浏览器显示。 为用户使用方便考虑,全自动网页制作系统无需繁琐的注…

陶哲轩如何用 GPT-4 辅助数学研究

关于陶哲轩(Terence Tao)用 GPT-4 进行数学研究的话题始于陶本人在 微软 Unlocked 上发表的 Embracing Change and Resetting Expectations 一文。文中提到: …… I could feed GPT-4 the first few PDF pages of a recent math preprint and…

Vue3动态CSS

Vue3动态CSS 动态css值动态css对象module模式 动态css值 <template><div class"div">动态css</div> </template><script setup langts> import {ref} from vueconst style ref(blue) </script><style scoped> .div{colo…

绕过无限debugger

目标网站 aHR0cHM6Ly93d3cuaWMubmV0LmNuL3NlYXJjaC81NTAuaHRtbA 改为Firefox浏览器进行调试,取消勾选 Pause on debugger statement 参考:https://blog.csdn.net/kdl_csdn/article/details/135137490

MySQL进阶之锁(表级锁,元数据锁,意向锁)

表级锁 介绍 表级锁&#xff0c;每次操作锁住整张表。锁定粒度大&#xff0c;发生锁冲突的概率最高&#xff0c;并发度最低。应用在MyISAM、 InnoDB、BDB等存储引擎中。 对于表级锁&#xff0c;主要分为以下三类&#xff1a; 表锁 元数据锁&#xff08;meta data lock&…

算法笔记刷题日记——3.简单入门模拟 3.1简单模拟

刷题日记 3.1 简单模拟 此类题型根据题目描述进行代码的编写&#xff0c;考察代码能力&#xff0c;刷题记录如下&#xff1a; B1001 B1032 B1016 B1026 B1046 B1008 B1012 B1018 A1042 A1046 A1065 B1010 A1002 A1009 错题记录 B1008 数组元素循环右移问题 一个数组_A_中存有…

C语言小游戏:贪吃蛇(游戏开发的环境和功能介绍)

❀❀❀ 文章由不准备秃的大伟原创 ❀❀❀ ♪♪♪ 若有转载&#xff0c;请联系博主哦~ ♪♪♪ ❤❤❤ 致力学好编程的宝藏博主&#xff0c;代码兴国&#xff01;❤❤❤ 生命不停&#xff0c;学习不止。铁汁们&#xff0c;我是大伟&#xff0c;欢迎来到大伟的游戏时间&#xff0c…

【论文阅读笔记】InstantID : Zero-shot Identity-Preserving Generation in Seconds

InstantID:秒级零样本身份保持生成 理解摘要Introduction贡献 Related WorkText-to-image Diffusion ModelsSubject-driven Image GenerationID Preserving Image Generation Method实验定性实验消融实验与先前方法的对比富有创意的更多任务新视角合成身份插值多身份区域控制合…

【遥感入门系列】遥感电磁辐射与遥感过程

遥感电磁辐射是比较难理解也是非常重要的内容&#xff0c;对于一般学习遥感专业的人来说&#xff0c;只需要学习个大概&#xff0c;这个大概主要包括你需要理解几个概念以及能从电磁辐射原理上解释一些遥感现象&#xff0c;进而为遥感过程的理解打下一个基础&#xff0c;如果你…

85.如何设计高并发系统(缓存、限流、熔断、降级)

文章目录 一&#xff1a;简介二&#xff1a;什么是高并发1、高并发的定义2、高并发的特点3、高并发场景和应用4、高并发的影响 三&#xff1a;高并发应对策略四&#xff1a;缓存1、浏览器缓存2、客户端缓存3、CDN缓存4、反向代理缓存5、本地缓存6、分布式缓存7、缓存问题8、缓存…

【代码随想录-哈希表】两个数组的交集

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…

STM32搭建开发环境

常用开发工具简介 集成开发环境 MDK&#xff1a;全名RealViewMDK&#xff0c;是Keil公司&#xff08;已被ARM收购的&#xff09;一款集成开发环境&#xff0c;界面美观&#xff0c;简单易用&#xff0c;是STM32最常用的集成开发环境EWARM&#xff1a;IAR公司的一款集成开发环…

Go语言Gin框架安全加固:全面解析SQL注入、XSS与CSRF的解决方案

前言 在使用 Gin 框架处理前端请求数据时&#xff0c;必须关注安全性问题&#xff0c;以防范常见的攻击。本文将探讨 Gin 框架中常见的安全问题&#xff0c;并提供相应的处理方法&#xff0c;以确保应用程序的稳健性和安全性。 处理前端请求数据时&#xff0c;确保应用程序的…

MFC结构体写入文件和读取

先定义一个结构体&#xff1b; struct myTxc { char c; CString name; int value; } txc; 读和写的菜单代码&#xff1b; void CjgtrwView::On32771() {// TODO: 在此添加命令处理程序代码CFile file(_T("test1.txt"), CFile::modeCreate | CFile::mod…

【Java 数据结构】String进阶

字符串常量池 1. 创建对象的思考2. 字符串常量池(StringTable)3. 再谈String对象创建 1. 创建对象的思考 下面两种创建String对象的方式相同吗&#xff1f; public static void main(String[] args) {String s1 "hello";String s2 "hello";String s3 …

Coil:Android上基于Kotlin协程的超级图片加载库

Coil&#xff1a;Android上基于Kotlin协程的超级图片加载库 1. coil简介 在当今移动应用程序的世界中&#xff0c;图片加载是一个不可或缺的功能。为了让应用程序能够高效地加载和显示图片&#xff0c;开发人员需要依赖于强大的图片加载库。而今天&#xff0c;我将向大家介绍…