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