目录
一、Redis万能回答(7个层面)
1、Redis读-写为什么这么快?
层面 | 描述 |
---|---|
内存存储 | 没有磁盘IO上的开销;类似于HashMap,读写操作时间复杂度都是O(1) |
执行是单线程 | ① Redis6.0之前其核心网络IO模型使用的是单线程 ② Redis6.0之后引入了多线程IO处理网络读写与协议解析; 但是不管哪个版本,Redis执行命令都是单线程,避免了多线程的切换与竞争锁的开销; |
非阻塞-IO多路复用 | Redis基于epoll函数模型实现多路复用IO技术(一个线程/进程处理多个网络模型的IO请求),以及Redis自身的事件处理模型将epoll模型中的连接、读写、关闭都转换为事件,减少网络IO时间; |
数据结构:计算向数据移动 | Redis本身提供了5种常见的数据结构(String、Hash、Set、List、SortedSet),每种类型提供了API方法,实现计算向数据移动,提高IO速度; |
重写虚拟内存模型VM | Redis本身的虚拟内存模型可以实现冷热数据分离(访问频率低的数据持久化到磁盘,内存空间优先存储热度高的数据,避免因内存不足造成访问速度下降) 注意:Redis的Swap分区没有用OS提供的,而是自己实现的;(Swap分区:当Linux内存不足时,会释放一部分物理空间,这部分被释放的数据存储在Swap分区中,待到用到这些数据时,在从Swap分区中恢复到内存) |
2、Redis的持久化机制
机制 | 触发机制 | 原理 | 优劣势 |
---|---|---|---|
AOF日志 | ① 每秒同步 ② 每写同步 ③ 不同步 | 1)将每一个redis的写指令记录在日志中,只追加不删除;当恢复时从头到尾执行一边写指令; 2)当AOF日志大小超过规定阈值时,触发重写机制; 3)低版本的redis重写机制时将AOF日志中的指令压缩到能恢复原数据的最小指令集; 4)高版本的redis重写机制是将AOF日志重写为RDB文件进行保存,恢复的时候先RDB,然后在AOF】 | 优势: 数据保存完整性较高; 缺点: 开启了AOF后会降低redis的整体性能,且持久化的文件会越来越大,恢复速度要慢; |
RDB快照 | ① save命令:阻塞Redis服务器,直到RDB保存结束; ② bgsave命令:Redis会在后台异步进行快照操作,快照同时还能响应客户端请求; | 当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作: 1)执行bgsave命令,Redis主进程会检查是否有子进程在执行RDB/AOF持久化任务,如果有的话,直接返回; 2) Redis 调用 fork() ,主线程阻塞,然后创建出子进程,阻塞解除; 3)子进程基于【写时复刻技术(COW)】将原数据写入到一个RDB文件中; 4)当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件; | 优势: 恢复速度快; 缺点: RDB无法做到实时持久化,若在两次bgsave间宕机,则会丢失区间(分钟级)的增量数据,不适用于实时性要求较高的场景; |
RDB快照-写时复刻技术(COW)
fork()之后,内核把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程;
① 当父子进程都只读内存时,相安无事。
② 当其中某个redis请求写操作时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入内核的一个中断例程。中断例程中,内核就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份;
③ 然后父进程就可以继续执行写操作,子进程继续在原内存上进行RDB写入;
3、Redis的事务
层面 | 解释 |
---|---|
事务 | ① Redis的单个操作都是原子性,要么执行要么不执行; ② Redis的批量操作可以通过【multi指令(事务开启) 和 exec指令(事务提交)】 或者 【Lua脚本】实现事务 ③ Redis不支持事务回滚(非原子性):Redis事务可以理解为一个打包的批量执行脚本,单个操作是原子性的,但是批量指令不是原子的;事务中的某条指令执行失败,并不会导致前面的指令回滚,也不会阻止后边的指令继续执行; ⭐不支持事务回滚的原因: 【Redis命令只会因为语法错误而失败;在Multi命令开启事务后,这些语法错误在指令会入队失败,并被redis服务器进行记录;待执行exec命令后,如果发现有入队失败的记录,则事务就会拒绝执行并取消事务;因此这也保证了我们【redis不需要进行回滚】;保证redis内部的简洁与高效; |
4、Redis的数据结构及其底层实现原理
数据结构 | 底层原理 |
---|---|
String 类型 | String的底层原理是基于 动态字符串 (SDS) 实现的,它不仅保存了数据char[],而且保存了缓冲区已经占用空间的长度,以及缓冲区剩余可用空间的长度。SDS相对于字符串char的优势: (1) 修改SDS时,可以预先检查SDS的缓冲区空间是否足够,如果足够会先拓展SDS的空间,以防止缓冲区溢出。而char字符串不会检查缓冲区是否足够,容易出现溢出情况。 (2) SDS有着预分配空间的机制,可以减少为字符串重新分配空间的次数。 |
List 类型 | List的底层原理是基于 双向链表 实现的。 |
Hash 类型 | Hash的底层原理是基于 字典 实现的, 其解决哈希冲突的方法采用的是拉链法,具体的就是将哈希表的装载因子维持在一个合理的范围之内,方便rehash。 |
Set 类型 | Set的底层原理是基于 intset数据结构和字典 实现的。(1) 当存储的数据全都是整数类型,且数量小于512个时,会使用intset存储数据。intset是一个由整数组成的有序集合,可以进行二分查找。 (2) 当不满足使用intset的条件时,都使用字典存储数据,同样是使用拉链法解决哈希冲突。 |
zSet 类型 | zset中的每个元素包含数据本身和一个对应的分数(score)。zset的数据本身不允许重复,但是score允许重复。其底层原理是: (1) 当数据较少时 ,使用 ziplist (压缩列表) 存储数据,它是一种特殊的双向链表。ziplist占用连续内存,每项元素都是(数据+score)的方式连续存储,按照score从小到大排序。(2) 当数据较多时 ,使用 字典 + 跳表 实现:字典用于根据key值查询score,跳表根据socre查找数据 (时间复杂度O(logN))。 |
Bitmap 类型 | Bitmap是布隆过滤器的底层,每一位只存储0和1。1就代表存在,0代表不存在。 |
位图数据结构
数据结构 | 原理 | 优缺点 | 使用场景 |
---|---|---|---|
BitMap | BitMap底层是int(32bit),一个int就能放32个数 | 优点: ① 查询速度快,O(1)的查询复杂度; ② 节约空间,能够用少量空间存储大量的非重复数字; 缺点: BitMap不适合存储稀疏数据,会超级浪费空间;比如,一个Posting List ={1,9999},为了表示这两个数,BitMap就需要用9999个bit位表示这两个数; | 与BitSet一致 |
BitSet | BitSet底层是long(64bit),它实现了一个按需增长的位向量,位 set 的每个组件都有一个boolean值;两个BitSet之间可进行位运算(逻辑与、逻辑或、逻辑异或)等操作; | 给BitSet1个GB的空间,能够大约存储85亿个数 优缺点: 与BitMap相比,能够快速的进行逻辑运算操作;其它的优缺点一样; | ① 海量数据的统计工作,比如日志分析、用户数统计等; ② Bloom Filter(布隆过滤器); ③ 快速去重 + 快速查询O(1); |
RoaringBitMap (压缩位图索引) | RoaringBitMap底层结构: ① short[] keys: key = N / 65536 ② Container[] values: value = N % 65536 ③ int size:包含了有效key和values中有效数据对的数量; | 解决了BitMap与BitSet存储稀疏数组时浪费空间的弊端;压缩思想: 与其重复写100遍0,不如声明0重复了100遍; | RBM是BitSet和BitMap的进阶版,集万千优点于一身; |
5、Redis的键过期删除策略
策略 | 描述 |
---|---|
定时删除 | 设置过期时间,到期立刻删除 |
惰性删除 | 用到这个key时,再判断是否过期,然后再删除; |
定期删除 | 每隔一段时间,就对一些key进行检查,若过期就删除; |
AOF主从同步过程中如何处理过期的key?
- salve节点并不会主动删除过期的key;master当检测到某个key过期后,那么这个删除key的命令会随着AOF文件一起从master节点发送至所有的slave节点,salve收到master的DEL命令后,才会去删除key;保证主从同步的一致性;
6、Redis的内存淘汰策略
策略 | 描述 |
---|---|
AllKeys-LRU(最常用) | 基于LRU缓存算法,移除最近最少使用的key; |
Volatile-LRU | 基于LRU缓存算法,移除设置了过期时间的全部key; |
Volatile-TTL | 从设置了过期时间的数据集中,移除将要过期的key; |
Volatile-random | 从设置了过期时间的数据集中,随机移除key; |
AllKeys-random | 从键空间中,随机移除key; |
No-eviction | 禁止移除key,内存不够写入时会报错; |
Volatile-LFU | 维护每个key的使用次数,当内存不够时,从【设置了过期时间的key中】,删除使用频率最低的那个; |
Allkeys-LFU | 维护每个key的使用次数,当内存不够时,从【全部的key中】,删除使用频率最低的那个; |
7、Redis主从复制
模式 | 描述 | 场景 |
---|---|---|
单机redis | 架构简单,部署方便;但是不保证数据可靠性,无节点备份,容易数据丢失; | 每秒的读QPS在1W以上 |
主从复制 | 优点: 高可用性,能够实现读写分离; 缺点: ① 故障恢复复杂,master故障时,需要手动的选择一个slave节点作为新的master; ② Redis复制中断后,slave发起psync,如果同步失败会进行全量同步,此时会卡顿;且全量同步过程中由于写时复刻COW机制,可能会造成主库内存溢出; | 读写分离 |
主从复制 + 哨兵集群 | 优点: ① 故障自动转移,master故障,无需人为指定新master,会有选举机制 ; ② 高可用,有主观下线与客观下线两种机制,不轻易实施故障转移; | 主要是解决单点故障问题,每秒的读QPS在10W级以上 |
Redis集群 | 特点: 异步复制,不能保证数据的强一致性; | 缓存1T等超多的数据 |
Redis自研 | 略 |
计算向数据移动与数据向计算移动?
- 数据向计算移动: 比如要获取某个集合的index = 2的数据,那么这种方式下,会先返回集合的全部数据给刻客户端,然后再取index = 2 位置上的元素展示;涉及到了大部分的数据移动,速度慢;Memcached就是这种;
- 计算向数据移动: 直接在数据层找到index = 2 位置上的元素,然后返回给客户端;没有数据的大规模移动,计算逻辑是在数据层面上解决的;Redis的数据结构提供了诸多API方法就可以实现这一点;
二、Rediss使用场景?
场景 | 应用 |
---|---|
分布式缓存 | 持久化机制、键过期删除策略、内存淘汰策略 |
分布式锁 | setnx命令获取锁,返回1则说明获取锁成功,反之失败;减库存、秒杀等场景;信号量等等 |
分布式会话 | 用redis保存全局session,结合SpringSession实现单点登录、社交登录等; |
消息队列 | Redis提供了发布/订阅阻塞队列共嗯那个,可以实现简易的消息队列;实现业务解耦、流量削峰与异步处理 |
排行榜 | key-Sorted带权重的有序数据集合 |
社交网络 | 点赞、共同好友等功能, key-Set、key-Hash等数据集合 |
最新列表 | key-List数据结构提供了【正向、反向查找】 |
计数器 | 商品浏览量+1等,保证时效性,incr命令操作内存+1; |
三、如何保证缓存与数据库的一致性?
方案 | 潜在问题 | 解决方法 |
---|---|---|
先删除缓存,后更新数据库 | 缓存可能会存储脏数据 | ① 延时双删: 防止请求A更新数据库的过程中,请求B查询缓存,没查到,然后将A没有更新之前的数据刷入缓存; ② 在主从模式下发现更新命令,则强制请求主库查询: 先删除缓存;有可能主库更新完之后,从库还没有同步完成,此时一个请求查询从库,会读到未更新前的数据;解决方法就是:强制在主库同步的过程中,让请求查询主库; ③ 利用JVM的内存队列实现【更新与读操作的异步串行化】: 系统内部维护n个内存队列;先删除缓存,然后更新的请求入队列;当另一个查询请求过来后,读到了空缓存;那么此时先不读数据库,而是将更新缓存的请求也发送到队列中,排在数据库的更新请求后边;等到数据库与缓存的更新请求都执行结束,此时才开始读;注意:可以做读操作去重 |
先更新数据库,后删除缓存 | 缓存可能会删除失败 | ① 订阅bin log日志 + 消息队列补偿机制: 先更新数据库,redis订阅bin log日志;若缓存删除失败,则各个redis服务器拉取bin log日志,进行缓存的更新 |
四、缓存异常及解决方案
- 针对所有异常的统一方案:
- 事前设置热点key永不过期;
- ① 第一步:先查布隆过滤器(DB中所有的key全存储在布隆过滤器中),过滤掉查询数据库根本没有的数据的请求;
- ② 第二步:经过布隆过滤器的key允许查询缓存;在某个线程redis缓存中首次查询不到时;抢互斥锁,只允许一个线程访问数据库;
- ③ 第二步:若数据库中有,则访问完DB将数据写入缓存,后续的其它线程直接访问缓存即可;若数据库中没有,则将(key,null)写入缓存;
问题 | 描述 | 解决方案 |
---|---|---|
缓存穿透 | 查询缓存中不存在的key,不能命中缓存。导致每次都去DB中查询; | ① 将不存在的key也存入缓存中,value = null; 这种方式不适用于大规模随机的key; ② 布隆过滤器: 将所有可能存在的key哈希到一个足够大的bitmap中,当一个一定不存在的key会被bitmap拦截掉,避免对数据库的直接访问; |
缓存击穿 | 大并发请求 + 某个热点key过期,在缓存中没有;导致重复去访问DB; | ① 缓存失效后,使用互斥锁或队列控制访问DB的线程数量; 例如,使用Redis的setnx去设置一个互斥锁,当获取到互斥锁后,再进行数据库操作并回设缓存,否则重试get缓存的方法; ② 热点key设置永不过期: 物理不过期,逻辑过期(过期时间也存储在value中,当发现快过期时,后台启动异步线程去重置过期时间) |
缓存雪崩 | 大规模的key同时过期(redis宕机或者key设置的过期时间一样) | ① 互斥锁控制访问DB的数量: 保证缓存失效时,只有一个线程能够获取到锁,进而访问数据库,更新缓存;期间其它线程等待并重试; ② 分散缓存失效时间: 在原有的失效时间基础上增加一个随机值; ③ 分级缓存: 设计多级缓存,上一级缓存失效,则访问下一级缓存(每一级缓存的过期时间都不同) ④ 熔断机制: 限流降级;超过阈值的并发请求直接提示“系统拥挤”; ⑤ 主从模式 + 哨兵: 防止redis宕机导致的全面崩溃; ⑥ 开启redis持久化机制: AOF与RDB; |
五、Redis事务
1、Redis事务三种实现方式
实现方式 | 描述 |
---|---|
事务指令 | Multi(开启事务)、Exec(执行命令,事务提交) |
Lua脚本 | Redis可以保证Lua脚本内的命令一次性、按顺序的执行 |
基于中间标记变量 | 通过额外的标记变量来标识事务是否执行完毕;执行请求时,首先要根据标记变量判断事务是否执行完毕 |
2、Redis事务的ACID都是怎么实现的?
事务 | redis是否支持 | 描述 |
---|---|---|
原子性(A) | × | Redis事务不支持事务回滚,因为redis指令执行失败只可能是语法错误,这些在开发过程中就能被发现 |
一致性(C) | √ | Redis事务的两种主要实现方式【multi指令与exec指令】【Lua脚本】; Redis 事务在执行的过程中,不会处理其它命令,而是等所有命令都执行完后,再处理其它命令。因此在 Redis 事务在执行过程中发生错误或进程被终结,都能保证数据的一致性 |
隔离性(I) | √ | Redis是单线程执行的,天然具有隔离性,不会出现脏读、幻读、不可重复读等问题; |
持久性(D) | √ | Redis有【AOF、RDB】两种持久化机制 |
3、Redis事务执行的三个阶段
阶段 | 描述 |
---|---|
Step1 | 执行 Multi命令 开启事务 |
Step2 | redis指令按顺序入队,入队成功返回Queued关键字;反之则入队失败,并被服务器所记录; |
Step3 | 执行 exec命令,检查是否有入队失败的记录来决定是否提交事务 ① 若有入队失败失败,则拒绝执行并取消该事务; ② 若没有入队失败记录,则正常执行并提交事务; |
执行multi命令,事务开启后,若服务端收到除了exec以外的命令,则会把请求放入队列中排队,待执行了exec命令后,才开始执行;
事务命令:
命令 | 描述 |
---|---|
multi | 开启事务 |
exec | 按顺序执行事务中的redis指令,提交事务 |
discard | 清空事务队列,并放弃执行事务;当事务中的redis指令入队失败后,在exec指令执行时发现失败记录,则调用用discard; |
watch | 监视一个或者多个key,如果事务提交前key被改动,那么事务将会被中断; watch命令是一种乐观锁机制,可以基于它实现与事务回滚一样的效果; |
unwatch | 取消监视 |
4、Redis事务为什么不支持回滚?
⭐因为redis不需要回滚: 【Redis命令只会因为语法错误而失败;在Multi命令开启事务后,这些语法错误在指令会入队失败,并被redis服务器进行记录;待执行exec命令后,如果发现有入队失败的记录,则事务就会拒绝执行并取消事务;因此这也保证了我们不需要进行redis回滚;保证redis内部的简洁与高效;
虽然Redis不支持回滚,但是可以通过Redis事务中的【watch命令】实现乐观锁,进而实现做到与事务回滚类似的的效果;
watch命令实现事务回滚的效果 |
---|
① 【watch命令】可以为 Redis 事务提供CAS操作 |
② 我们可以使用 watch 命令来监视一个或多个 key,如果被监视的 key 在事务执行前被修改过那么本次事务将会被取消;也就实现了" 事务回滚 " |
③ 只有确保被监视的 key,在事务开始前到执行的时间段内未被修改过,事务才会执行成功(类似乐观锁) |
④ 如果一次事务中存在被监视的 key,无论此次事务执行成功与否,该 key 的监视都将会在执行后失效 ,也就是说监视是一次性的 |
六、Redis线程模型
1、Redis的IO多路复用模型
Redis执行命令请求过程
- 当用户请求redis读写命令时,基于以下步骤:
步骤 | 描述 |
---|---|
Step1 | 用户线程阻塞,向内核空间请求数据 |
Step2 | 内核空间向硬件磁盘请求数据,并拷贝到内核缓冲区 |
Step3 | 内核缓冲区拷贝数据到用户空间 |
Step4 | 用户线程拿到了数据,开始执行redis命令请求,执行完毕将结果写入用户空间缓冲区 |
Step5 | 用户缓冲区数据拷贝到内核缓冲区,然后写入硬件磁盘 |
- 读数据时,要从磁盘读取数据到内核缓冲区,然后拷贝到用户缓冲区
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入磁盘
1)阻塞式IO
-
在上述磁盘、内核态与用户态的交互过程中,主要可以分为两个阶段,阻塞式IO在这两个阶段,用户线程都处于阻塞状态;
-
那么如果每个redis请求都分配一个线程,那么所有线程在等待数据与拷贝数据的过程中,都是阻塞的,效率太差;
阶段 | 描述 | 用户线程 |
---|---|---|
阶段一 | ① 内核向硬件磁盘请求数据 ② 内核空间数据就绪 | 阻塞 |
阶段二 | ①从内核空间拷贝数据到用户空间 ② 拷贝完成 | 阻塞 |
2)非阻塞式IO
-
非阻塞式IO虽然在第一阶段不阻塞,但是CPU会一直自旋的请求数据;
-
如果为每一个redis请求都分配一个线程,如果每个线程等待数据就绪的时间都很长,那么多个线程就会一直自旋式的向用户态请求数据,CPU使用率会暴增,但是效率却依然很差;
阶段 | 描述 | 用户线程 |
---|---|---|
阶段一 | ① 内核向硬件磁盘请求数据 ② 期间用户线程一直自旋的向内核请求数据 ③ 内核态数据准备就绪,用户线程不再自旋,等待数据拷贝 | 非阻塞 |
阶段二 | ①从内核空间拷贝数据到用户空间 ② 拷贝完成 | 阻塞 |
3)⭐IO多路复用
概念 | 描述 |
---|---|
文件描述符(File Descriptor) | 简称FD,是一个从0 开始的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket) |
IO多路复用 | 是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源 |
-
IO多路复用使用一个线程监听多个socket的句柄(fd),当某个socket数据就绪时,就会返回一个readable事件,通知用户线程,充分利用CPU资源;避免BIO对某个socket一直阻塞以及NIO的CPU空转问题;
-
而且用户线程不用分配多线程,一个线程就可处理多个客户端的请求;
阶段 | 描述 | 用户线程 |
---|---|---|
阶段一 | ① 进程调用select、poll、epoll模型来监听多个socket的FD信息② 期间任一socket的数据准备就绪,就返回readable事件 | 阻塞 |
阶段二 | ① 用户进程找到就绪的socket ② 依次调用recvfrom读取每个就绪状态socket的数据(按照事件的顺序进行读取,不会一直卡在一个socket上) ③ 内核将数据拷贝到用户空间 | 非阻塞 |
IO多路复用实现方式
- IO多路复用根据监听与通知方式的不同,有3种实现方式:
函数 | 描述 |
---|---|
select | ① 只会通知用户进程有FD就绪,但不确定具体是哪个FD,需要用户进程逐个遍历FD来确认 ② 监听的fd数量有限 |
poll | 与select的区别就是监听的FD数量无限制,底层基于链表; |
epoll | ① 在通知用户进程FD就绪的同时,把已就绪的FD写入用户空间,不用再入遍历FD查询;底层基于红黑树(redis默认) ② 事件通知函数epoll_wait有两个模式:边沿触发与水平触发; 1)边沿触发是FD就绪后,会重复通知多次用户线程,直至数据处理完成,是epoll模式的默认模式; 2)水平触发是FD就绪后,只通知1次; |
4)信号驱动IO
5)异步IO
2、Redis真的是单线程的吗?
工作线程是单线程;
- 1)Redis基于反应器reactor模式开发了一个事件处理器;因为这个文件事件处理器是单线程的,因此称Redis是单线程的;Redis线程模型基于epoll函数模型实现了【IO多路复用技术】,同时监听多个Socket,根据Socket上的事件选择对应的事件处理器来处理;我们常说的Redis单线程,实际上说的是【Redi的s工作线程是单线程】,其实在后台还有3个线程,主要用于处理耗时长的操作,分别是:
后台线程 | 作用 |
---|---|
close_File | 关闭 AOF、RDB 等过程中产生的大临时文件; |
aof_fsync | 将追加至 AOF 文件的数据写入磁盘; |
lazy_free | 惰性释放大对象; |
- 2)Redis6.x之前,网络IO操作与redis读写操作都是同一个工作线程中串行化的执行;为了优化网络IO模块,提高吞吐量,在redis6.x后引入了多线程模型;
- 3)Redis6.x之后,引入的多线程主要用来处理网络IO,主线程(工作线程)单线程串行化的执行redis命令;能够充分的利用多核CPU资源,并且分摊Redis的同步IO开销;
Redis6.x之前,工作线程串行处理网络IO与redis命令(业务处理)
Redis6.x之后,工作线程串行处理=redis命令(业务处理),网络IO由IO线程并行处理
1)Redis6.x之前的线程模型
- 客户端与Redis的通信过程
- 事件处理器的模型:
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器),当一个事件处理器处理完一个事件后,IO多路复用程序才会向文件事件分派器分配下一个事件;
步骤 | 阶段 | 描述 |
---|---|---|
Step1 | Redis启动 | 将【连接应答器】与【AE_READABLE】事件关联 |
Step2 | 客户端请求与redis服务器建立通信连接 | ① server socket收到连接请求,产生【AE_READABLE】事件 ②【AE_READABLE】事件被IO多路复用程序监听到,进入事件队列; ③【AE_READABLE】事件被事件分配器分给【连接应答器】进行处理; ④ 连接成功,【AE_READABLE】事件与【命令请求处理器】关联; |
Step3 | 客户端发起读写请求 | ① redis创建一个socket,产生【AE_READABLE】事件; ②【AE_READABLE】事件被IO多路复用程序监听到,进入事件队列; ③【AE_READABLE】事件被事件分配器分给【命令请求处理器】进行处理; ④ 工作线程单线程的操作redis执行命令,将执行结果存入命令回复处理器,然后将【AE_WRITEABLE】事件与【命令回复处理器】关联; |
Step4 | 客户端接收操作结果 | ①【当socket满足可写条件】,表示客户端准备好接收数据,产生【AE_WRITEABLE】事件; ②【AE_WRITEABLE】事件被IO多路复用程序监听到,进入事件队列; ③【AE_WRITEABLE】事件被事件分配器分给【命令回复处理器】进行处理; ④ 命令回复器将操作结果写入socket,供客户端读取; |
Step5 | 清除事件与处理器的关联关系 | 命令回复器处理完后,将【AE_WRITEABLE】事件与【命令回复处理器】的关联关系清除 |
2)Redis6.x之后的线程模型
- 客户端与Redis的通信过程
步骤 | 描述 |
---|---|
Step1 | 主线程接收连接请求,将可读Socket放入全局等待处理队列 |
Step2 | 根据轮询法分配网络IO线程,与socket绑定 |
Step3 | 主线程阻塞等待socket读取完成(从内核态拷贝数据到用户空间) |
Step4 | 主线程串行化的执行redis命令 |
Step4 | 主线程阻塞等待IO线程将数据会写到Socket中 |
- 事件处理器的模型:
- 网络IO(内核拷贝数据到用户空间的过程 + 用户空间将结果写到内核空间缓存)的过程是并行的,redis执行命令的过程是单线程串行化的;
注意,绿色的或紫色的都是串行化执行的,先执行完绿色的;才会执行紫色的,不会有并发问题;单看哪一个都是串行的,图只能这么画;
3、Redis是线程安全的吗?
- redis是单线程执行的,内部能够保证线程安全;但是外部使用的时候,业务逻辑需要我们自行保障;
七、零散面经(待更)
1、redis如何解决并发竞争key问题
方法 | 原理 |
---|---|
分布式锁 | 想操作某个key,要先抢到锁之后才能操作;实现方式有redis、zookeeper、mysql等 |
乐观锁 watch命令 | 用watch命令实现乐观锁 |
时间戳 | 即版本号机制,修改之后记录时间戳,若修改时时间戳早于现有的时间戳,则不操作;适用于有序场景 |
消息队列 | 在并发量很大的情况下,可以通过消息队列进行串行化处理。这在高并发场景中是一种很常见的解决方案 |