有关Redis之前有单独写过几篇文章
之所以单独写,是因为这几块内容比较大,而且也很重要,所以想写的详细点、深入点,让大家在理解的基础上记住它们,这让就不容易忘记。
所以有关以上相关的面试题就不再单独整理了,具体可以看相关文章。
- redis是什么?
- Redis为什么这么快?
- 为什么Redis 6.0之后改多线程呢?
- 你了解Redis的过期策略吗?
- 聊聊Redis内存淘汰策略?
- Redis事务机制是怎样的?
- 说说Redis哈希槽的概念?
- 为什么Redis Cluster会设计成16384个槽呢?
- Redis在集群中查找key的时候,是怎么定位到具体节点的?
- Redis底层使用的什么协议?
- Redis的Hash冲突怎么办?
- Redis相比memcached有哪些优势?
- 有哪些办法可以降低 Redis 的内存使用情况呢?
- Redis中获取海量数据的正确操作方式?
- 如何使用Redis的性能更高?
- 如何解决Redis的并发竞争Key问题?
- 使用过Redis做异步队列么,你是怎么用的?
- 用Redis做过延时队列吗? 具体应该怎么实现?
- 使用Redis统计网站的UV,应该怎么做?
- 什么是热Key问题,如何解决热key问题?
1、redis是什么
Redis是C语言开发的一个开源的高性能键值对(key-value)的内存数据库,它是一种NoSQL(泛指非关系型)的数据库。
与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒支持并发10W QPS。因此Redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁,也可以用来做消息中间件等。
除此之外,Redis还支持事务、持久化、LUA脚本、多种集群方案。
2、Redis为什么这么快?
1) 完全基于内存存储实现
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2) 合理的数据编码
Redis 支持多种数据数据类型,每种基本类型可能对多种数据编码。什么时候,使用什么样数据类型,使用什么样编码,是redis设计者总结优化的结果。
3) 单线程模型
Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如keys,hgetall命令),会造成排队阻塞。
Redis 6.0 引入了多线程提速,它的执行命令操作内存的仍然是个单线程的。
4) 合理的线程模型
使用多路I/O复用模型,非阻塞IO;
多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
3、为什么Redis 6.0 之后改多线程呢?
Redis6.0之前,Redis在处理客户端的请求时,包括读socket、解析、执行、写socket等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。
redis使用多线程并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。
这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。
4、你了解Redis的过期策略吗?
我们在set key的时候,可以给它设置一个过期时间,比如expire key 60。指定这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,然后也没走惰性删除。就会有很多过期key积在内存中,直接会导致内存爆的。或者有些时候,业务量大起来了,redis的key被大量使用,内存直接不够了,
这个时候就需要内存淘汰策略来保护自己了。
5、聊聊Redis内存淘汰策略?
redis有8种内存淘汰策略。
- volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
- allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰;
- volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key;
- allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
- 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 ,如果在事务执行之前,该key 被其他命令所改动,那么事务将被打断。 |
有关redis事务需要注意的就是
1)与mysql中事务不同,在redis事务遇到执行错误的时候,不会进行回滚,而是简单的放过了,并保证其他的命令正常执行(所以说redis的事务并不是保证原子性)。
2)当事务的执行过程中,如果redis意外的挂了。很遗憾只有部分命令执行了,后面的也就被丢弃了。
7、说说Redis哈希槽的概念?
Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有16384
个哈希槽,每个 key 通过 CRC16
校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
使用哈希槽的好处就在于可以方便的添加或移除节点。这种结构无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。
当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;
在这一点上,我们以后新增或移除节点的时候不用先停掉所有的 redis 服务。
8、为什么RedisCluster会设计成16384个槽呢?
2的14次方就是16384,这个当然不说一定要设计成16384个槽,作者对这个也做了解释。
地址如下: https://github.com/antirez/redis/issues/2576
1) 如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
如上所述,在消息头中,当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb 因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
2) redis的集群主节点数量基本不可能超过1000个。
如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
3) 槽位越小,节点少的情况下,压缩率高
Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。
如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
9、Redis在集群中查找key的时候,是怎么定位到具体节点的?
使用CRC16算法对key进行hash,再将hash值对16384取模,得到具体的槽位根据节点和槽位的映射信息(与集群建立连接后,客户端可以取得槽位映射信息),找到具体的节点地址 去具体的节点找key如果key不在这个节点上,则redis集群会返回moved指令,加上新的节点地址给客户端。
同时,客户端会刷新本地的节点槽位映射关系如果槽位正在迁移中,那么redis集群会返回asking指令给客户端,这是临时纠正,客户端不会刷新本地的节点槽位映射关系
10、Redis底层,使用的什么协议?
RESP
英文全称是Redis Serialization Protocol,它是专门为redis设计的一套序列化协议. 这个协议其实在redis的1.2版本时就已经出现了,但是到了redis2.0才最终成为redis通讯协议的标准。
RESP主要有实现简单、解析速度快、可读性好等优点。
11、Redis的Hash冲突怎么办
Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构,当发生 hash 发生碰撞时将会把元素追加到链表上。
在Redis中hash的内部结构也是一样的: 第一维是数组,第二维是链表.组成一个 全局哈希表
。
在 Java 中 HashMap 扩容是个很耗时的操作,需要去申请新的数组,扩容的成本并不低,因为需要遍历一个时间复杂度为O(n)的数组,并且为其中的每个enrty进行hash计算。加入到新数组中。
为了追求高性能,Redis 采用了渐进式 rehash
策略.这也是 hash 中最重要的部分.
redis在扩容的时候执行 rehash 策略会保留新旧两个 两个全局哈希表,查询时也会同时查询两个全局哈希表 ,Redis会将旧 全局哈希表 中的内容一点一点的迁移到新的 全局哈希表 中,当迁移完成时,就会用新的 全局哈希表 取代之前的。当 全局哈希表 移除了最后一个元素之后,这个数据结构将会被删除.
正常情况下,当 全局哈希表 中元素的个数等于数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。
如果 Redis 正在做 bgsave(持久化) 时,可能不会去扩容,因为要减少内存页的过多分离(Copy On Write).但是如果 全局哈希表 已经非常满了,元素的个数达到了数组长度的 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 为 user:book:123,则可以优化为 u:bk:123。
这样一来,你的 Redis 就可以节省大量的内存,这个方案对内存的优化非常直接和高效。
2) 避免存储 bigkey
bigkey,意思就是这个key的value值很大。除了控制 key 的长度之外,你同样需要关注 value 的大小,如果大量存储 bigkey,也会导致 Redis 内存增长过快。
除此之外,客户端在读写 bigkey 时,还有产生性能问题。
所以,你要避免在 Redis 中存储 bigkey,一般建议是:
- String:大小控制在 10KB 以下
- List/Hash/Set/ZSet:元素数量控制在 1 万以下
3) 尽可能地都设置过期时间
Redis 数据存储在内存中,这也意味着其资源是有限的。你在使用 Redis 时,要把它当做缓存来使用,而不是数据库。
所以,你的应用写入到 Redis中的数据,尽可能地都设置 过期时间。采用这种方案,可以让 Redis 中只保留经常访问的 热数据,内存利用率也会比较高。
4) 实例设置 maxmemory + 淘汰策略
虽然你的 Redis key 都设置了过期时间,但如果你的业务应用写入量很大,并且过期时间设置得比较久,那么短期间内 Redis 的内存依旧会快速增长。
如果不控制 Redis 的内存上限,也会导致使用过多的内存资源。
对于这种场景,你需要提前预估业务数据量,然后给这个实例设置 maxmemory 控制实例的内存上限,这样可以避免 Redis 的内存持续膨胀。
配置了 maxmemory,此时你还要设置数据淘汰策略,而淘汰策略如何选择,你需要结合你的业务特点来决定。
14、Redis中获取海量数据的正确操作方式?
有时候需要从Redis实例成千上万的key中找出特定前缀的key
列表来手动处理数据,可能是修改它的值,也可能是删除 key。这里就有一个问题,如何从海量的 key 中找出满足特定前缀的 key 列表来?
比如我们的用户token缓存是采用了【user_token:userid】格式的key,保存用户的token的值。这时候我们想看下有多少用户在线。
在Redis2.8版本之前,我们可以使用keys命令按照正则匹配得到我们需要的key。Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key。
keys user_token*
但是这个命令有一些缺点:
- 没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个 key 满足条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了。
- keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,
- 所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。
- 建议生产环境屏蔽keys命令
在满足需求和存在造成Redis卡顿之间究竟要如何选择呢?面对这个两难的抉择,Redis在2.8版本给我们提供了解决办法——scan命令
。
相比于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),在元素数量未知的情况下,一定不要无脑执行 LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1。
在查询数据时,你要遵循以下原则:
- 先查询数据元素的数量(LLEN/HLEN/SCARD/ZCARD)
- 元素数量较少,可一次性查询全量数据
- 元素数量非常多,分批查询数据(LRANGE/HASCAN/SSCAN/ZSCAN)
4) 批量命令代替单个命令
当你需要一次性操作多个 key 时,你应该使用批量命令来处理。
批量操作相比于多次单个操作的优势在于,可以显著减少客户端、服务端的来回网络 IO 次数。
所以我给你的建议是:
String / Hash 使用 MGET/MSET 替代 GET/SET,HMGET/HMSET 替代 HGET/HSET
其它数据类型使用 Pipeline,打包一次性发送多个命令到服务端执行
5) 避免集中过期 key
Redis 清理过期 key 是采用定时 + 懒惰的方式来做的,而且这个过程都是在主线程中执行。
如果你的业务存在大量 key 集中过期的情况,那么 Redis 在清理过期 key 时,也会有阻塞主线程的风险。
想要避免这种情况发生,你可以在设置过期时间时,增加一个随机时间,把这些 key 的过期时间打散,从而降低集中过期对主线程的影响。
6) 只使用 db0
尽管 Redis 提供了 16 个 db,但我只建议你使用 db0。
为什么呢?我总结了以下 3 点原因:
- 在一个连接上操作多个 db 数据时,每次都需要先执行 SELECT,这会给 Redis 带来额外的压力
- 使用多个 db 的目的是,按不同业务线存储数据,那为何不拆分多个实例存储呢?拆分多个实例部署,多个业务线不会互相影响,还能提高 Redis 的访问性能
- Redis Cluster 只支持 db0,如果后期你想要迁移到 Redis Cluster,迁移成本高
16、如何解决 Redis 的并发竞争 Key 问题
这个也是线上非常常见的一个问题,就是多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。
推荐一种方案:分布式锁(zookeeper和redis都可以实现分布式锁)。(如果不存在Redis的并发竞争Key问题,不要使用分布式锁,这样会影响性能)
17、使用过 Redis 做异步队列么,你是怎么用的?
一般使用list结构作为队列,rpush生产消息,lpop消费消息
。当lpop没有消息的时候,要适当sleep一会再重试。
如果不想sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
但如果是这样你发现redis作为消息队列是不安全的,它不能重复消费,一旦消费就会被删除。同时做消费者确认ACK也麻烦所以一般在实际开发中一般很少用redis中消息队列,因为现在已经有Kafka、RabbitMQ等成熟的消息队列了,它们的功能更加完善。
18、用Redis做延时队列,具体应该怎么实现?
延迟队列可以使用 zset(有序列表)实现,我们将消息序列化成一个字符串作为列表的value,这个消息的到期处理时间作为score,然后用定时器定时去扫描,一旦有执行时间小于或等于当前时间的任务,就立即执行。
19、使用Redis统计网站的UV,应该怎么做?
UV与PV不同,UV需要去重。一般有2种方案:
1、用BitMap。存的是用户的uid,计算UV的时候,做下bitcount就行了。
2、用布隆过滤器。将每次访问的用户uid都放到布隆过滤器中。优点是省内存,缺点是无法得 到精确的UV。但是对于不需要精确知道具体UV,只需要大概的数量级的场景,是个不错的选择。
20、什么是热Key问题,如何解决热key问题
所谓热key问题就是,突然有几十万的请求去访问redis上的某个特定key。那么,这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机。
而热点Key是怎么产生的呢?主要原因有两个:
- 用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
- 请求分片集中,超过单Redi服务器的性能,比如固定名称key,Hash落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点Key问题。
那么在日常开发中,如何识别到热点key呢?
- 凭经验判断哪些是热Key;
- 客户端统计上报;
- 服务代理层上报
如何解决热key问题?
- Redis集群扩容:增加分片副本,均衡读流量;
- 将热key分散到不同的服务器中;
- 使用二级缓存,即JVM本地缓存,减少Redis的读请求。
参考
[1] Redis 最佳实践指南: https://mp.weixin.qq.com/s/Fz1EbsmJP5k2Rh6ir_a1pQ
[2] Redis经典面试题: https://mp.weixin.qq.com/s/fBShKZbuR54yaIzzMR3R7g
[3] Redis面试20题: http://www.gameboys.cn/article/57
关注公众号:后端元宇宙。持续输出优质好文