Redis Interview
String
1.1 Set一个已有的数据会发生什么?
如果是同种类型的值,会覆盖原有值,同时会覆盖或者擦除键的过期时间
1.2 浮点型在String是用什么表示?
分析:基础知识,String有三种编码模式,INT只针对整形,所以浮点型必然是字符串存储
回答:将浮点数放入字符串对象,先将浮点数转换成字符串值,再保存转换所得的字符串值,所以浮点数在String对象中使用字符串值表示
1.3 String可以有多大?(512M)
分析:基础知识,需要知道明确的数值,进一步思考为什么是这个值
回答:一个Redis字符串最大为512M
Redis的String类型的最大长度是2^29-1 (536870911),这是因为Redis的字符串大小限制在512M(1)。这个限制主要是出于内存管理和性能考虑。如果字符串过大,可能会导致内存使用效率低下,并且在网络传输时也会消耗更多的带宽。此外,处理大字符串也会占用更多的CPU资源。因此,Redis设计者选择了512M作为字符串的最大长度,以平衡内存使用、网络带宽和CPU资源之间的关系.
1.4 Redis中字符串是怎么实现的?
分析:分情况讲编码类型,分别应用在什么场景。EMBSTR编码和RAW编码的选择阈值(可以说版本差别,3.2之前是39,3.2版本之后是44)
回答:redis字符串底层是String对象,String对象有三中编码方式:INT、EMBSTR、RAW。如果存储一个整形,可以用long表示的整数就以INT编码存储;如果存字符串,当字符串长度小于等于阈值(44)就使用EMBSTR编码;如果字符串大于阈值,则使用RAW编码。
Redis < 3.2 阈值=39,> 3.2 阈值=44
1.5 为什么EMBSTR的阈值是44?(超级冷门)
分析:
- redis默认使用jemalloc作为内存分配器
- jemalloc是以64字节作为内存单元做内存分配,如果超出了64个字节就超过了一个内存单元,尽量在一个内存单元,是为了减少内存寻址,又不会消耗分配过多没用到的内存,
- 围绕64字节的关键分界分析版本变化,Redis的字符串对象是由一个redisObject+sdshdr两部分组成,redisObject大小为4+4+24+32+64 = 128bits = 16bytes,其中的ptr是64,是为了方便各种编译器和目标平台上使用 sdshdr8占用内存大小: 1byte + 1byte + 1byte + 内联数组的大小,由于内联数组中还有一个'\0'占位一个字符,所以能用的大小为64-16(redisObject)-3(sdshdr非内容属性)-1('\0')=44
回答:redis是使用jemalloc内存分配器,Redis以64字节为阈值区分大小字符串 所以EMBSTR的边界数组,是受到64这个阈值影响 redis对象占用的内存大小由redisObject和sdshdr两个部分组成,redisObject占用16字节,sdshdr中已分配、已申请、标记三个字段占用了3个字节。'\0'占用了一个字节,能够存放的数据就是64 - (16 + 3 + 1) = 44
1.6 为什么EMBSTR的阈值之前是39?(超级冷门)
...
1.7 SDS有什么用?
分析:redis是C语言写的,SDS是对c字符串的封装,一般对比普遍C字符串。可以从以下的场景来描述:计算长度,扩容,缩容,二进制存储
回答:三点
- SDS包含容量字段,O(1)时间快速返回字符串长度,相比于C字符串,需要O(n)
- 有预留空间,在扩容时如果预留空间足够,就不需要重新分配内存,节约性能,缩容时也可以将减少的空间保留下来,后续再使用
- 不再以'\0'作为字符串结束判断标准,二进制安全,可以更方便存储一些二进制数据
List
2.1 List是完全先入先出吗?
分析:先入先出是指只允许从尾部入队,从头出队,而LIST是一个双端操作对象,所以List不是完全的先入先出
回答:List是双端操作对象,所以不是完全的先入先出,List也可以后入先出
2.2 怎么获取List指定范围内的数据?
回答:LRANGE命令参数为start和stop,比如一个列表有s1, s2, s3三个数据,用LRANGE 0 1 可以得到s1, s2
2.3 List如何移除特定值的数据?时间复杂度是多少?
回答:LREM命令可以移除特定的数据,比如LREM key 1 aaa,就回去移除第一个List key里面的值为aaa的数据,这个操作是遍历,所以时间复杂度为O(N)
2.4 List对象底层编码方式是什么?
分析:redis3.2版本之前和之后,List的底层编码方式是不同的,面试时对不同版本的Redis的List对象底层编码方式作出分析
回答:5.0.5版本,List对象的编码全部由QUICKLIST实现。QUICKLIST是一个压缩列表组成的双向链表,结合了ZIPLIST和LINKEDLIST两者的优点 QUICKLIST是在3.2版本之后才使用的,之前都是ZIPLIST和LINKEDLIST一起使用,元素少使用ZIPLIST,元素多使用LINKEDLIST
2.5 LINKEDLIST编码下,查询节点个数的时间复杂度是多少?
分析:LINKEDLIST的表头结构:
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void *(*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
回答:LINKEDLIST编码下,查询节点个数的时间复杂度是O(1),因为LINKEDLIST的表头结构中定义了链表锁包含节点数量的字段len
3.1 ZIPLIST有什么优点?
分析:考察基础认识,优点最好和LINKEDLIST对比
回答:ZIPLIST是压缩列表,是用来节约内存的,相比于LINKEDLIST链表式设计,压缩列表的内存都是紧凑排列在一起的,这带来了几个优点:1. 节约内存,2. 方便一次性分配,3. 遍历时局部性更强
3.2 ZIPLIST是怎么压缩数据?
分析:考察ZIPLIST数据结构,ZIPLIST本身是为了节约内存而开发的,从ZIPLIST结构入手
<zlbytes> <zltail> <zllen> <entry> <entry> ...<entry> <zlend>
// ---><entry>
<prevlen> <encoding> <entry-data>
回答:ZIPLIST分为三个部分,结构头,数据部分,结尾标识。其中,结构头记录了字节数,起始地址距尾部节点距离,节点个数;数据部分记录了上个节点的长度、内容编码,内容本身;最后一个1字节的结尾标识。因为ZIPLIST的结构是内存连续的,介绍了指针的开销。
3.3 ZIPLIST下List可以从后往前遍历吗?
分析:基石:List是一种双端数据结构,无论哪种底层编码,都需要支持从后往前遍历;阐述ZIPLIST如何做到从后往前遍历,都是在考察ZIPLIST的数据结构;ZIPLIST下的entry结构体包含了上个节点的长度,所以可以通过上一个节点的长度,找到上一个节点的起始位置,这样就实现了从后往前遍历
回答:可以,List是双端数据结构,无论哪种底层编码,都需要支持从后往前遍历。ZIPLIST每个节点中都保存了上一个节点的长度,所以可以用当前节点的地址减去上一个节点长度来找到上个节点起始位置,进而实现从后往前遍历
3.4 ZIPLIST下List如何从前往后遍历的?
分析:类似上面“ ZIPLIST下List可以从后往前遍历吗?“的分析,ZIPLIST的entry结构中,prevlen是找到上一个节点的起始位置,entry-data是实际的内容,在encoding中,无论String还是Int都是可以知道长度信息
回答:可以,List是双端操作,无论哪种底层编码,都需要能够支持从前往后遍历。ZIPLIST每个节点中的encoding字段,都包含了字符串或者整形的长度信息,可以用该信息实现往后遍历
3.5 在ZIPLIST数据结构下,查询节点个数的时间复杂度是多少?
回答:在ZIPLIST编码下,查询节点个数的时间复杂度是O(1),但是可以ZIPLIST中定义的zllen字段只有2字节,所以当数据个数超过65535时,zllen字段就失效了,这时想要知道节点个数就需要使用遍历,此时时间复杂度是O(n)