当前位置:文档之家› 头条高级面试题:请谈谈Redis9种数据结构以及它们的内部编码实现

头条高级面试题:请谈谈Redis9种数据结构以及它们的内部编码实现

5种普通数据结构这个没什么好说的,对Redis稍微有点了解的都知道5种最基本的数据结构:String,List,Hash,Set,Sorted Set。

不过,需要注意的是,这里依然有几个高频面试题。

•Set和Hash的关系答案就是Set是一个特殊的value为空的Hash。

Set类型操作的源码在t_set.c中。

以新增一个元素为例(int setTypeAdd(robj *subject, sds value)),如果编码类型是OBJ_ENCODING_HT,那么新增源码的源码如下,事实上就是对dict即Hash数据结构进行操作,并且dictSetVal时value是NULL:dictEntry *de = dictAddRaw(ht,value,NULL);if (de) {dictSetKey(ht,de,sdsdup(value));dictSetVal(ht,de,NULL);return 1;}同样的,我们在t_hash.c中看到Hash类型新增元素时,当判断编码类型是OBJ_ENCODING_HT时,也是调用dict的方法:dictAdd(o->ptr,f,v),dictAdd最终也是调用dictSetVal()方法,只不过v即value 不为NULL:/* Add an element to the target hash table */int dictAdd(dict *d, void *key, void *val){dictEntry *entry = dictAddRaw(d,key,NULL);if (!entry) return DICT_ERR;dictSetVal(d, entry, val);return DICT_OK;}所以,Redis中Set和Hash的关系就很清楚了,当编码是OBJ_ENCODING_HT时,两者都是dict数据类型,只不过Set是value 为NULL的特殊的dict。

•谈谈你对Sorted Set的理解Sorted Set的数据结构是一种跳表,即SkipList,如下图所示,红线是查找10的过程:SkipList•如何借助Sorted set实现多维排序Sorted Set默认情况下只能根据一个因子score进行排序。

如此一来,局限性就很大,举个栗子:热门排行榜需要按照下载量&最近更新时间排序,即类似数据库中的ORDER BY download_count, update_time DESC。

那这样的需求如果用Redis的Sorted Set实现呢?事实上很简单,思路就是将涉及排序的多个维度的列通过一定的方式转换成一个特殊的列,即result = function(x, y, z),即x,y,z是三个排序因子,例如下载量、时间等,通过自定义函数function()计算得到result,将result作为Sorted Set中的score的值,就能实现任意维度的排序需求了。

可以参考笔者之前的文章:《》。

Redis内部编码我们常说的String,List,Hash,Set,Sorted Set只是对外的编码,实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis可以在合适的场景选择更合适的内部编码。

如下图所示(图片纠正:intset编码,而不是inset编码),可以看到每种数据结构都有2种以上的内部编码实现,例如String数据结构就包含了raw、int和embstr三种内部编码。

同时,有些内部编码可以作为多种外部数据结构的内部实现,例如ziplist就是hash、list和zset共有的内部编码,而set的内部编码可能是hashtable或者intset:Redis内部编码Redis这样设计有两个好处:1.可以偷偷的改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动对外数据结构和命令。

2.多种内部编码实现可以在不同场景下发挥各自的优势。

例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降。

这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist。

String的3种内部编码由上图可知,String的3种内部编码分别是:int、embstr、raw。

int类型很好理解,当一个key的value是整型时,Redis就将其编码为int类型(另外还有一个条件:把这个value当作字符串来看,它的长度不能超过20)。

如下所示。

这种编码类型为了节省内存。

Redis默认会缓存10000个整型值(#define OBJ_SHARED_INTEGERS 10000),这就意味着,如果有10个不同的KEY,其value都是10000以内的值,事实上全部都是共享同一个对象:127.0.0.1:6379> set number "7890"OK127.0.0.1:6379> object encoding number"int"接下来就是ebmstr和raw两种内部编码的长度界限,请看下面的源码:#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44robj *createStringObject(const char *ptr, size_t len) {if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)return createEmbeddedStringObject(ptr,len);elsereturn createRawStringObject(ptr,len);}也就是说,embstr和raw编码的长度界限是44,我们可以做如下验证。

长度超过44以后,就是raw编码类型,不会有任何优化,是多长,就要消耗多少内存:127.0.0.1:6379> set name "a1234567890123456789012345678901234567890 123"OK127.0.0.1:6379> object encoding name"embstr"127.0.0.1:6379> set name "a1234567890123456789012345678901234567890 1234"OK127.0.0.1:6379> object encoding name"raw"那么为什么有embstr编码呢?它相比raw的优势在哪里?embstr编码将创建字符串对象所需的空间分配的次数从raw编码的两次降低为一次。

因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能更好地利用缓存带来的优势。

并且释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码对象的字符串对象需要调用两次内存释放函数。

如下图所示,左边是embstr编码,右边是raw编码:embstr V.S. rawziplist由前面的图可知,List,Hash,Sorted Set三种对外结构,在特殊情况下的内部编码都是ziplist,那么这个ziplist有什么神奇之处呢?以Hash为例,我们首先看一下什么条件下它的内部编码是ziplist:1.当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个);2.所有值都小于hash-max-ziplist-value配置(默认64个字节);如果是sorted set的话,同样需要满足两个条件:1.元素个数小于zset-max-ziplist-entries配置,默认128;2.所有值都小于zset-max-ziplist-value配置,默认64。

实际上,ziplist充分体现了Redis对于存储效率的追求。

一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。

这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。

而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。

它是一个表(list),但其实不是一个链表(linked list)。

ziplist的源码在ziplist.c这个文件中,其中有一段这样的描述 -- The general layout of the ziplist is as follows::<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend> •zlbytes:表示这个ziplist占用了多少空间,或者说占了多少字节,这其中包括了zlbytes本身占用的4个字节;•zltail:表示到ziplist中最后一个元素的偏移量,有了这个值,pop操作的时间复杂度就是O(1)了,即不需要遍历整个ziplist;•zllen:表示ziplist中有多少个entry,即保存了多少个元素。

由于这个字段占用16个字节,所以最大值是2^16-1,也就意味着,如果entry 的数量超过2^16-1时,需要遍历整个ziplist才知道entry的数量;•entry:真正保存的数据,有它自己的编码;•zlend:专门用来表示ziplist尾部的特殊字符,占用8个字节,值固定为255,即8个字节每一位都是1。

如下就是一个真实的ziplist编码,包含了2和5两个元素:[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]| | | | | |zlbytes zltail entries "2" "5" end linkedlist这是List的一种编码数据结构非常简单,就是我们非常熟悉的双向链表,对应Java中的LinkedList。

skiplist这个前面也已经提及,就是经典的跳表数据结构。

hashtable这个也很容易,对应Java中的HashMap。

intsetSet特殊内部编码,当满足下面的条件时Set的内部编码就是intset而不是hashtable:1.Set集合中必须是64位有符号的十进制整型;2.元素个数不能超过set-max-intset-entries配置,默认512;验证如下:127.0.0.1:6379> sadd scores 135(integer) 0127.0.0.1:6379> sadd scores 128(integer) 1127.0.0.1:6379> object encoding scores"intset"那么intset编码到底是个什么东西呢?看它的源码定义如下,很明显,就是整型数组,并且是一个有序的整型数组。

相关主题