redis反杀面试官之10问

简言

1. 笔者近几年来一直使用redis,也对redis有过仔细的研究,不敢说精通,非常熟悉肯定是有的

2. redis越来越火,网上相应的文章,总结,面试问题也有很多,但大多是应付简单面试用的,如果面试官再深入一些,恐怕大多数人都hold不住

3. 所以特在这里总结了一些有难度的问题,若你能认真学习研究,不但能大幅提高对redis的理解程度,反杀面试官也是轻轻松松

4. 问题不止10个,随时想到随时更新,有时间就会把答案补上来

问题和答案

问题1:持久化的混合存储模式(前面RDB+后面AOF),它的实现原理和好处

答:

先说原理:当执行命令bgrewriteaof触发AOF文件重写,会使用RDB的方式把数据快照保存到文件中,执行RDB的时候,把这期间redis执行的命令再用AOF的格式添加的文件后面

再说好处:单独RDB存储效率高,恢复效率也高,但是易丢失数据(执行RDB后又变更的数据会丢失);单独AOF存储虽然保证了数据极小可能的丢失,但是往硬盘刷数据的速度很低效,启动时加载AOF格式的数据更是慢到无法接受。混合存储前面使用RDB格式,保证了效率,后面使用AOF格式又保存了数据不丢失

问题2:scan命令,第三个参数应该怎么填

答:先看下scan命令的格式,第1个参数是下标,第2个参数是匹配模式,第三个参数是返回结果的限制个数

       1          2               3 
SCAN cursor [MATCH pattern] [COUNT count]

但是这个count限制并不是必定能起效,受多种因素影响,大致如下

1. 如果你不传count参数,那么redis会默认设置为10

2. 当redis发现hscan,zscan,scan命令遍历的对象是压缩列表实现的,说明对象内元素比较少,会全部遍历筛选后返回,返回的个数可能会超过count

3. redis是对数组里面单个下标里面的元素遍历,如果某个下标里面符合match格式的元素很多,会超出count值,单个下标遍历完后才会判断是否超出count值,所以可能已经超过了count

4. 如果单次遍历过程中耗时过长,为防止redis卡顿,即使符合条件的元素有很多,也会提前返回,此时个数会少于count值

综上:具体使用时要根据你对结果值的处理操作(元素数据总体量大小,单个元素的操作耗时等)来决定一批筛选多少个元素,redis已经为我们做好了防护

具体scan命令的实现原理可参考笔者的这篇博客redis的scan命令的源码分析,实现原理_papaya的博客-CSDN博客

问题3:scan命令,返回的游标值在redis中代表什么,是槽位吗,这个槽位和redis集群的槽位概念有关系吗

答:redis中的数据是以hash表的方式组织的,scan返回的游标值是hash的下标,从0开始,这个值和槽位无关。

由于scan遍历时采用高位遍历,所以返回的下标值可能忽大忽小。只需谨记:只要游标值不为0说明遍历未结束

问题4:scan命令,返回的key中,有一定概率会出现重复的情况,能说下到底什么情况下会出现重复吗?为什么,这种重复能优化吗

答:redis缩容的时候会出现,因为rehash采用高位加法,举例:遍历到下图中的橙色下标(110)时发生了缩容,前面的000,100,010都已经遍历过了,其中000,100节点会被rehash到新的00节点上,不会重复遍历。但是010,110节点都会rehash到新的10下标(第一行绿色的)上,那么010下标上对应的元素就会被重复遍历

问题5:redis的读写分离模式中,因为主从同步需要时间,所以有一定概率会出现主redis刚刚写入,从redis读取不到进而认为该key不存在的情况,如何优化

答:

问题6:redis命令查看单个key的大小有哪些命令,哪些是准确的,哪些是不准确的,比如--bigkeys命令的原理

答:

问题7:redis集群是使用raft算法还是paxos算法,知道raft算法、paxos算法、redis的raft算法的区别吗?

答:redis集群使用的是raft算法,但又不是原生的raft算法,进行了一些优化

问题8:rehash为什么要用高位加法,而不是我们通常的低位加法,好处是什么

答:采用高位进位加法,无论是扩容还是缩容,rehash 后的槽位在遍历顺序上是相邻的,这也是scan命令能保证全盘遍历的精妙之处,详见这篇博客:redis之rehash原理_papaya的博客-CSDN博客_redis rehash原理

问题9:redis对象中有个字段叫lru,只有24位,在LRU模式下,存储的是最后一次访问时间戳。由于只有24位,能表示的最大时间只有194天,超过194天的key怎么办,如何才能计算出它真正的空闲时间?

答:结论:超过194天的key,lru方式计算key的空闲时间不准确

计算redis对象空闲时间的代码如下

// 计算对象的空闲时间,也就是没有被访问的时间,返回结果是毫秒
unsigned long long estimateObjectIdleTime(robj* o)
{
    unsigned long long lruclock = LRU_CLOCK();
    // 获取Redis时钟,也就是server.lruclock的值,单位:秒
    if (lruclock >= o->lru)
    {
        // 正常递增时直接减即可(LRU_CLOCK_RESOLUTION的值默认是1000)
        return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
    }
    else
    {
        // 折返了,则加一轮最大值后再减(LRU_CLOCK_MAX表示一轮的最大值,即2^24 - 1)
        return (lruclock + LRU_CLOCK_MAX - 0->lru) * LRU_CLOCK_RESOLUTION;
    }
}

其中用到的LRU_CLOCK()函数,是获取当前节点实例的lru时间戳,代码如下

unsigned int LRU_CLOCK(void)
{
    unsigned int lruclock;
    if (1000 / server.hz <= LRU_CLOCK_RESOLUTION)
    {
        // 原子操作通常会走这里,我们只需注意这里
        atomicGet(server.lruclock, lruclock);
    }
    else
    {
        // 直接通过系统调用获取时间戳,hz 配置得太低(一般不会这么干),lruclock 更新不及时,需要实时获取系统时间戳
        lruclock = getLRUClock(); // 系统调用获取时间
    }
    return lruclock;
}

其中用到的 getLRUClock()函数,代码如下

unsigned int getLRUClock(void) {
    // 系统毫秒数 / 1000,得到秒数,最后做 & 运算,其中LRU_CLOCK_MAX的值为2的24次方 -1
    return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
}

上面代码中我已经尽量添加了注释,由上面代码可知,服务器的 lruclock是秒数时间戳,值是介于0 ~ 2的24次方之间,差不多194天,存在折返的可能性

对象robj的空闲时间判断逻辑如下:

若服务器lruclock未折返,则服务器lruclock的值大于robj的lru,相减即可得空闲时间

若服务器lruclock折返了,则服务器lruclock的值可能大于robj的lru,也可能小于robj的lru,此时

使用服务器lruclock + LRU_CLOCK_MAX - robj.lru 来计算空闲时间并不准确,因为可能需要加一个LRU_CLOCK_MAX ,也有可能需要加多个LRU_CLOCK_MAX,也有可能不需要加LRU_CLOCK_MAX

也就说,当服务器节点时间开启超过194天时,redis对象的空闲时间可能计算不准确

问题10:redis事务的watch有没有ABA问题,是如何解决的?

答:没有ABA问题。redis会为每个要监听的key维护一个监听的client列表,任何key发生变化时都会检测一下是否是watch中的key。若是watch中的key,则把监控这个key的所有client都标记为REDIS_DIRTY_CAS,意思是为该client的所有CAS操作都“dirty”了。当服务器收到该client的事务执行命令(即exec命令时),会检测是否有REDIS_DIRTY_CAS标记,若有,则直接返回,不再执行事务

详细分析可以见笔者的这篇博客:

redis的watch没有ABA的问题_papaya的博客-CSDN博客

问题11:布隆过滤器怎么删除元素呢?

答:经典的布隆过滤器不支持删除元素

如果一定要删除元素,业内普遍有两种做法

方法1:定时异步重建布隆过滤器,比如每隔3天把所有元素重新hash,建立新的布隆过滤器,重建完后再删掉旧的布隆过滤器。

方法2:使用计数型的布隆过滤器。因为经典的布隆过滤器的一个位只能为0或1,为1时不能记录有多少个元素引用了该位,一旦删除,数据就乱了。但计数型的布隆过滤器每个位是一个数字,记录了有多少个元素引用了该位,删除一个元素时只需对其hash对应的位的数字进行减1即可。

问题12:一般情况下redis cluster中key和slot的映射是通过算法(原理:crc16(key)%16384)对应的,知道如何强制把key映射到指定的slot呢?

答:在要操作的key中添加 {xxx} ,键哈希标签原理,源码如下

unsigned int keyHashSlot(char *key, int keylen) {
	undefined int s, e;
    
	// 找到 { 的index
	for (s = 0; s < keylen; s++)
	{
        if (key[s] == '{') 
			break;
	}
	// 没找到就计算crc16()的值
    if (s == keylen) 
	{
		return crc16(key,keylen) & 0x3FFF;
	}
	// 再往后面找 } 的index
    for (e = s+1; e < keylen; e++)
	{
        if (key[e] == '}') 
			break;
	}
	// 没找到 } 或者{}之间为空,仍然计算crc16()的值
    if (e == keylen || e == s+1) 
	{
		return crc16(key,keylen) & 0x3FFF;
	}
	
	// 获取{}之间的字符串进行crc16()计算
    return crc16(key+s+1,e-s-1) & 0x3FFF;
}

问题13:redis7.0中新加的multipart-aof的优点

答:

问题14:redis cluster中为什么最多16384个节点?计算key的hash值用的是函数crc16(),结果是0-65535之间,为什么redis cluster最大值不是65535个节点?

答:当初思考这个问题的时候我也是这么想的,如果最多65535个节点,那直接返回crc16(key)不就行了,为什么要使用crc16(key)%16384呢?

好在这个问题前人已经问题过,这个是redis作者的回答

why redis-cluster use 16384 slots? · Issue #2576 · redis/redis · GitHub

作者原版回答如下:
The reason is:

  • Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
  • At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.

So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set

我再解释下

重点:redis cluster中各个节点之间是相互连接,相互通讯的,会定期发送ping/pong消息

在这里,需要关注三个关键点

(1)交换什么数据

(2)数据包有多大

(3)交换的频率什么样

redis源码中,通讯协议的定义如下图

注意看红框的内容

1. type表示消息类型。

2. 有个myslots的char数组,长度为16383/8,这其实是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点管理的。

数据包有多大?
在消息头中,最占空间的是myslots[CLUSTER_SLOTS/8]。这块的大小是 16384÷8÷1024 = 2kb
那在消息体中,会携带一定数量的其他节点信息用于交换。
那这个其他节点的信息,到底是几个节点的信息呢?
约为集群总节点数量的1/10,至少携带3个节点的信息。
这里的重点是:节点数量越多,消息体内容越大。

消息体大小是10个节点的状态信息约1kb

交换的频率什么样?
redis集群内节点,每秒都在发ping消息。规律如下

(1)每秒会随机选取5个节点,找出最久没有通信的节点发送ping消息

(2)每100毫秒(1秒10次)都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster-node-timeout/2 则立刻发送ping消息

现在开始正式回答redis cluster为什么最多支持16384个节点

原因1:如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
如上所述,在消息头中,最占空间的是myslots[CLUSTER_SLOTS/8]
当槽位为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的压缩率就很低。

ps:文件压缩率指的是,文件压缩前后的大小比。

原因4:redis单个节点的效率已经可以达到10万+的级别,10000个节点就能支持到10亿qps的量级,16384个节点足以应对目前地球上任何程序需求了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值