一.需要并发控制的原因

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

二.解决方案

加锁和原子性

加锁:

  • 在读取数据前,客户端需要先获得锁,否则需要等待。
  • 当一个客户端获得锁后,就会一直持有这把锁直到客户端完成数据更新才释放这把锁。

存在问题:

  • 加锁操作过多,会降低系统的并发访问性能;
  • Redis客户端加锁,需要分布式锁实现复杂,需要额外的存储系统来提升加锁操作。

原子操作是另一种提升并发访问控制的方法

三.原子操作

  • 原子操作:执行过程中保持原子性的操作,而原子操作执行时不需要加锁实现了无锁操作。
  • 这样既能保证并发控制还能减少对系统开发性能的影响。

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

  1. 并发访问控制:对多个客户端访问操作同一份数据的过程进行控制,以保证任何一个客户端发送的操作在Redis实例上执行时具有互斥性。
  2. 例如:客户端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 脚本中吗?

答案:

  1. 这2个逻辑都是读操作,不会对资源临界区产生修改,所以不需要做并发控制。
  2. 减少 lua 脚本中的命令,可以降低Redis执行脚本的时间,避免阻塞 Redis。

使用lua脚本注意点:

  1. lua 脚本尽量只编写通用的逻辑代码,避免直接写死变量。变量通过外部调用方传递进来,这样 lua 脚本的可复用度更高。
  2. 建议先使用SCRIPT LOAD命令把 lua 脚本加载到 Redis 中,然后得到一个脚本唯一摘要值,再通过EVALSHA命令 + 脚本摘要值来执行脚本,这样可以避免每次发送脚本内容到 Redis,减少网络开销。

Redis核心技术与实战-学习笔记(二十九):Redis并发控制相关推荐

  1. Redis核心技术与实战-学习笔记(十五):消息队列(Redis的解决方案)

    一.消息队列 消息队列:分布式系统必备的一个基础软件,能支持组件通信消息的快速读写 Redis本身支持数据的快速访问,满足消息队列的读写性能需求 二.Redis适合做消息队列吗? 消息队列的消息存取需 ...

  2. Redis核心技术与实战-学习笔记(十四):时间序列数据

    一.时间序列数据 时间序列数据:没有严格的关系模型,记录的信息可以表示成键和值的关系. (例如,一个设备ID对应一条记录): 时间序列数据的读写特点 持续高并发写入,需要连续记录数万个设备的实时状态值 ...

  3. Redis核心技术与实战-学习笔记(二十六):缓存雪崩、击穿、穿透

    一.缓存雪崩 缓存雪崩:大量应用请求无法在Redis缓存中进行处理,应用请求频繁访问数据库,导致数据库压力激增. 产生原因: 缓存中有大量数据同时过期,导致大量请求无法得到处理 数据保存在缓存中,并设 ...

  4. Redis核心技术与实战-学习笔记(三十五)Redis使用建议

    键值对使用规范 key的命令规范,只有命名规范,才能提供可读性强,可维护性好的key,方便日常管理: value的设计规范,包括避免bigkey,选择高效序列化方法和压缩方法,使用整数对象共享池,数据 ...

  5. Redis核心技术与实战-学习笔记(五)内存快照RDB

    一.为什么需要RDB AOF 方法优势:每次执行只需要记录操作命令,需要持久化的数据量不大.在进行写后日志只要不采用always(同步写回)的持久化策略就不会对性能造成太大影响. AOF方法劣势:AO ...

  6. Redis核心技术与实战-学习笔记(七)哨兵机制

    一.主库挂了,如何不间断服务? 主库挂了,需要运行一个新的主库:将从库切换为主库.这就涉及到三个问题: 主库真的挂了吗? 选择哪个从库作为主库? 如何把新主库相关信息通知给从库和客户端 Redis主从 ...

  7. C++语法学习笔记二十九: 详解decltype含义,decltype主要用途

    实例代码 // 详解decltype含义,decltype主要用途#include <iostream> #include <functional> #include < ...

  8. opencv学习笔记二十九:SIFT特征点检测与匹配

    SIFT(Scale-invariant feature transform)是一种检测局部特征的算法,该算法通过求一幅图中的特征点(interest points,or corner points) ...

  9. Mr.J-- jQuery学习笔记(二十九)--属性操作方法(获取属性判断)

    获取 attr() <span class="span1" name="it666"></span> <span class=&q ...

最新文章

  1. 如何在Linux下写无线网卡的驱动
  2. 保护你的Web服务器 iptables防火墙脚本全解读
  3. xcode 怎么调用midi开发录音_音频应用专业录音声卡:雅马哈UR242声卡教程
  4. Job 存储和持久化 (第二部分)
  5. 如何配置数据库ODBC数据源
  6. MVC 中使用uploadify上传图片遇到的蛋疼问题
  7. f-admin——基于Laravel框架开发的基础权限后台系统
  8. 面向对象-类与对象、关键字、异常使用
  9. QT 与webkit(wke) 交互
  10. 近世代数——Part2 群:基础与子群 课后习题
  11. ES6 阮一峰阅读学习
  12. oracle sql列转行_oracle SQL 行、列转换
  13. php 百度地图 云存储,jspopular3.0 | 百度地图API SDK
  14. STM32 与 ST-Link V2仿真器 接线与烧录
  15. GPS原始信号数据解析
  16. 小程序 界面响应速度优化
  17. 《软件测试》 --- 读书笔记
  18. java类详解_JAVA 内部类详解
  19. 中关村科技企业家协会网安创新分会在京成立,墨云科技成为首批会员单位
  20. Linux操作系统的基本使用(ubuntu)

热门文章

  1. 解决open()不能打开带中文的文件路径
  2. Python爬虫-爬取手机应用市场中APP下载量
  3. 使用 parted 对单个磁盘进行分区并进行配额
  4. 新建一个html标题为李白,李白的诗,如果用自媒体的思维来起标题,画风是这样的...
  5. prometheus remote write for springboot 远程写入<一>
  6. 火影忍者中的天才七忍者
  7. Android文字广告(Textview上下滚动),使用TextSwitcher控件实现
  8. C语言——求三个数中最大值(6种方法)
  9. mysql笛卡尔积查询很慢_浅谈MySQL使用笛卡尔积原理进行多表查询
  10. c++,全局函数做友名