Redis总结及项目中应用
总结redis常见问题与超卖问题
目录
3.Redis是线程安全的吗?
一、 项目应用
项目一采用Redis缓存方案,对支付渠道参数全面缓存提高支付交易性能。
在调用支付服务的时候,先从redis查询,如果查询到则返回,否则从数据库查询,从数据库查询完毕再将数据缓存到redis。
项目二采用Redis实现分布式Session问题,以及实现数据缓存功能,以提高系统性能。
分布式session问题?
之前的代码运行在同一台服务器上,所有操作都在一台服务器上,随着逐步发展壮大,当需要部署多台服务器的时候,Nginx使用默认轮询策略去实现负载均衡,将请求按照顺序分发到不同的服务器上去,这个时候比如我们各开始在Tomcat1登录,用户信息在Tomcat1的Session中。过了一会请求又被Nginx分发到Tomcat2上去,这个时候Tomcat2上面没有Session信息,于是用户又需要重新去登录。
解决方案:
(1)session复制:把session信息同步到集群中的服务器上去,这样的话每台服务器都保存着session信息,任何一台服务器宕机都不会导致Session数据丢失,服务器使用Session时,直接从本地获取。这种方式的缺点也很明显,因为session需要同步,并且同步过程是所有应用服务器来完成,由此对服务器的性能损耗也比较大。
(2)session黏贴:就是把请求黏贴到刚刚存有session信息的节点上去。缺点是新增及其的时候会重新Hash,导致重新登录,应用重启后,需要重新登录。
(3)前端存储:不占用服务端内存。缺点是存在安全难问题,数据大小受到cookie限制。
(4)Session集中存储:将session信息统一存储,这样优点是安全,容易水平扩展,缺点是增加复杂度,需要修改代码。本项目采用的是redis实现分布式Session,把session信息集中存储到redis中,每次都可以去redis中寻找信息。
分布式项目用分布式session和token的区别是什么
区别与session是客户端不保存会话信息。
基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie,基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今互联网时代更多类型的客户端接入系统,系统多采用前后端分离架构进行实现,所以基于token的方式更合适。
“Servlet规范描述了HTTP请求及响应处理过程相关的对象及其作用。”
库存超卖问题这么解决?
高并发的情况下并行,可能存在一个人抢购到两个商品,项目采用的方法是去加一个唯一索引,把用户id和商品id绑成一个索引,判断Id是否唯一,在redis中进行。去解决同一个用户秒杀多个商品的问题。
主要目的要减少对数据库的访问,尽可能将数据缓存到Redis,从缓存中获取数据。同时加入MQ对访问数据库的并发量进行削峰。从而达到在保证数据库高可用的情况下防止超卖。
(1)在系统初始化时,将商品数量缓存到Redis中;
(2)在接到秒杀请求时,在Redis中进行预减库存,当Redis中库存不足时,直接返回秒杀失败,否则继续进行下一步;
Redis预见库存有两种实现方式:第一种是实现一个InitializingBean,会有一个初始化方法,该方法是原子性的,访问一次库存减一。第二种是使用【lua脚本】实现分布式锁,确保原子性操作。
小于0设置内存标记的map为true,用户开始访问到map是true的话直接返回;
(3)将请求放入异步队列中,返回正在排队中;
(4)服务端异步队列将请求出队,出队成功的请求可以生成秒杀订单,减少数据库库存,返回秒杀订单详情;
(5)当后台订单创建成功之后向用户发送一个秒杀成功通知。前端以此来判断是否秒杀成功,秒杀成功则进入秒杀订单详情,否则秒杀失败。
仅仅通过缓存并没有很好解决我们秒杀带来的高并发。还需要去数据库获取库存,扣减库存。通过redis预减库存,减少数据库访问,但是数据库还需要与redis频繁交互,redis在单独的服务器上。我们可以通过内存标记,减少redis的访问。接下来去优化下单操作。下单直接去找数据库的话数据库压力也很大,所以可以先让请求进入MQ进行缓冲,通过队列异步下单,增强用户体验。
库存少卖问题这么解决?
失败后就应该让Redis的库存再加上1;
二、Redis面经
谈谈你对redis的理解?
Redis是一个基于Key-Value存储结构的Nosql开源内存数据库,它提供了5种常见的数据类型,像String、Map、ZSet、Set、List ,针对于不同的结构呢,可以解决不同场景的问题,因此它可以去覆盖应用开发里面的大部分的业务场景,比如Top10问题、好友关注列表问题、热点话题等等。其次由于Redis是一个基于内存的一个存储并且在数据结构上做了大量的一些优化,所以IO性能会比较好,在实际开发里面呢,我们会把它用在应用和数据库之间的一个分布式缓存中间件,并且它又是一个非关系数据库的存储,不存在表之间关联查询的一些问题,所以它可以很好的去提升应用程序的数据IO效率,最后作为企业级开发来说,它又提供了主从复制+哨兵,以及集群的方式去实现高可用(,在Redis集群里面呢,通过hash槽的方式去实现数据的分片,进一步提升了整体的一个性能和可扩展性)。 以上即我的理解。
1.Redis读-写为什么这么快?
层面 | 描述 |
内存存储 | 没有磁盘IO上的消耗;类似于HashMap,读写操作事件复杂度都是O(1) |
执行是否单线程 | (1)Redis6.0之前其核心网络IO模型使用的是单线程; (2)Redis6.0之后引入了多线程IO处理网络读写与协议解析; 但是不管哪个版本,Redis执行命令都是单线程的,避免了多线程的切换与竞争锁的开销。 |
非阻塞-IO多路复用 | Redis基于epoll函数模型实现多路复用IO技术(一个线程/进程处理多个网络模型的IO请求)以及Redis自身的事件处理模型将epoll模型中的连接、读写、关闭都转换为事件,减少网IO时间。 (6.0之前(1)+ 6.0之后 (2)+) |
数据结构:计算向数据移动 | Redis本身提供了5种常见的数据结构(String、Hash、Set、List、SortedSet),每种类型提供了API方法,实现计算向数据移动,提高IO速度; |
重写虚拟内存模型VM | Redis本身的虚拟内存模型可以实现冷热数据分离(访问频率低的数据持久化到磁盘,内存空间有限存储热度高的数据,避免因内存不足造成访问速度下降) 注意:Redis的Swap分区没有用OS提供的,而是自己实现的;(Swap分区:当Linux内存不足时,会释放一部分物理空间,这部分被释放的数据存储到Swap分区种,待用到这些数据时,再从Swap分区种恢复到内存) |
计算向数据移动与数据向计算移动?
数据向计算移动: 比如要获取某个集合的index = 2的数据,那么这种方式下,会先返回集合的全部数据给刻客户端,然后再取index = 2 位置上的元素展示;涉及到了大部分的数据移动,速度慢;Memcached就是这种;
计算向数据移动: 直接在数据层找到index = 2 位置上的元素,然后返回给客户端;没有数据的大规模移动,计算逻辑是在数据层面上解决的;Redis的数据结构提供了诸多API方法就可以实现这一点;
2.Redis持久化机制?
机制 | 触发机制 | 原理 | 优劣势 |
AOF日志 | (1)每秒同步 (2)修改同步 (3)不同步 | (1)将每一个redis的写指令记录在日志中,只追加不删除,当恢复时从头到尾执行一遍写指令; (2)当AOF日志大小超过规定阈值时,触发重写机制; (3)低版本的redis重写机制是将AOF日志中的指令压缩到能恢复元数据的最小指令集; (4)高版本的redis重写机制是将AOF日志重写为RDB文件进行保存,恢复的时候先RDB,然后在AOF | 优势:数据保存完整性较高 缺点:开启了AOF后会降低redis的整体性能,且持久化的文件越大,恢复速度越慢。 |
RDB快照 | (1)sava命令:阻塞Redis服务器,直到RDB保存结束; (2)bgsava命令:Redis会在后台异步进行快照操作,快照同时还能响应客户端请求。 | 当Redis需要保存dump.rdb文件时,服务器会执行以下操作: (1)执行bgsave命令,Redis主进程会检查是否有子进程在执行RDB/AOF持久化任务,如果有的话,直接返回; (2)调用fork(),主线程阻塞,然后创建子进程,阻塞解除; (3)子进程基于【写时复刻技术(COW)】将元数据写入到一个RDB文件中; (4)当子进程完成对新RDB文件写入时,Redis用新RDB文件替换原来的RDB文件,并删除旧的RDB文件。 | 优势:恢复速度快; 缺点:RDB无法做到实时持久化,如果两次bgsave之间宕机,则会丢失区间(分钟级)的增量数据,不适用于实时性要求较高的场景。 |
RDB快照-写时复刻技术(COW)
fork()子进程,内核把父进程中所有的内存页的权限设置为read-only,然后子进程的地址空间指向父进程;
(1)当父子进程都只读内存时,相安无事。
(2)当其中某个redis请求写操作时,CPU硬件检测到内存页是read-only的,于是出发页异常中断(page-fault),陷入内核的一个中断例程。中断例程中,内核会把出发的异常的页复制一份,于是父子进程各自持有独立的一份;
(3)然后父进程就可以继续执行写操作,子进程继续在原内存上进行RDB写入。
3.Redis事务
层面 | 解释 |
事 务 | (1)Redis的单个操作都是原子性的,要么执行要么不执行; (2)Redis的批量操作都可以通过【multi指令(事务开启)和exec指令(事务提交)】或者【lua脚本】实现事务; (3)Redis不支持事务回滚(非原子性):Redis事务可以理解为一个打包的批量执行的脚本,单个操作是原子性的,但是批量指令不是原子的;事务中的某条指令执行失败,并不会导致前面的指令回滚,也不会组织后面的指令继续执行; 不支持事务回滚的原因:【Redis命令只会因为语法错误而失败;在Multi命令开启事务后,这些语法错误的指令会入队失败,并被redis服务器进行记录;待执行exec命令后,如果发现有入队失败的记录,则事务就会拒绝执行并取消事务;因为这也保证了文明【redis不需要进行回滚】;保证redis内部的简洁和高效;】 |
4.Redis数据结构
常用数据类型 | 描述 |
String
| 最常见的一种数据类型,普通的key-value存储都可以归为此类。其中value既可以是数字,也可以是字符串。常规key-value缓存应用。微博粉丝数统计等。 |
Hash
|
包含键值对的无序散列表。适用于存储对象的序列化JSON字符串
|
Set
| 无序的去重集合,提供了计算【交集、并集】等操作,适用于【社交网络求共同好友、共同关注】等; |
List
| 有序可重复的集合,底层双向链表,提供【正向与反向查找】操作,适用于粉丝列表、文章的评论之类的等功能; |
ZSet
| Set的排序版,去重但可以排序,维护了一个score权重参数,适用于【排行榜、带权重的消息队列】等场景; |
三种特殊数据类型:geospatial地理位置、hyperloglog、bitmaps
5.Redis的键过期删除策略
策略 | 描述 |
定时删除 | 设置过期时间,到期立刻删除 |
惰性删除 | 用到这个key的时候,判断它是否过期,过期就删除,返回null |
定期删除 | 每隔一段时间,对一些key进行检查,过期就删除 |
设置过期时间和永久有效:expire(秒级)或pexpire(毫秒级)命令设置过期时间 , persist keyneme移除过期时间,设置为永不过期
AOF主从同步过程中如何处理过期的key?
salve节点并不会主动删除过期的key;master当检测到某个key过期后,那么这个删除key的命令会随着AOF文件一起从master节点发送至所有的slave节点,salve收到master的DEL命令后,才会去删除key;保证主从同步的一致性;
6.Redis内存淘汰算法和原理?
Redis里面的内存淘汰策略是指当内存的使用率达到了maxmemory的上限的时候,它的一个内存释放的一种行为。Redis里面提供了很多种内存淘汰算法,归纳起来呢主要有四种,第一种是Random算法,随机移除某个key;第二种是TTL算法,就是在设置了过期时间的键里面呢,去找到更早过期时间的key,进行有限的移除;第三种是LRU算法,去移除最近很少使用的key;第四种是LFU算法,跟LRU算法类似。LRU是一种比较常见的内存淘汰算法,在Redis里面,它会维护一个大小为16的候选池,而这个候选池里面的数据会根据时间进行排序,每次随机抽取5个key,放到候选池里面,当候选池满了 以后,访问的时间间隔最大的key就会从候选池里面取出来淘汰掉,通过这个设计呢,就可以把真实的最少访问的key,从内存里面淘汰,但是这样一种LRU算法还是存在一些问题,假如一个key很长时间没有被访问,但是最近偶然被访问,那么LRU就会认为这是一个热点key,不会被淘汰,所以在Redis4里面,增加了一个LFU的算法,相比于LRU,LFU呢去增加了访问频率这样一个维度来统计数据的热点情况。LFU的主要设计是使用了两个双向链表去形成一个二维的双向链表,一个用来保存访问频率,另一个是用来保存访问频率相同的所有元素,当添加元素的时候,访问频次默认为1,于是找到相同频次的节点,然后添加到相同频次节点对应的双向链表的头部,当元素被访问的时候,就会增加对应key的访问频率,并且把当前访问的节点移动到下一个频次节点。当然有可能会出现某个数据前期的访问次数很多,但是后续一直不使用了。所以如果单纯按照这样的一个访问频次来进行 淘汰的话这个key就很难被淘汰掉,所以在LFU的算法里面去通过使用频率和上次访问时间来标记数据的这样一个热度,如果某个数据有读和写,就增加访问频率,如果一段时间内这个数据没有读写,那么就减少访问频率。所以啊通过LFU算法改进之后,就可以真正达到非热点数据的淘汰。当然LFU也有缺点,相比LRU算法,LFU增加了访问频次的维护,以及实现的复杂度要比LRU更高。以上就是我我对这个问题的理解。
在业务场景中,LRU算法还可以用来解决Top10的问题,或者在IM类问题中,缓存最近的聊天记录等等。
策略 | 描述 (LRU置换算法、ttl生存时间值、LFU最不经常使用) |
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以上 |
主从复制 | 优点: 高可用,能够实现读写分离; 缺点:(1)故障恢复复杂,master故障时,需要手动去选择一个slave节点作为新的master; (2)redis复制中断后,slave发起psync,如果同步时会进行全量同步,此时会卡顿;且全量同步过程中由于写时复刻COW机制,可能会造成主库内存溢出; | 读写分离 |
主从复制+哨兵集群 | 优点:(1)master故障,无需认为指定新的master,会有选举机制选出新的master (2)高可用,有主观下线和客观下线两种机制,不轻易实施故障转移; | 主要解决单点故障问题,QPS在10w级以上 |
Redis集群 | 异步复制,不能保证数据的强一致性 | 缓存1T等超多数据 |
Redis自研 | 高可用,但是实现复杂,开发成本高 |
三、Redis使用场景?
场景 | 应用 |
缓存 |
合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在
Redis
用在缓 存的场合非常多
|
分布式锁 | setnx命令获取锁,返回1则说明获取锁成功,反之失败;减库存、秒杀等场景等;信号量等。 |
分布式会话 | 实现分布式session,避免用户重复认证; |
消息队列 | Redis提供了发布/订阅阻塞队列,可以简化消息队列;实现业务解耦,流量削峰和异步消息 |
排行榜 |
有序集合数据类构能实现各种复杂的排行榜应用。
|
社交网络 | 电子、共同好友等功能,key-set,key-hash等数据类型 |
最新列表 | key-list数据结构提供了【正向、反向查找】 |
计数器 | 商品浏览量+1等,保证时效性,incr命令操作内存+1 |
四、如何保证缓存与数据库的一致性?
方案 | 潜在问题 | 解决方法 |
先删除缓存,后更新数据库 | 缓存可能会存储脏数据(A更新,B查,A删B查B写缓存) | (1)延迟双删:防止请求A更新数据库过程中,请求B查缓存,没查到,然后将A没有更新之前的数据刷入缓存;更新之后休眠1s,再次淘汰缓存。 (2)在主从模式下发现更新命令,则强制请求主库查询:先删除缓存,有可能此时请求B去从库查询,但是从库还没有同步完成,此时查询从库,会读到未更新前的数据,解决方法就是:强制在主库同步的过程中,让请求去查询主库。 (3)利用JVM的内存队列实现【更新与读操作的异步串行化】:系统内部维护n个内存队列;先删除缓存,然后更新的请求入队列;当另一个查询请求过来后,读到了空缓存;那么此时先不读数据库,而是将重新读取数据+更新缓存的请求页也发送到队列中,排在数据库的更新请求之后;等到数据库与缓存的更新请求都执行结束,此时才开始读;注意:可以做读操作去重 |
先更新数据库,后删除缓存 | 缓存可能会删除失败 | 订阅binlog日志+消息队列补偿机制:先更新数据库,redis订阅binlog日志;若缓存删除失败,则各个redis服务器拉取binlog日志,进行缓存的更新 |
五、缓存异常及解决方案
1.什么是缓存预热
缓存预热是指系统上线后,提前将相关缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中,对数据库造成流量的压力。
缓存预热解决方案:
数据量不大的时候,工程启动的时候进行加载缓存动作; |
数据量大的时候,设置一个定时任务脚本,进行缓存的刷新; |
数据量太大的时候,优先保证热点数据进行提前加载到缓存。 |
2. 什么是缓存降级?
3.针对所有异常的统一解决方案:
事前设置热点key永不过期
(1)第一步:先查布隆过滤器(DB中所有的key全存储在布隆过滤器中),过滤掉查询数据库中不存在的数据的请求;
(2)第二步:经过布隆过滤器的key允许查询缓存;在某个线程redis缓存中首次查询不到时,抢互斥锁,只允许一个线程访问数据库;
(3)第三步:若数据库中有,则访问完DB将数据写入缓存,后期的其他线程直接访问缓存即可;若数据库中没有,则将(key,null)写入缓存。
问题 | 描述 | 解决方案 |
缓存穿透 | 查询缓存中不存在的key,不能命中缓存,导致每次都去DB中查询 | (1)将不存在的key也存入缓存中,value=null;这种方式不适用于大规模随机的key; (2)布隆过滤器:将所有可能存在的key哈希到一个足够大的bitmap中,当一个一定不存在的key访问时会被bitmap拦截掉,避免对数据库的直接访问; |
缓存击穿 | 大并发请求+某个热点key过期,在缓存中没有;导致重复去访问DB; | (1)缓存失效后,使用互斥锁或队列快照访问DB的线程数量;例如:使用Redis中的setnx去设置一个互斥锁,当获取到互斥锁后,再进行数据库操作并回设缓存,否走重试get缓存的方法; (2)热点key设置永不过期:物理不过期,逻辑过期(过期事件也存储在value中,当发现快过期时,后台启动异步线程去重制过期时间) |
缓存雪崩 | 大规模的key同时过期(redis宕机或者key设置的过期时间一样) | (1)互斥锁控制访问DB的数量:保证缓存失效时,只有一个线程能获得到锁,进而访问数据库更新缓存;期间其他线程等待重试; (2)分散缓存失效时间:在原有的失效时间基础上增加一个随机值; (3)分级缓存,上一级缓存失效,则访问下一级缓存(每一级缓存的过期时间都不同) (4)熔断机制:限流降级;超过阈值的并发请求直接提示“系统拥挤”; (5)主从模式+哨兵:防止redis宕机导致全面崩溃; (6)开启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命令,检查是否有入队失败的记录来决定是否提交事务 (1)若有入队失败记录,则拒绝执行并取消该事务; (2)若没有入队失败记录,则正常执行并提交事务 |
执行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命令实现事务回滚的效果 |
(1)【watch命令】可以为Redis事务提供CAS操作 |
(2)我们可以使用watch命令来监视一个或者多个key,如果被监视的key在事务执行前被修改那么本次事务会被取消;也就是实现了“事务回滚” |
(3)只有确保被监视的key,在事务开始前到执行的时间段未被修改过,事务才会执行成功(类似乐观锁) |
(4)如果依次事务中存在被监视的key无论此次事务执行成功与否,该key的监视都将会在执行后失效,也就是说监视是一次性的 |
七、Redis线程模型
1.Redis的IO多路复用模型
redis执行命令请求过程
当用户请求redis读写命令时,基于以下步骤:
步骤 | 描述 |
Step1 | 用户线程阻塞,向内核空间请求数据 |
Step2 | 内核空间向硬件磁盘请求数据,并拷贝到内核空间缓冲区 |
Step3 | 内核缓冲区拷贝数据到用户空间 |
Step4 | 用户线程拿到了数据,开始执行redis命令请求,执行完毕将结果写入用户空间缓冲区 |
Step5 | 用户缓冲区数据拷贝到内核缓冲区,然后写入硬件磁盘 |
·读数据时,要从磁盘读取数据到内核缓冲区,然后拷贝到用户缓冲区
·写数据时,要把用户缓冲区数据拷贝到内核缓冲区,然后写入磁盘
1)阻塞式IO(BIO)
在上述磁盘、内核空间与用户态的交互过程中,主要可以分为两个阶段,阻塞式IO在这两个阶段,用户线程都处于阻塞状态;
那么如果每个redis请求都分配一个线程,那么所有线程在等待数据拷贝数据的过程中,都是阻塞的,效率太差;
阶段 | 描述 | 用户线程 |
---|---|---|
阶段一 | ① 内核向硬件磁盘请求数据 ② 内核空间数据就绪 | 阻塞 |
阶段二 | ①从内核空间拷贝数据到用户空间 ② 拷贝完成 | 阻塞 |
2)非阻塞式IO(NIO)
非阻塞式IO虽然在第一阶段不阻塞,但是CPU会一直自旋的请求数据;
如果为每一个redos请求都分配一个线程,如果每个线程等待线程数据就绪的时间都很长,那么多个线程就会一直自旋式的向用户态请求数据,CPU使用率会暴增,但是效率却依旧很差;
阶段 | 描述 | 用户线程 |
---|---|---|
阶段一 | ① 内核向硬件磁盘请求数据 ② 期间用户线程一直自旋的向内核请求数据 ③ 内核态数据准备就绪,用户线程不再自旋,等待数据拷贝 | 非阻塞 |
阶段二 | ①从内核空间拷贝数据到用户空间 ② 拷贝完成 | 阻塞 |
3)IO多路复用(Redis)
概念 | 描述 |
---|---|
文件描述符(File Descriptor) | 简称FD,是一个非负的整数,本质上是一个索引值。当打开一个文件时,内核向进程返回一个FD,后续read、write这个文件时,只需用这个文件描述符来标识该文件,将其作为参数传入read、write。 |
IO多路复用 | 是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源 |
IO多路复用使用一个线程监听多个socket的句柄(fd),当某个socket数据准备就绪时,就会返回一个readable事件,通知用户线程,充分利用CPU资源;避免BIO对某个socket一直阻塞以及NIO的CPU空转问题;
而且用户线程不用分配多线程,一个线程就可以处理多个客户端的请求。
阶段 | 描述 | 用户线程 |
阶段一 | 进程调用select、poll、epoll模型来监听多个socket的FD信息; 期间任一socket的数据准备就绪,就返回readable事件 | 阻塞 |
阶段二 | (1)用户线程找到就绪的socket;(2)依次调用recvfrom读取每个就绪状态的数据(按照事件的顺序进行读取,不会一直卡在一个socket上);(3)内核将数据拷贝到用户空间 | 非阻塞 |
IO多路复用实现方式
4)信号驱动IO(不常用)
5)阻塞式IO(BIO)
2.Redis真的是单线程的吗?
(1)Redis6.x之前的线程模型
· 客户端与Redis的通信过程
·事件处理器的模型
事件处理器(连接应答处理器、命令处理器、命令恢复处理器),当一个事件处理器处理完一个事件后,IO多路复用才会向文件事件分派器分配下一个事件
步骤 | 阶段 | 描述 |
Step1 | Redis启动 | 将连接应答器与【AE—READABLE】事件关联 |
Step2 | 客户端请求与redis服务器建立通信连接 | (1)server socket收到连接请求,产生【AE—READABLE】事件 (2)【AE_READABLE】事件被IO多路复用程序监听到,进入事件队列; (3)【AE_READABLE】事件被事件分配器分给【连接应答器】进行处理; (4)连接成功,【AE_READABLE】事件与【命令请求处理器】关联; |
Step3 | 客户端发起读写请求 | (1)redis创建一个socket事件,产生【AE_READABLE】事件; (2)【AE_READABLE】事件被IO多路复用程序监听到,进入事件队列; (3)【AE_READABLE】事件被事件分配器分配给【命令请求处理器】进行处理; (4)工作线程单线程的操作redis执行命令,将执行结果存入命令回复处理器,然后将【AE_WRITEABLE】事件与【命令回复处理器】关联; |
Step4 | 客户端接收操作 | (1)【当socket满足可写条件】,表示客户端准备好接收数据,产生【AE_WRITEABLE】事件; (2)【AE_WRITEABLE】事件被IO多路复用程序监听到,进入事件队列; (3)【AE_WRITEABLE】事件被事件分配器分配给【命令回复处理器】进行处理; (4)命令回复器将操作结果写入socket,供客户端读取; |
Step5 | 清除事件与处理器关系 | 命令回复处理器处理完后,将【AE_WRITEABLE】事件与【命令恢复处理器】关联关系清楚 |
(2)Redis6.x之后的线程模型
就是多线程IO处理网络读写与协议解析+IO多路复用。之前都是单线程执行的。之后引入了多线程处理网络IO请求和协议解析。
Writeable;
3.Redis是线程安全的吗?
redis是单线程执行的,内部能够保证线程安全,但是外部使用的时候,业务逻辑需要我们自行保障。
更多推荐
所有评论(0)