Redis面试题
一、3种常用的缓存读写(数据库双写)策略详解
参考文章:https://blog.csdn.net/m0_61802230/article/details/124109238
https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html
1.旁路缓存模式(Cache Aside Pattern)——Cache起辅助作用,写操作先写DB再删除Cache
(如何保证缓存与数据库双写时的数据一致性——使用cache aside pattern)
读:先读Cache,若存在直接返回,若不存在则读DB并返回,之后由客户端将数据放到Cache中;
写:先写DB,然后删除Cache中的数据;
特点:可以保证较强的数据一致性;
使用场景:对数据一致性要求较高的场景
在写数据时,可以先删除Cache中的数据,再写DB吗?
答:不可以,可能会导致DB和Cache中的数据不一致,比如线程1先请求写入数据A,在删除Cache中的数据A开始向DB中写入数据A时,线程2请求读取数据A,此时会将尚未DB中尚未更新的数据A放到Cache中,最后DB中的数据A更新完成后就和Cache中的数据不一致了;
在写数据时,先写DB,然后删除Cache中的数据一定不会导致数据不一致吗?
答:理论上仍然有可能导致数据不一致,但概率很低,
存在这样一种情况,线程1先请求读取数据A(此时Cache中不存在数据A),在从DB读取数据并向Cache写入数据A的同时,线程2请求写入数据A,若线程2的写入DB的操作 快于 线程1从DB读取数据并向Cache写入数据A的操作,则有可能导致数据不一致(写入Cache的是更新之前的数据A);
但这种情况发生的概率很低,因为DB的写操作通常都是要加锁的,速度要慢于读操作,不太可能在读操作完成之前先完成写操作;
2.读写穿透模式(Read/Write through pattern)——DB和Cache高度一致
读:先读Cache,若存在直接返回,若不存在则读DB并由Cache自行写入数据,之后再返回;
写:先查Cache,若不存在则直接写入DB,若存在则先写入Cache,然后由Cache自行同步更新DB;
使用场景:很少使用,主要Redis没有提供将缓存中的数据写入DB的功能;
3.异步缓存写入模式(Write Behind Pattern)——以Cache为主,异步更新DB
读:先读Cache,若存在直接返回,若不存在则读DB并由Cache自行写入数据,之后再返回;——同读写穿透
写:先查Cache,若不存在则直接写入DB,若存在则写入Cache,之后由Cache异步、批量地更新DB;
特点:写性能非常好,缺点是数据一致性不高(存在Cache尚未将数据同步到DB就关闭了的可能性)
使用场景:写操作较多,同时对数据一致性要求不高的场景;
二、Redis的数据结构
1.五种基本数据结构
①String字符串
可以存储任何类型的数据,比如字符串、整型、浮点数、图片或任何序列化之后的对象;
应用场景:
SET、GET——缓存 session、token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存);
INCR、DECR——需要计数的场景,如记录点赞数;
如何使用redis实现分布式锁?
可以通过给SET命令添加NX参数,配合Lua脚本来实现分布式锁;
具体来说,客户端在执行需要加锁的操作前,通过SET命令和NX参数为锁设置一个该客户端特有的value,来互斥地为该操作加锁;
在需要删除锁时,首先通过value来判断删除锁的是不是加锁的客户端,若是则使用DEL删除key,这两步必须是原子操作,可以通过Lua脚本来实现;
②List列表
实际就是一个双向链表,可以从头部或尾部插入/读取数据,相比队列或栈更方便操作,同时内存开销也更大;
应用场景:
LPUSH、LRANGE——推送和查看公众号最新更新的文章;
③Hash哈希表
一个String类型的键值对映射表,适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值;
应用场景:
HSET、HGET——存储和修改用户信息、商品信息等这种包含多个属性的对象;
④Set集合
无序集合,集合中的元素没有先后顺序但都唯一,类似Java中的HashSet;
应用场景:
SCARD——获取集合中元素数量,可用于存放和统计所有点赞用户等;
SINTER、SUNION、SDIFF——取交、并、差集,可用于统计共同好友(交集)、共同关注(交集)、好友推荐(差集)等场景;
SPOP、SRANDMEMBER——随机获取集合中的元素,可用于随机抽奖的场景;
⑤SortedSet有序集合
和 Set 相比,Sorted Set 增加了一个权重参数score,使得集合中的元素能够按score进行有序排列,还可以通过 score的范围来获取元素的列表;
应用场景:
ZRANGE、ZREVRANGE、ZREVRANK——各种排行榜、求排名的场景;
2.三种特殊数据结构
①BitMap位图
可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)
应用场景:
SETBIT、GETBIT、BITCOUNT、BITOP——0/1即可表示状态信息的场景,例如用户签到情况统计;
②HyperLogLog
基于基数概率算法,用于统计一个集合中不重复的元素个数,特点是占用空间非常小,只需要 12k 的空间就能存储接近2^64个不同元素;
应用场景:
PFADD、PFCOUNT——用于数据量巨大的计数统计的场景,例如网站UV(unique visitor独立访客)统计;
③Geospatial Index
主要用于存储地理位置信息经纬度,基于 Sorted Set 实现
应用场景:
GAOADD、GEORADIUS、GEORADIUSBYMEMBER——用于需要使用地理位置数据的场景,例如计算附近的人;
3.redis数据结构相关问题
①String 还是 Hash 存储对象数据更好呢?
分情况而言,String存放的是整个对象,Hash对对象的每个属性单独存储,
使用String会更加节省内存,而使用Hash可以很方便地对对象的属性进行操作;
②String底层实现是什么?
redis是基于C语言写的,但是Redis 的 String 类型的底层实现并不是 C 语言中的字符串,而是自己编写了SDS(simple dynamic string,简单动态字符串)作为底层实现;
SDS的特点是:
①有5种实现方式,区别在于长度不同,可以根据字符串实际长度来选择,节约内存空间;
②包含了字符串长度、剩余可用空间大小、用于存储字符串的char型数组、低三位的保存类型标志四个属性;
③可以以O(1)的时间复杂度直接获得字符串长度;
三、Redis持久化机制
1.RDB持久化
①什么是RDB持久化?
就是通过创建快照来获得某个时间点内存中数据的副本,
这个快照可以复制到其他服务器上创建服务器副本,或是用于重启服务器时恢复内存数据;
RDB持久化也是Redis默认的持久化方式,可以在配置文件中设置生成快照的频率;
②RDB创建快照会阻塞主线程吗?
使用save命令会阻塞主线程,使用bgsave则会在子线程中创建快照,不会阻塞主线程;
2.AOF持久化
①什么是AOF持久化?
就是将每一条写命令(增删改)都写入AOF文件中,类似于日志文件,AOF文件也可以用于数据恢复;
具体来说这些命令首先会写入内存中的AOF缓冲区,之后再写入系统内核缓冲区中的AOF文件,最后根据对应的持久化方式选择时机将AOF文件同步到磁盘中;
②AOF的持久化方式有哪些?(fsync策略,又称刷盘策略)
always:主线程每执行一条写命令,就立刻将其同步到磁盘中,这种方式会严重降低redis的性能;
everysec:每隔一秒将内核缓冲区的AOF文件同步到磁盘中;
no:让操作系统自行决定何时将内核缓冲区中的AOF文件同步到磁盘中(在Linux中通常是30s一次);
③AOF为什么是执行完命令再记录日志?(关系型数据库如MySQL中都是先记录日志再执行命令)
先记录日志再执行命令是为了方便故障恢复,
而因为AOF记录日志是在redis主线程中进行的,为了避免阻塞当前命令的执行,AOF中选择先执行命令再记录日志;
不过这样也有风险,一是如果执行完命令redis就宕机,则会丢失数据,二是可能会阻塞后续命令的执行;
④介绍一下AOF重写
当AOF文件太大时,redis就会在子线程中重新生成一个新的体积更小且数据库状态保持一致的AOF文件,用来替代原来的AOF文件;
具体来说,新AOF文件的生成是通过读取数据库中的键值对来实现的,类似RDB快照的生成,是无需参考原AOF文件的;
AOF重写期间增量数据如何处理?
在AOF重写过程中执行的写命令,会单独记录到内存中的AOF缓冲区中,待新AOF文件生成之后再添加到文件末尾,
由于这些写命令既要存入原AOF文件中,又需要存入到新AOF文件中,因此会写入磁盘两次,同时也会额外消耗内存,这一问题在redis7.0之前一直未解决;
在7.0中通过引入Multi-part AOF 机制解决了此问题;详见:https://developer.aliyun.com/article/866957
⑤AOF校验机制
就是redis在启动时会对AOF文件进行检查,以判断文件是否完整;
具体来说就是通过CRC64算法计算整个AOF文件的校验和,来与AOF文件末尾保存的校验和进行比对(计算校验和时不包括末尾的校验和),相同则说明文件完整,反之文件不完整;
RDB也有此机制
3.Redis4.0对持久化机制做了什么优化?
4.0开始支持RDB+AOF混合持久化,默认关闭,可以通过配置项aof-use-rdb-preamble开启,
相比于纯AOF持久化,混合持久化在AOF重写时会直接将RDB快照写到AOF开头,
这样做的好处是快速加载数据,避免丢失数据,缺点是AOF文件里的RDB部分是压缩格式,可读性较差;
4.如何选择RDB和AOF?
RDB相比AOF优秀之处在于:
①RDB文件存储的是经过压缩的二进制数据,文件很小,适合做数据备份;
②RDB还原大数据集时速度更快,不用像AOF那样一条语句一条语句地执行;
AOF相比RDB优秀之处在于:
①数据安全性更高,可以实现实时或者秒级持久化数据;
②AOF文件存储的是操作命令,可读性高,可以通过手动操作AOF文件来解决一些命令,比如误输入的命令只要还没重写,就可以通过删除该命令并重启redis来恢复数据;
如何选择:
①数据安全性要求不高,就用RDB;
②数据安全性要求高,就用rdb+aof混合持久化;
③不建议单独使用AOF,因为RDB快照对于数据库备份、重启都是非常好用的;
四、缓存的分类——本地缓存和分布式缓存
1.为什么要用缓存?
缓存其实就是以空间换时间,就是将原本存储在较慢的空间中的数据选择一部分在较快的空间中也存储一份,这样在下次读取这些数据时就可以直接从后者进行访问,大大加快访问速度;
缓存的应用有哪些?
①CPU Cache:解决CPU处理速度和内存不匹配的问题;
②内存缓存:解决硬盘访问速度过慢的问题;
③TLB快表:提高虚拟地址到物理地址的转址速度;
除了缓存之外,以空间换时间的思想还有哪些应用案例?
①索引:索引就是一种将数据库表中的某些字段按照一定的排序规则组织成一个单独的数据结构,需要占用额外空间,但可以大大提高检索效率,降低排序成本;
②数据库表字段冗余:将经常联合查询的数据冗余存储在同一张表中,减少多表关联查询的次数,提高查询性能;
③CDN(内容分发网络):将静态资源分发到多个不同的地方实现就近访问,加快静态资源的访问速度;
2.缓存的分类
①本地缓存
什么是本地缓存?
就是将数据存储在当前机器的内存中;
有哪些实现方案?
如EhCache、Guava、Caffeine等;
优缺点:
优点是请求本地缓存的速度非常快,没有额外的网络开销,;
缺点是对分布式架构支持不友好,本地缓存只能在本地机上存储,其次本地缓存容量受服务部署的机器的限制,如果本地机器当前系统服务耗费大量内存,就没有多少内存可供本地缓存使用了;
②分布式缓存
什么是分布式缓存?
就是将数据存储在远程服务器的内存中;
有哪些实现方案?
主流的就是Redis;
优缺点:
优点是对分布式架构支持的很好,多台机器可以共用一份缓存,且容量和性能不受当前机器的限制;
缺点是系统复杂性增加,且需要一定的网络开销;
③多级缓存
就是以本地缓存作为一级缓存,分布式缓存作为二级缓存,读取数据时先到一级缓存中去读,如果一级缓存中没有再去二级缓存中读取;
优点是性能更加强大;
缺点是维护负担较大,需要保证一级缓存和二级缓存的数据一致性;
适用场景:
①缓存的数据不会频繁修改,比较稳定;
②数据访问量特别大,比如秒杀场景;
五、Redis主从复制:主从节点之间如何同步数据?
1.什么是主从复制?
就是将一台redis主节点的数据复制到其它从节点中,并尽可能保证主、从节点之间数据的一致性;
主从复制不仅保障了redis服务的高可用,还实现了读写分离,提高了系统的并发量,尤其是读并发量;
2.主从复制下从节点会主动删除过期数据吗?
会,
redis中常用的过期数据删除策略有两个:
一个是惰性删除,只有在取出key时才对数据进行过期检查,这种方式对CPU友好,但会产生大量未删除的过期数据;
另一个是定期删除,每隔一段时间抽取一批key进行过期检查,删除过期的key,这种方式对内存友好,但会给CPU带来负担;
redis采用的是两种删除策略共用的方式;
主从复制下从节点会读到过期数据吗?
有可能,
①redis3.2之前,从节点读取数据是不会进行过期检查的,可能会返回过期数据,3.2之后就会进行过期检查了,如果key过期就会删除数据并返回空值;
②如果是采用expire/pexpire设置过期时间的话,因为这个命令是从执行完命令之后开始计时,而主从节点之间的命令同步可能存在延迟,
这就会导致从节点的实际过期时间比主节点晚,在此期间可能会从从节点中读到过期数据;
如何避免读到过期数据?
可以使用expirea/pexpireat设置过期时间,这个命令是用时间戳来设置过期时间,只要保证主从节点的时钟一致,就能保证过期时间一致,从而避免在从节点中读到过期数据;
3.主从节点之间如何同步数据?
①2.8版本之前的SYNC方案
slave发起复制请求,master收到请求后让子线程执行BGSAVE命令全量复制生成RDB文件发给slave,salve解析RDB文件更新本地数据;
对于master在执行BGSAVE之后执行的写命令,会单独存在一个叫做replication buffer复制缓存区的地方,待slave更新数据完成后就发给slave,salve执行执行命令更新数据,
之后master和slave之间会持续维护一个长连接来同步写命令;
这种方案的问题在于BGSAVE命令会消耗大量的内存资源和CPU资源,而SYNC方案中slave和master断开连接后,重新连接都需要执行BGSAVE进行全量同步;
②2.8版本的PSYNC方案
PSYNC方案中,slave会记录master的runid和自己的复制偏移量offset,master也会记录自己写入缓冲区的偏移量,
在主从节点断开重连时,在runid匹配的情况下,通过master和slave记录的偏移量,就可以将slave中缺少的数据同步过去,称为增量同步;
master如何通过复制偏移量找到slave缺少的数据?
master通过一个环形的复制积压缓冲区来记录从生成RDB文件开始收到的全部写命令,这个缓冲区每个master只有一个,由它和它的全部slave共用;
PSYNC方案的问题在于,当salve宕机或是重启,丢失了runid和offset,或是当master宕机进行主从切换,新的maser的runid、offse都发生变化时,还是要进行全量更新;
③4.0的PSYNC2.0方案
在2.0方案中,舍弃了runid,取而代之的是replid和replid2,
对于master来说,replid就是自己的id,没有发生主从切换时,replid2为空,发生主从切换后,新master的replid2就是旧master的replid;
对于slave来说,replid就是自己当前同步的master的replid,replid2就是自己上一个同步的master的replid;
然后还新增了两个和偏移量有关的字段:
一个是master_repl_offset——当前的复制偏移量;
一个是second_replid_offset——没有发生主从切换时,second_replid_offset为-1,发生主从切换后,新master的second_replid_offset就是旧master的master_repl_offset;
在2.0方案中,即使发生了主从切换,仍有可能进行增量同步(因为新的master会记录旧maser的id和offset,新slave也会记录自己上一个同步的master的id);
④总结——简略版
在2.8版本之前,只能使用全量同步,也就是当slave发起同步请求时,master让子线程执行BGSAVE生成RDB文件传输给slave,slave收到RDB文件之后更新本地数据,
同时master在生成RDB文件期间执行的写命令也会单独存在一个叫做replication buffer复制缓存区的地方,待slave更新数据完成后就发给slave,salve执行执行命令更新数据;
全量同步的问题在于BGSAVE命令会消耗大量的内存资源和CPU资源;
在2.8版本之后,引入了运行id和偏移量的概念,当运行id匹配时,通过偏移量的比对,可以只执行增量同步,也就是不需要执行BGSAVE命令生成RDB文件,
只将slave中缺少的那部分数据同步过去就可以了,大大降低了数据同步的开销;
4.为什么主从全量复制使用RDB而不是AOF?
①RDB文件存储的是经过压缩的二进制数据,文件很小,而AOF会记录每一次写命令,通常会大很多,使用RDB文件在传输时更节省带宽;
②使用RDB文件同步数据时直接还原数据即可,无需像AOF文件那样一条指令一条指令地执行,在恢复大数据集时,使用RDB文件更快;
5.主从复制方案有什么痛点?
痛点在于一旦master宕机,就需要我们去从slave中选取新的master,并让slave复制新的master,整个过程都需要人工干预,响应速度慢且容易出错;
六、Redis Sentinel:如何实现自动化地故障转移?
1.什么是sentinel?
就是在主从复制的基础上,在主从节点之外,新增多个sentinel节点,用来监控master和slave节点的运行状态,
并当master节点发生故障时,自动选出一个slave升级为master,保证redis服务的可用性;
2.sentinel的作用有哪些?
①监控所有redis节点的运行状态,包括sentinel节点本身;
②故障转移:当master节点发生故障时,自动选出一个slave升级为master,保证redis服务的可用性;
③通知:通知slave新的master节点的信息,让其执行replicaof称为新master的slave;
④配置提供:客户端连接sentinel请求master的地址,当发生故障转移时,sentinel会自动通知新的master信息给客户端;
(如果要实现高可用,通常sentinel节点不少于三台)
3.sentinel如何检测节点下线?
sentinel节点通常会以每秒一次的频率向集群中的每个节点发送PING命令,如果某个节点超过一定时间未进行有效回复的话,该sentinel节点就会认为此节点已下线,
这种情况称为主观下线SDOWN;
当一定数量的sentinel节点都认为某个节点主观下线时,这种情况就称为客观下线ODOWN;
当master节点被认定客观下线时,就会触发故障转移;
4.sentinel是如何选举出leader的?
使用了Raft算法,详见:https://javaguide.cn/distributed-system/protocol/raft-algorithm.html
简而言之就是先到先得,即在一轮选举中sentinel A向B发出成为leader的申请,如果在此之前B没有同意过其它sentinel,就会同意A成为leader;
5.sentinel是如何进行故障转移的?(如何选出新的master)
首先leader会筛选出所有在线的salve节点,然后依次根据下面三个维度进行筛选:
①首先看每个slave节点的slave-priority属性的值,值越小表示优先级越高,0除外,0表示不参与master选举;
②其次看每个slave节点的复制进度,也就是数据越完整,从master复制数据越快,这一项得分越高;
③如果上面两项都一样,那就看每个节点的运行id,值最小的选为新master;
6.sentinel可以防止脑裂吗?
参考文章:https://blog.csdn.net/Andrew_Chenwq/article/details/127497081
什么是脑裂?
指由于网络分区或者硬件故障等原因,导致 Redis 集群中的节点互相失去连接,出现多个主节点为客户提供写服务,这种情况下可能会导致数据丢失;
sentinel可以防止脑裂吗?
可以通过配置下面两个字段来尽量规避脑裂:
①min-replicas-to-write:表示master必须至少写入slave的数量,否则就停止接收新的写请求;
②min-replicas-max-lag:表示当master经过多长时间得不到slave的响应时,就认为这个slave失联,停止接收新的写请求;
在假故障期间,通常都会出现master写入salve数量不达标或是有slave失联的情况,此时主节点拒绝写入,就可以避免脑裂造成的数据丢失问题;
不过即使如此也不能完全避免脑裂,要想完全解决问题需要引入redis cluster集群;
七、Redis Cluster:缓存的数据量太大怎么办?
1.为什么需要redis cluster?
因为主从复制和sentinel机制都只增加了slave的数量,不能在高并发场景下缓解写压力大、缓存数据量大的问题,
而redis cluster通过部署多台master,同时对外提供读/写服务,缓存数据库均匀地分布在这些master节点上,客户端的请求会根据路由规则发送到目标maser节点上;
同时为了保证每个master的高可用性,还为每个master配备若干slave节点并内置sentinel机制;
这样一来,redis cluser既继承了sentinel机制的主从复制、故障转移功能,又大大缓解了高并发场景下的写压力和缓存数据量大的问题;
2.一个基本的redis cluster架构是怎样的?
一个基本的redis cluster至少需要三主三从,也就是三个master和对应的slave,这里的slave不提供读服务,只在master出现故障时替换master;
当cluster中的任意master节点发生故障时,只要其还有可替换的slave节点,redis服务就仍可正常运行,
当没有可替换的节点时,默认情况下为了保证所有的哈希槽都可用,cluster会停止服务,如果这种情况下还想要集群继续服务的话,可以通过修改配置文件中的cluster-require-full-coverage字段为no;
当需要添加新的master时,只需要重新分配哈希槽即可;
当需要删除master时,需要先将这个master占用的哈希槽分配给其它master,然后再进行删除操作;
3.redis cluster中的数据是如何分布的?
redis cluster采用了哈希槽分区的方式,共有2^14=16384个哈希槽,每个键值对都属于一个哈希槽;
要计算给定的key应该分配到哪个哈希槽,
首先要对key值计算CRC-16校验码,然后用校验码对16384取模,得到的结果即为对应的哈希槽;
当客户端发出读/写请求时,根据key值得到对应的哈希槽,再根据哈希槽和节点的对应关系,找到对应的master即可;
4.为什么redis cluster的哈希槽是16384个?
①哈希槽太大会导致心跳包太大,消耗太多带宽;
②redis cluster中主节点通常不会太多,16384个哈希槽足够了;
③哈希槽总数越少,对存储哈希槽信息的bitmap压缩效果越好;
5.redis cluster在扩容缩容期间可以提供服务吗?
可以,为了让redis cluster在扩容缩容期间仍然能够正常提供服务,redis cluster提供了重定向机制,分为两个类型:
一种是ASK重定向:这是一种临时重定向,后续查询仍发送到旧节点;
另一种是MOVED重定向:可以看作永久重定向,后续查询发送到新节点;
当客户端向指定节点发送请求,
若请求的key所在的哈希槽在该节点中,则直接响应客户端的请求;
若请求的key所在的哈希槽已经迁移到了其他节点中,则返回MOVED重定向错误,并告知客户端当前哈希槽由哪个节点负责,客户端会更新哈希槽分配信息,后续查询将会发送到新节点;
若请求的eky所在的哈希槽正在迁移过程中,且此时不在该节点中,则返回ASK重定向错误,并告知客户端哈希槽被迁移到的新节点,
客户端收到ASK重定向错误,将会自动发送一条ASKING命令给新节点,此命令不包含请求信息,只是询问新节点哈希槽迁移是否已完成,
若迁移未完成,则返回TRYAGAIN错误,
若迁移已完成,则客户端就可以发送请求了;
ASK重定向并不会让客户端更新哈希槽分配信息,该客户端下一个对应该哈希槽的请求还是会发给旧节点;(直到迁移完成返回MOVED重定向错误)
6.redis cluster各节点之间是怎么进行通信的?
redis cluster中个节点基于Gossip协议来进行通信共享信息,每个redis节点都维护了一份集群的状态信息,
各节点之间会相互发送各种gossip消息:
比如MEET消息,可以将一个redis节点加入到redis cluster中;
还有PING/PONG消息,可以用来检查各个节点的状态,包括在线状态、疑下线状态PFAIL、下线状态FAIL;
还有FAIL消息,当集群内半数以内的节点都认为某节点处于PFAIL状态,就会在集群中广播一条信息,让所有节点都将该节点标记为FAIL状态;
八、Redis线程模型
1.redis单线程模型
①什么是redis单线程模型?
在redis6.0版本之前,Redis 的核心网络模型一直是一个典型的单 Reactor 模型:利用 epoll/select/kqueue 等多路复用技术,在单线程的事件循环中不断去处理事件(客户端请求),最后回写响应数据到客户端,
因而称redis为单线程模型;
②既然是单线程,那么是如何监听大量的客户端连接的?
redis通过I/O多路复用程序来监听来自客户端的大量连接,然后将感兴趣的事件及类型注册到内核中并监听每个事件是否发生;
③什么是I/O多路复用?
一种同步 IO 模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出 cpu。
IO 是指网络 IO,多路指多个TCP连接(即 socket 或者 channel),复用指复用一个或几个线程。
I/O复用的三种实现方式:select、poll、epoll;
就是将进程处理单个时间的时间控制控制在一定范围内,这样进程就能够在一个时间段内并发地处理多个事件,类似CPU并发处理多个进程时的时分多路复用;
select/poll
select 和 poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的 Socket 集合。
在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。
很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。
epoll
epoll 通过两个方面,很好解决了 select/poll 的问题。
- epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。
- epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。
边缘触发和水平触发
epoll 支持边缘触发和水平触发的方式,而 select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高;
- 使用边缘触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
- 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
④什么是reactor模式?
reactor模式是一种常见的高性能服务器开发模式,它是一种事件驱动机制,它逆转了事件处理的流程,不再主动地等待事件的就绪,而是提前注册好回调函数,待对应事件发生时就调用回调函数;
核心思想就是利用I/O复用技术来监听套接字上的读写事件,并为每个套接字关联不同的事件处理函数,一旦某个套接字上发生可读、可写事件,就调用相应的事件处理函数;
⑤Redis到底是单线程还是多线程?
在6.0版本之前,redis在处理客户端的请求时,包括从socket读取命令、解析命令、执行命令、将结果写回socket都是由一个主线程顺序执行的,此时的redis基本可以认为是单线程的;
在6.0版本之后,redis在处理客户端请求时,除了执行命令仍旧以单线程的方式顺序执行外,其他步骤包括读取解析命令、写回结果都是以多线程的方式处理了;
⑥redis为什么快?
Ⅰ.基于内存操作:redis的所有数据都是存储在内存中的,所有操作都是内存级别的,因此性能非常高;
Ⅱ.数据结构简单:redis中使用的数据结构的大部分操作的时间复杂度都是O(1),效率非常高;
Ⅲ.多路复用和非阻塞I/O:redis使用了多路复用功能来监听多个socket连接客户端,这样就可以用一个线程来处理多个请求,避免了阻塞I/O操作;
Ⅳ.单线程避免了上下文切换:多线程存在线程切换的开销问题,还有可能会发生死锁,单线程则不存在这些问题;(如果问单线程的redis为什么快,可以加上这一条)
2.Redis多线程
①redis6.0之前为什么不使用多线程?
Ⅰ. 单线程编程代码简洁且易于维护;
Ⅱ.即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO;
Ⅲ. redis的性能瓶颈不在CPU,而在于内存和网络;
②6.0之前redis没有多线程的应用吗?
6.0之前redis的核心网络模型一直是单线程的,不过在4.0的时候引入了多线程来做一些异步操作,
主要是针对那些非常耗时的命令,通过将这些命令的执行异步化,避免阻塞单线程的事件循环;
这些非阻塞命令包括UNLINK(对应DEL)、FLUSHALL ASYNC(对应FLUSH)等,在处理的数据量特别大时,使用异步命令可以避免阻塞主线程;
③redis6.0之后为何引入了多线程?
因为随着互联网业务系统要处理的线上流量越来越大,redis的单线程模式会导致系统消耗大量CPU时间在网络I/O上,从而降低吞吐量,
为了提高网络I/O读写性能,redis6.0引入了多线程,
不过虽然redis6.0引入了多线程,但也只在读取和解析客户端命令、以及向客户端回写响应数据上进行了异步化,客户端命令的执行仍然是在主线程上以单线程顺序执行;
④什么是redis后台线程?
就是一些用来在后台执行一些比较耗时的操作的线程;
比如bio_close_file后台线程:用来释放 AOF / RDB 等过程中产生的临时文件资源;
bio_aof_fsync后台线程:调用fsync函数将内核缓冲区还未同步到磁盘的数据强制刷到磁盘的AOF文件中;
bio_lazy_free后台线程:释放已删除的大对象占用的内存空间;
九、Redis内存管理
1.redis给缓存数据设置过期数据有什么用?
一方面是因为内存空间是有限的,缓存数据无限期存储会占满内存;
另一方面有些数据本身是要求有时效性的,比如验证码,通常要求只能在一定时间内查询到;
2.redis如何判断数据是否过期?
redis通过一个叫做过期字典的哈希表来保存数据过期的时间,过期字典的key指向redis数据库中的某个key,value则是一个long long类型的过期时间;
3.redis中过期数据的删除策略
有两个:
一个是惰性删除,只有在取出key时才对数据进行过期检查,这种方式对CPU友好,但会产生大量未删除的过期数据;
另一个是定期删除,每隔一段时间抽取一批key进行过期检查,删除过期的key,这种方式对内存友好,但会给CPU带来负担;
redis采用的是两种删除策略共用的方式;
4.redis内存淘汰策略
主要有两类,一类是针对设置了过期数据的数据,有LRU最近最少使用、RANDOM随机挑选、LFU最不经常使用、TTL已经过期的数据,
还有一类是针对全部数据,有LRU最近最少使用、LFU最不经常使用、RANDOM随机挑选,还有一个no-evition,内存占满后禁止存入数据;
十、Redis事务
1.什么是Redis事务?
redis事务提供了一种将多个命令请求打包的功能,可以按顺序执行打包的全部命令,并且中途不会被打断;
2.如何使用Redis事务?
MULTI:开始输入事务命令;
EXEC:执行事务中的命令;
DISCARD:清空队列中的全部命令;
WATCH:监视指定的key是否被其它客户端修改,如果是的话,则本客户端的事务中如果含有修改该key的命令,则整个事务执行失败;
3.Redis事务支持原子性吗?
原子性指事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么全部不执行;
Redis事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
为什么不支持?
Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。
4.Redis事务支持持久性吗?
持久性指 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响;
Redis支持持久化,且有三种持久化的方式:RDB、AOF、RDB+AOF混合模式,
但是这几种持久化方式中除了AOF采用always的刷盘策略,否则无法保证不丢失数据,而always的刷盘策略性能很差实际开发中基本不会使用,
因此Redis事务是无法保证持久性的;
5.如何解决Redis事务的缺陷?
可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销
一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。
十一、Redis性能优化
1.使用批量操作减少网络传输
一条redis命令在客户端和服务器间传输是有往返时间的,发送一条命令还需要socket的I/O成本,
因此采用批量传输多条命令可以减少网络传输次数,同时减小socket的I/O成本。
批量操作有哪些:
①原生批处理命令
MSET、MGET、HMSET、HMGET、SADD等;
不过这种方式存在一个问题,那就是无法保证所有的key都在同一个哈希槽上,可能还是需要多次网络传输,
可以通过手动维护key和slot之间的关系,保证同一批处理的key都在同一个哈希槽上来解决;
②pipeline
可以通过pipeline将一批redis命令封装成一组,通过pipeline一次性地提交到redis服务器上,过程中只需要一次网络传输;
这种方式也存在和原生批处理命令相同的问题,那就是无法保证所有的key都在同一个哈希槽上,可能还是需要多次网络传输,
可以通过手动维护key和slot之间的关系,保证同一批处理的key都在同一个哈希槽上来解决;
pipeline和原生批处理命令的区别
Ⅰ.批处理命令是原子操作,而pipeline是非原子操作;
Ⅱ.pipeline可以打包不同的命令,而批处理命令不可以;
Ⅲ.批处理命令是redis服务器端支持实现的,而pipeline需要服务器和客户端的共同实现;
pipeline和redis事务的区别
Ⅰ.事务是原子操作,两个不同的事务不能同时运行,而pipeline是非原子操作,两个pipeline中的命令可以交错地执行;
Ⅱ.事务中的每条命令都要单独发给服务器,而pipeline中的命令可以一次全部发给服务器,请求次数更少;
另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 Lua 脚本 。
③Lua脚本
lua脚本支持批量处理多条命令,一个lua脚本可以视作一条命令来执行,一段lua脚本执行过程中不会有其它Lua脚本或是redis命令执行,可以看作是原子操作,
同时Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理;
不过Lua脚本也存在一些问题:
Ⅰ.如果Lua脚本运行时出错,那么后面的命令将不会执行,但前面已经执行的操作也不可撤销,因此Lua脚本也是不满足原子性的;
Ⅱ.在Redis cluster中,Lua脚本的原子操作也无法保证,因为无法保证一个Lua脚本中处理的key都在同一个slot中;
2.大量key集中过期问题
redis中对过期key的处理方式是定期删除+惰性删除,
这种方式存在一个问题,在定期删除的过程中,如果突然遇到大量过期key的话,由于定期删除是在主线程中进行的,
这就会导致客户端必须等待全部过期key都删除完才能正常请求服务,响应速度变慢;
解决办法有两个:
①给key设置随机过期时间,避免同一时间大量key集中过期;
②开启lazy-free延迟删除,让redis采用异步的方式延迟释放key使用的内存,将该操作交给子线程,不占用主线程;
推荐两种方式同时使用;
3.bigKey(大Key)
①什么是bigKey?
如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。
具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个;
②bigkey 有什么危害?
bigkey 除了会消耗更多的内存空间和带宽,对性能造成比较大的影响;
③如何发现 bigkey?
Ⅰ.使用redis自带的--bigkeys参数来查找
这种方式只能找到每种数据结构中最大的那个key,同时给出每种数据类型的键值个数与平均大小;
如果想要查找大于10kb的所有key,--bigkeys就做不到了;
同时,在线上使用--bigkeys命令时,为了降低对redis的影响,还需要加上-i参数,控制扫描的频率,比如设为0.1表示扫描过程中每次扫描后休息的时间间隔为 0.1 秒;
(MEMORY USAGE key:计算某个key值占用的内存大小,单位为字节)
Ⅱ.还可以用一些开源的工具通过分析RDB文件来找bigKey
④如何删除bigKey
Ⅰ.String
一般用del,如果过大就用unlink;
Ⅱ.hash
使用hscan和hdel分批次删除元素,最后用del删除key;
Ⅲ.list
使用ltrim分批次删除元素,最后用del删除key;
Ⅳ.set
使用sscan、srem分批次删除元素,最后用del删除key;
Ⅴ.zset
使用zscan、zremrangebyrank分批次删除元素,最后用del删除key;
⑤bigKey生产调优
开启lazy-free延迟删除,让redis采用异步的方式延迟释放key使用的内存,将该操作交给子线程,不占用主线程;
4.hotKey(热Key)
①什么是hotkey?
如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey。
hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。
②hotkey有什么危害?
处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。
此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。
因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。
③如何发现hotkey?
Ⅰ.使用redis自带的--hotkeys参数来查找
该参数能够返回所有 key 的被访问次数。
使用该方案的前提条件是 Redis Server 的 maxmemory-policy
参数设置为 LFU 算法,可以是volatile-lfu或是allkeys-lfu;
Ⅱ.使用monitor命令
命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。
由于该命令对 Redis 性能的影响比较大,因此禁止长时间开启。
在发生紧急情况时,我们可以选择在合适的时机短暂执行 MONITOR
命令并将输出重定向至文件,在关闭 MONITOR
命令后通过对文件中请求进行归类分析即可找出这段时间中的 hotkey。
Ⅲ.还可以根据业务情况提前分析哪些key可能成为热key,或是借助一些开源软件进行分析;
④如何处理hotkey?
Ⅰ.读写分离,主节点处理写请求,从节点处理读请求;
Ⅱ.采用redis cluster,将热点数据分布在多个redis节点上;
Ⅲ.采用二级缓存,将热点数据存储在本地缓存如Caffeine中,提高吞吐量;
5.慢查询命令
①什么是慢查询命令?
慢查询命令是指那些执行时间较长的命令,redis中虽然大部分命令都是O(1)的时间复杂度,但仍有一些遍历的命令时间复杂度是O(n),
例如keys *、hgetall、lrange、smembers、sinter/sunion/sdiff等,尽量少用这些命令,有遍历需求时可以使用hscan、sscan、zscan等;
还有一些时间复杂度为O(logn+m)的命令有可能成为慢查询命令,例如zrange/zrevrange、zremrangebyrank/zremrangebyscore,
其中n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,时间复杂度比O(n)还要高;
②如何找到慢查询命令?
可以在配置文件中通过slowlog-log-slower-than
参数设置慢查询命令的阈值,并使用 slowlog-max-len
参数设置慢查询命令的最大记录条数。
当 Redis 服务器检测到执行时间超过 slowlog-log-slower-than
阈值的命令时,就会将该命令记录在慢查询日志(slow log) 中,这点和 MySQL 记录慢查询语句类似。
当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。
注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。
之后获取慢查询日志的内容很简单,直接使用SLOWLOG GET
命令即可。命令默认返回最近 10 条的的慢查询命令,你也自己可以指定返回的慢查询命令的数量 SLOWLOG GET N
。
127.0.0.1:6379> SLOWLOG GET #慢日志查询
1) 1) (integer) 5
2) (integer) 1684326682
3) (integer) 12000
4) 1) "KEYS"
2) "*"
5) "172.17.0.1:61152"
6) ""
// ...
慢查询日志中的每个条目都由以下六个值组成:
- 唯一渐进的日志标识符。
- 处理记录命令的 Unix 时间戳。
- 执行所需的时间量,以微秒为单位。
- 组成命令参数的数组。
- 客户端 IP 地址和端口。
- 客户端名称。
其他比较常用的慢查询相关的命令:
# 返回慢查询命令的数量
127.0.0.1:6379> SLOWLOG LEN
(integer) 128
# 清空慢查询命令
127.0.0.1:6379> SLOWLOG RESET
OK
6.Redis内存碎片
①为什么会有redis内存碎片?
①Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间;
②频繁修改redis中的数据也可能产生内存碎片,比如删除redis中的某个数据时,redis通常不会轻易释放内存给操作系统;
②如何查看redis内存碎片的信息?
可以使用info memory查看redis内存相关的信息,其中的mem_fragmentation内存碎片率值越大表示内存碎片越严重;
通常,当内存碎片率>1.5时才需要清理内存碎片;
(mem_fragmentation内存碎片率=used_memory_rss(操作系统实际分配给 Redis 的物理内存空间大小)/used_memory(Redis 内存分配器为了存储数据实际使用的内存空间大小))
③如何清理redis内存碎片?
①redis4.0之后自带内存整理功能,可以通过配置项activedefrag开启;
②清理内存碎片的时机可以通过修改配置项,设置内存碎片占用空间达到多少或是内存碎片率达到多少时进行清理;
③内存清理可能会对redis性能造成影响,可以通过配置项设置内存碎片清理的CPU占用率;
④重启节点可以做到内存碎片重新整理。如果你采用的是高可用架构的 Redis 集群的话,你可以将碎片率过高的主节点转换为从节点,以便进行安全重启;
十二、Redis生产问题
1.缓存穿透
①什么是缓存穿透?
指大量请求的key是不合理的,既不存在于redis缓存中,也不存在于数据库中,导致这些请求全部落到了数据库上,对数据库造成了巨大的压力,甚至导致数据库宕机;
②解决办法
Ⅰ.首先,最基本的是做好参数校验,对于一些不合法的参数请求直接抛出异常信息返回给客户端;
Ⅱ.对无效key进行缓存并设置较短的过期时间
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,并尽量将无效的 key 的过期时间设置短一点比如 1 分钟;
Ⅲ.布隆过滤器
把所有可能存在的请求的值都存放在布隆过滤器中,当请求过来时,先使用布隆过滤器判断是否存在于缓存和数据库中,
若不存在则直接返回请求参数错误信息给客户端,存在的话才会查询缓存或数据库。
需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
布隆过滤器的原理
当一个元素加入布隆过滤器中的时候,会进行下列操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行下列操作:
- 对给定元素再次进行相同的哈希计算;
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
出现误判的原因就是不同的元素得到的哈希值有可能是一样的;
2.缓存击穿
①什么是缓存击穿?
请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中。
这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
②解决办法
Ⅰ.针对热点数据提前预热,将其存入缓存中并设置合理的过期时间;
Ⅱ.请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力;
3.缓存雪崩
①什么是缓存雪崩?
缓存在同一时间大面积的失效,或是缓存服务宕机,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。
②解决办法
针对redis服务不可用的情况:
Ⅰ.使用redis cluster,避免单机出现问题整个缓存服务都没办法使用;
Ⅱ.限流,避免同时处理大量的请求;
针对缓存大面积失效的情况:
Ⅰ.设置不同的失效时间比如随机设置缓存的失效时间;
Ⅱ.设置二级缓存;
③缓存击穿和缓存雪崩有什么区别?
缓存击穿强调的是某个热点数据不存在与缓存中导致的大量请求落到数据库上;
缓存雪崩强调的是大量或所有的数据突然失效导致的大量请求落到数据库上;——一个是点,一个是面
4.哪些情况可能导致redis堵塞?
①一些时间复杂度为O(n)的命令,例如keys *、lrange、hgetall、smembers等;
②创建RDB快照的命令:save;(bgsave则不会)
③AOF:aof日志记录阻塞、aof刷盘阻塞、aof重写阻塞;
④大key:查找大key——--bigkeys、删除大key;
⑤清空数据库;
⑥集群扩容;
⑦swap内存交换;
⑧网络问题;
十三、Redis使用规范
1.生产环境下禁用这些命令:keys *、flushdb、flushall
可以在配置文件中禁用这些命令:
2.不能使用keys *,那用什么遍历redis数据库?——SCAN
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
SCAN 返回一个包含两个元素的数组,
第一个元素是用于进行下一次迭代的新游标, cursor=0表示已遍历完毕;
第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回零表示迭代已结束。
SCAN的遍历顺序
非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。