之前写过几篇关于Redis的文章Redis缓存穿透、击穿、雪崩、数据库和缓存一致性说说Redis的五种数据结构及其现实应用场景文章让你了解Redis的持久化(RDB、AOF)是如何实现Redis的高可用的吗?(master-slave,sentinel,cluster)分开写是因为这几块的内容比较多,比较重要,所以想写的详细深入,让大家在理解的基础上记住。忘记是不容易的。因此,以上相关的面试题不再单独整理,具体可以阅读相关文章。什么是Redis?为什么Redis这么快?为什么Redis6.0之后改成多线程?你知道Redis的过期策略吗?说说Redis的内存淘汰策略?Redis的事务机制是什么?说说Redishashslot的概念?为什么RedisCluster设计成16384个槽位?Redis在集群中查找key时是如何定位到具体节点的?Redis底层使用什么协议?RedisHash冲突怎么办?Redis相对于memcached有什么优势?减少Redis内存使用的方法有哪些?Redis获取海量数据的正确方式?如何使用Redis以获得更高的性能?如何解决Redis的并发争key问题?你用过Redis作为异步队列吗?你是怎么用的?你有没有用Redis做延迟队列?应该如何实施?如何使用Redis统计网站的UV?什么是热键问题,如何解决热键问题?1、什么是redis?Redis是一个用C语言开发的开源高性能键值对(key-value)内存数据库。它是一个NoSQL(泛指非关系型)数据库。与MySQL数据库不同,Redis数据存储在内存中。其读写速度非常快,支持并发10WQPS每秒。因此,Redis在缓存方面被广泛使用。此外,Redis还常用于分布式锁和消息中间件。此外,Redis还支持事务、持久化、LUA脚本和多集群方案。2、为什么Redis这么快?1)完全基于内存存储实现完全基于内存,大部分请求都是纯内存操作,速度非常快。数据存在于内存中,类似于HashMap,HashMap的优点是查找和运算的时间复杂度为O(1);2)合理的数据编码Redis支持多种数据类型,每个基本类型都可以用于多种数据编码。什么时候,用什么数据类型,用什么编码,都是redis设计者总结和优化的结果。3)单线程模型Redis是单线程模型,单线程避免了CPU不必要的上下文切换和竞争锁的消耗。也因为是单线程的,如果一个命令执行时间过长(比如keys,hgetall命令),会造成队列阻塞。Redis6.0引入了多线程提速,其执行命令操作内存依然是单线程。4)合理的线程模型采用多通道I/O复用模型,非阻塞IO;多I/O多路复用技术可以让单个线程高效处理多个连接请求,Redis使用epoll作为I/O多通道多路复用技术的实现。而且Redis自带的事件处理模型将epoll中的连接、读写、关闭都转化为事件,以免在网络I/O上浪费太多时间。3、为什么Redis6.0以后改成多线程了?在Redis6.0之前,Redis在处理客户端请求时,包括读取socket、解析、执行、写入socket等,都是由一个串行的主线程来处理的,也就是所谓的“单线程”。redis使用多线程并不意味着它完全抛弃了单线程。Redis仍然采用单线程模型处理客户端请求,但是采用多线程处理数据读写和协议解析,使用单线程执行命令。这样做的目的是因为redis的性能瓶颈在于网络IO而不是CPU。使用多线程可以提高IO读写的效率,从而提高redis的整体性能。4、你知道Redis的过期策略吗?我们在设置key的时候,可以给它设置一个过期时间,比如expirekey60。指定key在60s后过期,60s后redis怎么处理呢?先介绍几种过期策略:定时过期。每个有过期时间的key都需要创建一个定时器,当达到过期时间后key会被立即清除。该策略可以立即清除过期数据,对内存友好;但是处理过期数据会占用大量的CPU资源,从而影响缓存的响应时间和吞吐量。惰性过期只有在访问一个key的时候,才会判断这个key是否过期,过期了就清空。这种策略可以最大程度的节省CPU资源,但是对内存很不友好。在极端情况下,大量过期的key可能不会被再次访问,因此不会被清除,占用大量内存。定时过期每隔一定时间扫描一定数量数据库的expires字典中一定数量的key,清除过期的key。该策略是前两者的折衷方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下优化平衡CPU和内存资源。Redis中同时使用了惰性过期和周期性过期两种过期策略。假设Redis目前存储了30万个key,并且都设置了过期时间,如果每100ms检查一次所有的key,CPU负载会非常高,最终可能会挂掉。所以redis采用周期性过期,每隔100ms随机选取一定数量的key进行校验删除。但是,最后可能还有很多过期的key没有被删除。这时候redis采用的是惰性删除。当你拿到一个key时,redis会检查它。如果key设置了过期时间,已经过期,此时会被删除。但是,如果常规删除错过了许多过期键,则不会执行延迟删除。内存中会积累大量的过期key,直接导致内存爆炸。或者有的时候,业务量大了,redis的key被大量使用,内存直接不足。这时候就需要内存淘汰策略来保护自己。5、说说Redis的内存淘汰策略?Redis有8种内存淘汰策略。volatile-lru:当内存不足以容纳新写入的数据时,使用LRU(leastrecentlyused)算法从设置过期时间的key中淘汰;allkeys-lru:当内存不足以容纳新写入的数据时,使用LRU(leastrecentlyused)算法淘汰所有keys;volatile-lfu:4.0版本新增,当内存不足以容纳新写入的数据时,使用LFU算法删除过期key中的key;allkeys-lfu:4.0版本新增,当内存不足以容纳新写入的数据时,使用LFU算法淘汰所有key;volatile-random:当内存不足以容纳新写入的数据时,使用设置了过期时间的key,随机淘汰数据;allkeys-random:当内存不足以容纳新写入的数据时,随机淘汰所有key的数据;volatile-ttl:当内存不足以容纳新写入的数据时,设置key的过期时间其中,根据过期时间进行淘汰,过期早的优先;noeviction:默认策略,当内存不足以容纳新写入的数据时,新的写操作会报错;6.说说Redis的事务机制?Redis使用MULTI、EXEC、WATCH和一组命令来实现事务机制。事务支持一次执行多条命令,一个事务中的所有命令都会被序列化。在事务执行过程中,队列中的命令会按顺序序列化,其他客户端提交的命令请求不会插入到事务执行命令序列中。简而言之,Redis事务是队列中一系列命令的顺序、一次性、独占执行。Redis执行事务的流程如下:启动事务(MULTI)命令入队执行事务(EXEC),取消事务(DISCARD)命令说明EXEC执行事务块中的所有命令DISCARD取消事务,放弃所有命令的执行在事务块的MULTI标记的一个事务块的开头,UNWATCH通过WATCH命令取消对所有key的监听。WATCH监视密钥。如果在事务执行之前key被其他命令改变,事务将被中断。redis事务需要注意的是:1)与mysql中的事务不同,当redis事务遇到执行错误时,不会回滚,只是简单放过,并保证其他命令正常执行(所以redis事务是不保证是原子的)。2)事务执行过程中,如果redis意外挂掉。不幸的是,只执行了部分命令,其余的都被丢弃了。7、说说Redishashslot的概念?Redis集群没有使用一致性哈希,而是引入了哈希槽的概念。Redis集群有16384个哈希槽。每个key通过CRC16校验后,取16384取模,决定放置哪个slot。集群的每个节点负责一些哈希槽。使用哈希槽的好处是可以很容易地添加或删除节点。这种结构无论是对某个节点进行增删改查,都不会导致集群不可用。当需要添加节点时,只需要将其他节点的部分哈希槽移动到新节点即可;当需要移除节点时,只需要将被移除节点上的哈希槽移动到其他节点即可;至此,我们以后添加或删除节点时,不需要先停止所有的redis服务。8、为什么RedisCluster设计成16384槽?2的14次方是16384,当然这并不是说一定要设计成16384槽。作者也对此进行了解释。地址如下:https://github.com/antirez/re...1)如果slot是65536,发送的心跳报文头是8k,发送的心跳包过大。上面提到消息头中,当slot为65536时,这个块的大小为:65536÷8÷1024=8kb因为每一秒,redis节点需要发送一定数量的ping消息作为心跳包,如果theslot是65536,这个ping报文的报文头太大,浪费带宽。2)redisclustermaster节点的数量基本不可能超过1000个,如前所述,集群节点越多,心跳包的消息体中携带的数据就越多。如果超过1000个节点,也会造成网络拥塞。所以redis的作者不建议redis集群节点数超过1000个。那么,对于节点数小于1000的redis集群,16384个槽就够了。不需要扩展到65536。3)槽越小,节点越少,压缩率越高。在Redis主节点的配置信息中,其负责的哈希槽以位图的形式保存。在传输过程中,位图进行了压缩,但是如果位图的填充率slots/N很高(N代表节点数),位图的压缩率很低。如果节点数少,哈希槽数多,则位图的压缩率很低。9、Redis在集群中搜索key时,如何定位到具体的节点?使用CRC16算法对key进行哈希,然后将哈希值取到16384,得到具体的slot。根据节点和slot的映射信息(与集群建立连接后,client可以获得slot映射信息),找到具体的slot。节点地址到具体节点去寻找key。如果key不在这个节点上,redis集群会返回moved的命令,并将新的节点地址添加到客户端。同时,客户端会刷新本地节点槽位映射关系。如果slot正在迁移,redis集群会返回asking命令给客户端。这是临时修正,客户端不会刷新本地节点槽位映射关系。10、Redis底层使用什么协议?RESP的英文全称是RedisSerializationProtocol,是专门为redis设计的一套序列化协议。这个协议其实在redis1.2版本就出现了,但是直到redis2.0才最终成为redis通信协议的标准。RESP主要具有实现简单、解析速度快、可读性好等优点。11、如何处理Redis中的Hash冲突Redis中的Hash与Java中的HashMap比较类似,都是数组+链表的结构。当发生哈希冲突时,元素将被添加到链表中。Redis中hash的内部结构也是一样的:第一维是数组,第二维是链表,组成一个全局的哈希表。在Java中,HashMap扩容是一个非常耗时的操作。需要申请一个新的数组。扩容的代价并不低,因为需要遍历一个时间复杂度为O(n)的数组,并对其中的每一个entrty进行hash。计算。添加到新数组中。为了追求高性能,Redis采用了渐进式rehash策略。这也是哈希最重要的部分。redis在扩容时执行rehash策略时,会保留两个全局哈希表,旧的和新的,同时查询这两个。全局哈希表,Redis会将旧的全局哈希表的内容一点一点的迁移到新的全局哈希表中,当迁移完成后,会用新的全局哈希表替换掉之前的。当全局哈希表的最后一个元素被移除时,数据结构将被删除。一般情况下,当全局哈希表的元素个数等于数组的长度时,就会开始扩容,扩容后的新数组是原数组大小的两倍。Redis如果在做bgsave(持久化),可能不会扩容,因为需要减少内存页的过度分离(CopyOnWrite)。但是如果全局哈希表很满,元素个数达到数组长度的5倍时,Redis会强制扩容。12、Redis与memcached相比有什么优势?Memcached的所有值都是简单的字符串。Redis作为其替代者,支持更丰富的数据类型。Redis比Memcached快得多。Redis可以持久化它的数据。13.Redis的内存占用有哪些?当你的业务应用在Redis中存储的数据很少时,你可能不太关心内存资源的使用情况。但是随着你业务的发展,你的业务会在Redis中存储越来越多的数据。那么在使用Redis的时候可以做些什么来节省内存呢?这里有4条建议:1)控制key的长度最简单直接的内存优化就是控制key的长度。开发业务时,需要提前预估整个Redis写入的key数量。如果key的数量达到了百万级别,那么太长的key名也会占用过多的内存空间。因此,需要保证key简单明了,定义的key越短越好。比如原来的key是user123,可以优化成u:bk:123。这样你的Redis可以节省很多内存,这个方案对内存的优化非常直接高效。2)避免存储bigkeybigkey,也就是说这个key的值非常大。除了控制key的长度,还需要注意value的大小。如果存储大量的bigkey,也会导致Redis内存增长过快。此外,客户端在读写bigkeys时也存在性能问题。因此,您应该避免将bigkeys存储在Redis中。一般的建议是:String:大小保持在10KB以下List/Hash/Set/ZSet:元素个数保持在10000以下3)尽量设置过期时间。Redis数据存储在内存中,这也意味着它的资源是有限的。当您使用Redis时,请将其用作缓存,而不是数据库。所以,你的应用程序写入Redis的数据,尽量设置一个过期时间。采用这种方案,Redis中可以只保留经常访问的热点数据,内存利用率会比较高。4)实例设置maxmemory+淘汰策略虽然你的Rediskey设置了过期时间,但是如果你的业务应用写的比较多,过期时间设置的时间比较长,Redis的内存还是会在短时间内快速增长.如果不控制Redis的内存上限,也会导致内存资源的使用过多。对于这种场景,需要提前预估业务数据量,然后为这个实例设置maxmemory,控制实例内存的上限,避免Redis内存不断膨胀。配置maxmemory后,此时需要设置数据淘汰策略,如何选择淘汰策略需要根据自己的业务特点来决定。14、在Redis中获取海量数据的正确操作方法是什么?有时需要从Redis实例中数以万计的key中找出特定前缀的key列表来手动处理数据,可能是修改其值或删除key。这里有一个问题,如何从大量的key中找出满足特定前缀的key列表呢?例如,我们的用户令牌缓存使用[user_token:userid]格式的键来保存用户令牌的值。这个时候我们想看看有多少用户在线。在Redis2.8之前,我们可以使用keys命令,根据正则匹配得到我们需要的key。Redis提供了一个简单暴力的指令keys来列出所有满足特定正则字符串规则的key。keysuser_token*但是这个命令有一些缺点:没有offset和limit参数,一次性吐出所有满足条件的key。万一实例中有几百个w键满足条件,当你看到满屏的字符串,没有完的时候,就知道难了。keys算法是一种复杂度为O(n)的遍历算法。如果实例中有千万级的key,这条指令会导致Redis服务卡死,其他所有读写Redis的指令都会延迟甚至超时。报错,因为Redis是一个单线程程序,所有的指令都是顺序执行的,其他指令必须等到当前keys指令执行完才能继续执行。建议生产环境屏蔽keys命令。在满足要求和导致Redis卡死之间如何取舍?面对这种困境,Redis在2.8版本中为我们提供了一个解决方案——扫描命令。与keys命令相比,scan命令有两个明显的优势:虽然scan命令的时间复杂度也是O(N),但是它是分批执行的,不会阻塞线程。scan命令提供了limit参数,可以控制每次返回结果的最大数量。这两个优点帮助我们解决了上面的问题,但是scan命令并不完美,它返回的结果可能会重复,所以客户端去重非常重要。15.如何使用Redis以获得更高的性能?1)master关闭持久化。一般我们在生产中采用的持久化策略是master关闭持久化,slave开启RDB。必要的时候,AOF和RDB都开启。2)不要使用复杂度高的命令。Redis使用单线程模型来处理请求。当执行复杂度高的命令时,会消耗较多的CPU资源。主线程中的其他请求只能等待。发生排队延迟。所以需要避免执行sort、sinter、sinterstore、zunionstore、zinterstore等聚合命令,这种聚合操作建议放在客户端执行,不要让Redis承担太多计算工作。3)执行O(N)条命令时,注意N的大小,避免使用复杂度过高的命令,能高枕无忧吗?答案是否定的。当你在执行O(N)条命令时,还需要注意N的大小。就像上面提到的使用keys命令一样,如果一次查询的数据太多,在网络传输过程中会耗时过长过程,操作延迟会变大。所以,对于容器类型(List/Hash/Set/ZSet),当元素个数未知时,一定不要无脑执行LRANGEkey0-1/HGETALL/SMEMBERS/ZRANGEkey0-1。查询数据时应遵循以下原则:先查询数据元素个数(LLEN/HLEN/SCARD/ZCARD)且元素个数少的,一次查询全量数据即可。/SSCAN/ZSCAN)4)批命令代替单个命令当需要同时操作多个按键时,应该使用批命令来处理。与多次单次操作相比,批量操作的优势在于可以显着减少客户端和服务端之间的往返网络IO次数。所以我给你的建议是:String/Hash使用MGET/MSET代替GET/SET,HMGET/HMSET代替HGET/HSET其他数据类型使用Pipeline,打包发送多个命令到服务器执行5)避免集中过期keyRedis清除过期键采用定时+懒惰的方式,这个过程在主线程中执行。如果你的业务有大量key过期,Redis在清理过期key的时候也可能存在阻塞主线程的风险。为了避免这种情况的发生,可以在设置过期时间的时候加入一个随机时间来分散这些key的过期时间,从而减少集中过期对主线程的影响。6)只使用db0虽然Redis提供了16个dbs,但是我只推荐你使用db0。为什么?我总结了以下三个原因:在一个连接上操作多个db数据时,每次都需要先执行SELECT,这会给Redis带来额外的压力。使用多个db的目的是为了根据不同的业务线存储数据,那么为什么不拆分多个实例存储呢?拆分部署多个实例,多个业务线不会互相影响,也可以提高Redis的访问性能。Redis集群只支持db0。如果以后要迁移到RedisCluster,迁移成本会很高。16、如何解决RedisKey的并发竞争问题这也是网上很常见的一个问题,就是多个client同时写入一个key,本该先到的数据可能会晚到,导致数据版本错误;或者多个client同时获取一个key,修改value之后再写回去,只要顺序不对,数据就会出错。推荐一个方案:分布式锁(zookeeper和redis都可以实现分布式锁)。(如果没有Redis并发争用key问题,不要使用分布式锁,会影响性能)17.你用过Redis做异步队列吗?你是怎么用的?一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,需要sleep一会再试。如果你不想睡觉怎么办?该列表还有一个名为blpop的指令。当没有消息时,它会阻塞,直到消息到达。但是如果是这样的话,你发现redis作为消息队列并不安全,不能重复消费,一旦消费就删除。同时作为消费者确认ACK比较麻烦,所以redis中的消息队列在实际开发中很少用到,因为现在已经有了Kafka、RabbitMQ等成熟的消息队列,功能也比较完善。18.使用Redis作为延迟队列,应该如何实现?延迟队列可以使用zset(有序列表)来实现。我们将消息序列化为一个字符串作为列表的值。以这条消息的过期处理时间作为分值,然后使用定时器定时扫描。一旦执行时间小于或等于当前时间的任务立即执行。19、如何使用Redis统计网站的UV?UV不同于PV,UV需要去重。一般有两种选择:1.使用BitMap。存储用户的uid,计算UV的时候直接做bitcount即可。2.使用布隆过滤器。将每次访问的用户uid放入布隆过滤器。优点是节省内存,缺点是无法获取准确的UV。但是对于不需要准确知道具体UV,只需要一个大概数量级的场景来说,是一个不错的选择。20、什么是热键问题,如何解决热键问题?所谓hotkey问题就是突然有几十万个请求去访问redis上的某个特定的key。那么,这样就会导致流量过于集中,达到物理网卡的上限,从而导致redis服务器宕机。热键是如何生成的?主要有两个原因:用户消费的数据远大于产生的数据,比如秒杀、热点新闻等场景,读多写少。集中请求分片超过了单个Redi服务器的性能。比如定名key和Hash落入同一个服务器,瞬时访问量极大,超出机器瓶颈,导致hotkey问题。那么在日常开发中,如何识别热键呢?根据经验判断哪些是热键;客户统计报告;服务代理层上报如何解决热键问题?Redis集群扩容:增加分片副本,平衡读流量;将热键分发到不同的服务器;使用二级缓存,即JVM本地缓存,减少Redis的读请求。参考[1]Redis最佳实践指南:https://mp.weixin.qq.com/s/Fz...[2]Redis经典面试题:https://mp.weixin.qq.com/s/fB...[3]Redis面试20题:http://www.gameboys.cn/articl...关注公众号:后端元界。持续输出优质好文
