Redis核心技术与实战-学习笔记(二十九):Redis并发控制
一.需要并发控制的原因
- Redis不可避免的会遇到并发访问问题,比如多用户同时下单,就会对缓存在Redis中的商品库存并发更新,一旦有了并发操作,数据就会被修改,如果我们没有对并发写请求做好控制,就可能导致数据被改错,影响业务正常使用(数据库存数据错误,导致下单异常)
二.解决方案
加锁和原子性
加锁:
- 在读取数据前,客户端需要先获得锁,否则需要等待。
- 当一个客户端获得锁后,就会一直持有这把锁直到客户端完成数据更新才释放这把锁。
存在问题:
- 加锁操作过多,会降低系统的并发访问性能;
- Redis客户端加锁,需要分布式锁实现复杂,需要额外的存储系统来提升加锁操作。
原子操作是另一种提升并发访问控制的方法
三.原子操作
- 原子操作:执行过程中保持原子性的操作,而原子操作执行时不需要加锁实现了无锁操作。
- 这样既能保证并发控制还能减少对系统开发性能的影响。
四.并发访问中需要对什么进行控制
- 并发访问控制:对多个客户端访问操作同一份数据的过程进行控制,以保证任何一个客户端发送的操作在Redis实例上执行时具有互斥性。
- 例如:客户端A访问操作在执行时,客户端B操作需要等到A操作结束后才能执行。
并发访问控制对应的操作主要是数据修改操作。
当客户端修改数据时,基本流程分为两步:
- 客户端先把数据读取到本地,在本地进行修改;
- 客户端修改完数据后,在写回Redis
RMW操作:
读取-修改-写回(Read-Modify-Write)
- 当有多个客户端对同一份数据进行RMW操作的话,我们就需要让RMW操作涉及的代码以原子性方式执行。
- 访问同一份数据的RMW操作代码,就叫做临界区代码。
五.多客户端更新商品库存
current=GET(id)
current--
SET(id,current)
- 客户端首先会根据商品id
- 从Redis中读取商品当前的库存值current(Read)
- 客户端对库存值减一(Modify)
- 再把库存值写回Redis(write)
- 当有多个客户端执行这段代码时,这就是一份临界区代码。
假设现在有两个客户端A和B,同时执行刚才的临界区代码就会出现错误:
- 客户端A在t1时去库存值10并扣减1
- t2时,客户端A还没有把扣除后的库存值9写回Redis
- 此时,客户端B读到库存值10也减扣1,B记录的库存值也为9
- 等到t3时,A往Redis写回库存值9
- t4时,B写回库存值9
错误:
- 客户端A和B对库存做了一次扣减,库存值应该为8所以这里的库存值明显更新错了。
产生原因
- 临界区代码中的客户端读取数据,更新数据,写回数据涉及了三个操作,而这三个操作在执行时候不具有互斥性。
- 多个客户端基于相同的初始值进行修改,而不是基于前一个客户端修改后的值进行。
解决方案一:加锁
- 用锁将并行操作变成串行操作,串行操作具有互斥性,
- 当一个客户端持有锁后,其他客户端只能等待锁释放后,才能拿到锁修改。
LOCK()
current=GET(id)
current--;
SET(id,current)
UNLOCK()
加锁会导致系统并发性能降低
后,才能拿到锁修改。
- 当客户端A加锁执行操作时;
- 客户端B,C就需要等待
- A释放锁后,假设B拿到锁后,那么C还需要继续等待
- t1时段内只有A访问共享数据
- t2时段只有B能访问共享数据
- 系统的并发性能下降
t1 | A拿锁执行互斥操作 |
B等待 |
C等待 |
t2 | A释放锁执行释放操作 | B拿锁执行互斥操作 | C等待 |
五.原子操作
原子性操作也能实现并发控制,但是原子性操作对系统并发性能的影响较小
Redis的两种原子操作:
- 把多个操作在Redis中实现成一个操作,也就是单命令操作
- 把多个操作写到一个Lua脚本中,以原子性的方式执行单个lua脚本。
六.Redis本身的单命令操作
- Redis是使用单线程来串行处理客户端的请求操作命令;
- 当Redis执行某个命令操作,其他命令是无法执行的,这相当于命令操作时互斥操作。
- 当然,Redis的快照生成,AOF重写这些操作,可以使用后台线程或者子进程执行,也就是和主线程的操作并行执行
- 不过这些数据只是读取数据,不会修改数据,所以我们并需要对它们做并发操作。
存在问题:
虽然Redis单个命令操作可以原子性的执行,但是在实际操作中,数据修改可能包含多个操作,至少包含读数据,数据增减,写回数据三个操作,这显然就不是单个命令操作
- Redis提供了INCR/DECR 命令
DECR 命令
如果我们执行RMW操作是对数据进行增减值的,Redis提供的原子操作INCR 和 DECR可以直接帮助我们进行并发控制。
如果我们执行后的操作并不能简单的增减数据,而是更加复杂的判断逻辑或者其他操作,那么Redis单命令操作已经无法保证多个操作的互斥执行
七.Lua脚本
- Redis会把Lua脚本作为一个整体执行,在执行过程中不会被其他命令打断,从而保证lua操作的原子性
- 如果我们有多个操作要执行,又无法用INCR/DECR这种命令操作实现,就可以把这些要执行的操作编写到Lua脚本中
- 使用Redis的eval命令执行脚本
- 这样代码执行时候就具有互斥性
八.Lua的使用
- 当一个业务应用的访问要不过户增加时,我们有时需要限制某个客户端在一定时间范围内的访问次数
- 比如爆款商品的购买限流,社交网络中每分钟点赞次数限制等
解决方案
- 我们把客户端IP作为key,把客户端的访问次数作为value。客户端每访问一次我们就用INCR增加访问次数。
- 这种场景下,客户端限流其实同时包含对访问次数和时间范围的限制,例如每分钟的访问次数不能超过20
- 我们在客户端第一次访问时,给对应键值对设置过期时间,丽日设置为60s后过期。
- 同时客户端每次访问时,我们读取客户端当前的访问次数,如果次数超过阈值,就报错,限制客户端再次访问
//获取ip对应的访问次数
current=GET(ip)
//如果超过访问次数20就报错
IF current==null && current >20throw new exception
else{//如果访问次数不足20次,增加一次访问计数value=INCR(ip)if(value == 1){//如果是第一次访问,将键值对的过期时间设置为60s后expire (ip,60)}
}
- 对于这些操作,我们同样需要保证他们的原子性。
- 否则如果客户端使用多线程访问,访问次数初始值是0,第一个线程执行了INCR(ip)操作后,
- 第二个线程紧接着也执行了 INCR(ip)
- 此时ip对应的访问次数就被增加到了2,我们就无法再对这个ip设置过期时间
- 导致ip对应的客户端访问次数达到20次后,无法进行访问
我们可以使用Lua脚本保证并发控制
- 访问次数加一
- 判断访问次数是否为1
- 设置过期时间
local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 thenredis.call("expire",KEYS[1],60)
end
设置脚本名称为lua.script,我们接着就可以使用Redis客户端,带上eval选项来执行该脚本。
脚本所需的参数通过以下命令中的keys和args进行传递:
Redis -cli --eval lua.script keys,args
访问次数加一,判断访问次数是否为1,设置过期时间这三个操作可以原子性的执行。
在编写Lua脚本时,我们要避免把不需要做并发控制的操作写入脚本中。
九.课后问题:
Redis 在执行 Lua 脚本时,是可以保证原子性的,那么,在我举的 Lua 脚本例子(lua.script)中,你觉得是否需要把读取客户端 ip 的访问次数,也就是 GET(ip),以及判断访问次数是否超过 20 的判断逻辑,也加到 Lua 脚本中吗?
答案:
- 这2个逻辑都是读操作,不会对资源临界区产生修改,所以不需要做并发控制。
- 减少 lua 脚本中的命令,可以降低Redis执行脚本的时间,避免阻塞 Redis。
使用lua脚本注意点:
- lua 脚本尽量只编写通用的逻辑代码,避免直接写死变量。变量通过外部调用方传递进来,这样 lua 脚本的可复用度更高。
- 建议先使用SCRIPT LOAD命令把 lua 脚本加载到 Redis 中,然后得到一个脚本唯一摘要值,再通过EVALSHA命令 + 脚本摘要值来执行脚本,这样可以避免每次发送脚本内容到 Redis,减少网络开销。
Redis核心技术与实战-学习笔记(二十九):Redis并发控制相关推荐
- Redis核心技术与实战-学习笔记(十五):消息队列(Redis的解决方案)
一.消息队列 消息队列:分布式系统必备的一个基础软件,能支持组件通信消息的快速读写 Redis本身支持数据的快速访问,满足消息队列的读写性能需求 二.Redis适合做消息队列吗? 消息队列的消息存取需 ...
- Redis核心技术与实战-学习笔记(十四):时间序列数据
一.时间序列数据 时间序列数据:没有严格的关系模型,记录的信息可以表示成键和值的关系. (例如,一个设备ID对应一条记录): 时间序列数据的读写特点 持续高并发写入,需要连续记录数万个设备的实时状态值 ...
- Redis核心技术与实战-学习笔记(二十六):缓存雪崩、击穿、穿透
一.缓存雪崩 缓存雪崩:大量应用请求无法在Redis缓存中进行处理,应用请求频繁访问数据库,导致数据库压力激增. 产生原因: 缓存中有大量数据同时过期,导致大量请求无法得到处理 数据保存在缓存中,并设 ...
- Redis核心技术与实战-学习笔记(三十五)Redis使用建议
键值对使用规范 key的命令规范,只有命名规范,才能提供可读性强,可维护性好的key,方便日常管理: value的设计规范,包括避免bigkey,选择高效序列化方法和压缩方法,使用整数对象共享池,数据 ...
- Redis核心技术与实战-学习笔记(五)内存快照RDB
一.为什么需要RDB AOF 方法优势:每次执行只需要记录操作命令,需要持久化的数据量不大.在进行写后日志只要不采用always(同步写回)的持久化策略就不会对性能造成太大影响. AOF方法劣势:AO ...
- Redis核心技术与实战-学习笔记(七)哨兵机制
一.主库挂了,如何不间断服务? 主库挂了,需要运行一个新的主库:将从库切换为主库.这就涉及到三个问题: 主库真的挂了吗? 选择哪个从库作为主库? 如何把新主库相关信息通知给从库和客户端 Redis主从 ...
- C++语法学习笔记二十九: 详解decltype含义,decltype主要用途
实例代码 // 详解decltype含义,decltype主要用途#include <iostream> #include <functional> #include < ...
- opencv学习笔记二十九:SIFT特征点检测与匹配
SIFT(Scale-invariant feature transform)是一种检测局部特征的算法,该算法通过求一幅图中的特征点(interest points,or corner points) ...
- Mr.J-- jQuery学习笔记(二十九)--属性操作方法(获取属性判断)
获取 attr() <span class="span1" name="it666"></span> <span class=&q ...
最新文章
- 如何在Linux下写无线网卡的驱动
- 保护你的Web服务器 iptables防火墙脚本全解读
- xcode 怎么调用midi开发录音_音频应用专业录音声卡:雅马哈UR242声卡教程
- Job 存储和持久化 (第二部分)
- 如何配置数据库ODBC数据源
- MVC 中使用uploadify上传图片遇到的蛋疼问题
- f-admin——基于Laravel框架开发的基础权限后台系统
- 面向对象-类与对象、关键字、异常使用
- QT 与webkit(wke) 交互
- 近世代数——Part2 群:基础与子群 课后习题
- ES6 阮一峰阅读学习
- oracle sql列转行_oracle SQL 行、列转换
- php 百度地图 云存储,jspopular3.0 | 百度地图API SDK
- STM32 与 ST-Link V2仿真器 接线与烧录
- GPS原始信号数据解析
- 小程序 界面响应速度优化
- 《软件测试》 --- 读书笔记
- java类详解_JAVA 内部类详解
- 中关村科技企业家协会网安创新分会在京成立,墨云科技成为首批会员单位
- Linux操作系统的基本使用(ubuntu)
热门文章
- 解决open()不能打开带中文的文件路径
- Python爬虫-爬取手机应用市场中APP下载量
- 使用 parted 对单个磁盘进行分区并进行配额
- 新建一个html标题为李白,李白的诗,如果用自媒体的思维来起标题,画风是这样的...
- prometheus remote write for springboot 远程写入<一>
- 火影忍者中的天才七忍者
- Android文字广告(Textview上下滚动),使用TextSwitcher控件实现
- C语言——求三个数中最大值(6种方法)
- mysql笛卡尔积查询很慢_浅谈MySQL使用笛卡尔积原理进行多表查询
- c++,全局函数做友名