转载本文请联系程序员jinjunzhu公众号。又到了金银四的跳槽季,不少同学已经开始行动起来了。今天就来帮忙,送出这套redis面试题,帮助大家过关。1为什么redis响应快1.1数据存储在内存中Redis数据存储在内存中,读写操作只需要访问内存,不需要磁盘IO。1.2.底层数据结构redis数据以key:value的格式存储在哈希表中,时间复杂度为o(1)。Redis为值定义了丰富的数据结构,包括状态字符串、双向链表、压缩列表、哈希、跳表和整数数组。可以根据值的特点选择最高效的数据结构。1.3.单线程模型redis的网络IO和数据读写采用单线程模型,可以和CPU绑定,避免了线程上下文切换带来的开销。“注意:Redis6.0引入了网络请求的多线程模型,读写操作仍然采用单线程操作。”Redis多线程网络模型如下图所示:1.4.IO多路复用。Redis采用epoll网络模型,如下图所示:Kernel会一直监听新的socket连接事件和已建立的socket连接的读写事件,将监听到的事件放入事件队列中,redis使用单线程连续处理事件队列。这避免了等待连接和读写事件到达的阻塞。这些事件都绑定了回调函数,回调函数会调用redis的处理函数进行处理。2Redis底层数据结构Redis有5种数据类型,包括“字符串、列表、集合、有序集和字典”。Redis的底层数据结构有6种,包括“动态字符串、双向链表、压缩列表(ziplist)、哈希表、跳表(skiplist)和整型数组”。Redis数据类型与底层数据结构有如下对应关系:2.1.字符串类型的底层数据结构是一个动态字符串。2.2.如果列表同时满足以下条件,则使用压缩列表,否则使用双向链表。列表中单个元素小于64字节列表中元素个数小于512“压缩列表”是内存中连续的内存空间,结构如下:“压缩列表查找时间复杂度为o(n)”2.3。Collection如果同时满足以下条件,则使用有序整数数组,否则使用哈希表。集合中的元素都是整数类型。集合中的元素个数不超过512。2.4.如果有序集同时满足以下两个条件,则使用压缩列表,否则使用跳跃列表。集合中的元素数小于64字节。集合中的元素个数小于128。”注:有序集合还有一张HASH表,用于保存集合中元素的分数。在做ZSCORE操作时,查询的就是这张HASH表,所以效率很高。”“跳表”的结构如下:如果不加索引,数字10需要查找10次,使用二级索引,数字10需要查找5次,而一级索引需要查询3次。.跳表的每一层都是一个有序的链表,最底层存放的是所有的数据。跳表插入、删除、查询的时间复杂度为o(logN)。跳表需要存储额外的索引节点,会增加额外的空间开销。2.5.如果字典同时满足以下两个条件,则使用压缩列表,否则使用哈希表。字典中每个条目的键/值小于64字节。字典中的元素个数小于512。3种redis缓存淘汰策略Redis一共有8种淘汰策略,如下图所示:volatile-lfu和allkeys-lfu策略是4.0版本新增的。“lru”是按照最少最近访问数据的原则淘汰数据。可能的问题是,如果最近访问大量的冷数据,会占用大量的内存空间。如果缓存已满,一些热点数据将被淘汰。失去。“lfu”是按照数据访问频率最小的原则淘汰数据。如果两个数据的访问时间相同,则淘汰访问时间较早的数据。4redis数据持久化redis持久化有两种方式,一种是写后日志(AOF),一种是内存快照(RDB)。4.1.AOF日志AOF日志记录每条接收到的命令。当redis发生故障并从宕机中恢复时,可以加载AOF日志中的命令并进行重放,实现故障恢复。AOF有三种同步策略,如下图所示:如果图片不是对数据丢失特别敏感的业务,建议使用everysec,在主线程上阻塞少,丢失数据只有1秒失败后。4.2.RDB快照RDB快照是内存快照,记录了某个时刻redis的所有数据。4.3.混合日志从redis4.0开始,AOF文件也可以保存RDB快照。当AOF被覆盖时,redis会清除AOF文件的内容,并先记录一个RDB快照。此数据以“REDIS”开头。记录完RDB内容后,AOF文件就会记录AOF命令。故障恢复时,先加载AOF文件中的RDB快照,再回放AOF文件中的后续命令。4.4.主从同步redis主从同步时,主节点会先生成一个RDB快照发送给从节点,并将快照后的命令写入主从同步缓冲区(复制缓冲区)。从节点加载RDB文件后,主节点向从节点发送缓冲命令。4.5.AOF重写AOF日志是附加记录命令的,所以同一个key可能有多个命令,这些命令可以合二为一。例如,可以将同一个key的多个set操作日志合并为一个。4.6.在阻塞点AOF重写和RDB快照执行过程中,redis会fork一个子进程执行操作,子进程执行时是否阻塞主线程。”但是要注意两点:“在fork子进程的过程中,redis主线程会拷贝一份内存页表(记录虚拟内存和物理内存的映射关系)给子进程。这个进程是阻塞的,redis主线程内存越大,阻塞时间越长;子进程和redis主线程共享一块物理内存,如果有新的请求到来,必须使用copyonwrite方法将需要修改的数据页复制到新的内存空间进行修改。如下图所示:注意:如果启用大内存页,则需要为每个副本分配2MB内存。5Redis高可用下图是“一主二从三哨兵”的架构图。从图中可以看出,哨兵之间、哨兵与主从节点之间、哨兵与客户端之间都建立了连接。如果master节点宕机,sentinel集群需要完成主从切换,如下图所示:下面依次说一下“5.1~5.4”这四个步骤。5.1.判断主节点下线当哨兵监测到主节点下线时,会向其他哨兵发送确认命令,其他命令根据自己的判断回复“Y”或“N”。如果超过n/2+1个sentinel认为master节点下线,则判断master节点下线。这里n是哨兵集群的数量。参数n/2+1由quorum参数配置。比如有5个sentinel,这里一般配置为3。它也可以配置为其他值。5.2.选举新的主节点判断主节点下线后,哨兵集群将重新选举新的主节点。5.2.1淘汰不稳定的从节点根据配置参数down-after-milliseconds*10淘汰。“down-after-milliseconds”表示主从节点断开连接的时间,10表示次数。如果从节点与主节点之间的连接断开时间超过down-after-milliseconds10次以上,则从节点被淘汰。5.2.2slave-priority参数“slave-priority”参数配置从节点的优先级。在选择从节点时,Sentinel会优先选择优先级高的从节点。5.2.3复制进度redis有一个记录主从增量复制的缓冲区叫repl_backlog_buffer,它是一个环状结构的缓冲区,如下图:主节点有一个写偏移量master_repl_offset,从节点也有一个偏移slave_repl_offset。slave_repl_offset最接近master_repl_offset的slave节点优先作为新的master节点。因此,优先选择上图中偏移量为114的从节点作为新的主节点。5.2.4当ID号优先级和参数相同时,优先选择ID号小的从节点作为新的主节点。5.3.sentinelleader的选举第一个判断master节点下线的sentinel节点收到其他节点的回复确认master节点下线,然后向其他sentinel发送命令申请成为sentinelleader。“成为leader的条件如下:”获得的选票必须大于quorum值,并且必须获得半数以上的选票。如果集群配置了5个sentinel,quorum值设置为3,其中一个sentinel节点挂了,很有可能会判断master节点下线,但是无法进行切换,因为sentinel无法选举领导者。如果集群中有2个sentinel,其中一个挂掉了,那么这个sentinel的leader一定不能选出来。ThefigurebelowshowstheprocessofSentinel1beingsuccessfullyelectedastheleader:5.4.主节点切换选出新的主节点和Sentinelleader后,Sentinelleader会进行主从切换操作。完成后会做一些“事件通知”:通知其他哨兵新的主节点地址,通知所有从节点新的主节点地址。从节点收到后,会向新的主节点请求主从同步,并通知客户端连接到新的主节点。5.5.Master在slave切换过程中,如果客户端的读请求发送到slave节点,则可以正常处理。写入请求将失败,直到客户端收到新主地址的通知。客户端可以采取一些应急措施来应对主节点下线,比如缓存写请求。为了及时获取新主节点的信息,客户端可以订阅Sentinel的主节点下线事件和新主节点变更事件。6为什么redis变慢redis变慢的原因有很多。总结起来有11个原因,如下图所示:从图中可以看出redis变慢的主要原因有两个:“阻塞主线程和操作系统限制”。6.1主线程阻塞6.1.1.AOF改写和RDB快照前面提到,redis在改写AOF时,主线程会fork一个bgrewriteaof子进程。redis在做RDB快照时,主线程会fork一个bgsave子进程。从表面上看,这两个操作并没有阻塞主线程,但是fork子进程的过程是在主线程中完成的。fork子进程时,redis需要拷贝内存页表。如果redis实例很大,这个副本会消耗很多CPU资源,阻塞主线程的时间也会更长。6.1.2.大内存页Redis默认支持2MB的大内存页。使用大内存页可以在一定程度上减少redis的内存分配次数,但是会对数据持久化造成一定的影响。RedisAOF改写和RDB快照过程中,如果主线程收到新的写请求,需要CopyOnWrite。使用大内存页,即使redis只修改其中一个1kb大小的key,也需要拷贝一整页数据,即2MB。当写入量很大时,大量的副本会导致redis性能下降。6.1.3.命令复杂度高执行复杂度高的命令是redis阻塞的常见原因。例如,要对集合或列表数据类型执行SORT操作,复杂度为O(N+M*log(M))。6.1.4.bigkey操作如果一个key的值很大,创建时分配内存会很耗时,删除时释放内存也很耗时。redis4.0之后引入了layfree机制,可以使用子进程异步删除,不至于影响主线程的执行。使用UNLINK命令代替DEL命令,可以使用子进程异步删除。redis6.0增加配置项lazyfree-lazy-user-del,配置yes后,del命令也可以通过subprocess异步删除。如果lazyfree-lazy-user-del没有设置为yes,redis是否使用异步删除取决于删除的时机。对于String类型和使用整型数组和压缩列表的底层数据类型,redis不会使用异步删除。6.1.5.从节点全量同步从节点全量同步过程中,需要先清除内存中的数据,然后再加载RDB文件。此过程被阻止。如果有读请求,只能等到RDB文件加载完成。只能处理请求,所以响应会很慢。另外,如果redis实例很大,RDB文件会很大,从库加载的时间会很长。所以尽量让redis实例不要太大。比如单实例限4G。如果超过,则使用切片簇。6.1.6.AOF同步写盘有3种appendfsync策略:always、everysec、no。如果总是使用,每个命令将同步写入磁盘。这个过程是阻塞的,只有写盘成功后才能处理下一条命令。除非是严格不允许数据丢失的场景,否则尽量不要选择always策略。建议尽量选择everysec策略。如果您对数据丢失不敏感,可以使用no。6.1.7.当内存达到maxmemory,内存达到maxmemory时,需要使用淘汰策略淘汰一些key。即使使用lazyfree异步删除,也会阻塞选择key的过程。可以选择更快的淘汰策略,比如用随机淘汰代替LRU和LFU算法淘汰。也可以通过扩展分片的数量来减少key淘汰的时间消耗。6.2操作系统限制6.2.1。使用swap使用swap的原因是操作系统无法为redis分配足够的内存。如果其他操作开启了swap,内存数据需要不断的用swap换入换出。性能影响是巨大的。操作系统无法分配内存也可能是由其他进程使用大量内存引起的。6.2.2.网络问题如果网卡负载大,对redis性能影响很大。一方面有可能是redis的访问量真的很大,另一方面也有可能是其他流量大的程序占用了带宽。这最好从操作和维护级别进行监控。6.2.3.线程上下文切换虽然redis是单线程的,但是在多核CPU的情况下也可能会发生上下文切换。如果主线程从一个物理内核切换到另一个物理内核,则无法使用CPU的高效L1和L2缓存。如下图所示:为了防止这种情况,redis可以绑定一个CPU物理核。6.2.4.磁盘性能低对于AOF同步写入磁盘的使用场景,如果磁盘性能低,也会影响redis的响应。性能较好的SSD硬盘可以优先考虑。7设计排行榜功能redis的zset类型保存了score值,可以方便的实现排行榜的功能。比如要统计10篇文章的排名,可以先创建一个存储10篇文章的zset。每当读者阅读一篇文章时,使用ZINCRBY命令对这篇文章的评分加1,最后使用range命令统计排名靠前的文章。8redis实现了分布式锁8.1。redis单节点的分布式锁如下图所示。一项服务部署两个客户端。获取分布式锁时,一个成功,一个失败。Redis一般使用setnx来实现分布式锁。命令如下:SETNXKEY_NAMEVALUE设置成功返回1,设置失败返回0。使用单节点分布式锁存在一些问题。8.1.1.client1获得锁后,如果发生故障,则无法释放锁,其他client将永远无法获得锁。解决方法是使用如下命令设置key的过期时间:SETkeyvalue[EXseconds][PXmilliseconds]NX8.1.2client2accidentallydeletedthelock解决方法是在设置key的value的时候添加一个clientrepresentation,对于例如,在client1上设置key,在value前拼接一个stringapplication1,删除时做判断。8.2.Redis红锁Redis单节点会存在可靠性问题,节点故障后锁操作会失败。为了处理单点故障问题,redis设计了多节点分布式锁,也叫红锁。主要思想是客户端向多个redis实例请求锁,只有超过半数的实例都成功加锁,才算分布式锁成功。如下图,客户端分别用3个实例请求锁,有2个实例成功加锁,所以分布式锁获取成功:9CacheAvalanche,Breakdown,Penetration9.1。大量的缓存数据失效,大量的客户端请求会发送到数据库,导致数据库压力激增。如下图所示:“主要有3种处理方式:”在设置key的过期时间的时候加入一个小的随机数。限流服务降级9.2。缓存分解一个hotkey,突然过期,大量请求发往数据库。解决方案是不为热点密钥设置过期时间。9.3.缓存穿透一个hotkey,查询缓存和查询数据库都不存在,发生缓存穿透。如下图所示:“主要有两种处理方式:”缓存热点的空值和默认值查询数据库前查询布隆过滤器10数据倾斜什么是数据倾斜?看下面这个面试题:如果redis有个hotspotkey,qps可以达到100w,如何存储?如果这个热点key放在一个redis实例上,这个实例面临的访问压力会非常大。如下图,redis3实例保存的是hotkeyfoo,访问压力会很大:“主要有两种解决方案:”1.使用客户端本地缓存来缓存key,所以会有两种本次改造存在的问题:Clientcache热点key可能会消耗大量内存。客户端需要保证本地缓存和redis缓存的一致性。2.给热点key加上一个随机前缀,让它保存在不同的redis实例上。这样也会造成两个问题:访问的时候,需要给key加上前缀。删除时,客户端需要根据所有前缀删除保存在不同实例上的key。有一道经典的面试题,如何对内存中的10亿个整数重新排序?我们先计算一下10亿个整数占用的内存。java中一个整数类型占用四个字节,占用内存大小约为10亿*4/1024/1024=3.7G。占用内存太大。内存不够用怎么办?11.1.位图介绍位图类型使用的数据结构是String,底层存储格式是二进制位数组。假设我们有1、4、6、9四个数,存储在如下图所示的位数组中:在这个位数组中,10位空间用来存储四个整数,占用的空间为很小。回到面试题,我们用“(最大值-最小值+1)”在位数组的长度是10亿个整数。如果有负数,则需要进行转换,将最小负数的绝对值与所有数字相加。例如{-2,0,1,3},我们将其转换为{0,2,3,5},因为数组下标必须从011.2开始。使用场景11.2.1。员工签到记录以100人为一个某公司想统计一个月内的全职员工人数,可以每天创建一张位图,签到员工的位位置为1。统计当天签到的员工,只需要使用BITCOUNT命令即可。统计当月的全职员工,只需要对当月每一天的位图进行交集操作即可。命令如下:BITOPANDsrckey1srckey2srckey3...srckey30srckeyN表示第N天位图11.2.2的签到记录。网站日活跃用户统计,所以我们创建一个位图,长度为100000,每个用户ID占用一个位,如果用户登录,将位位置设置为1,一天结束时,使用BITCOUNT命令统计当天登录的用户总数。
