头条高级面试题请谈谈Redis9种数据结构以及它们的内部编码实现.docx
- 文档编号:28617162
- 上传时间:2023-07-19
- 格式:DOCX
- 页数:8
- 大小:109.93KB
头条高级面试题请谈谈Redis9种数据结构以及它们的内部编码实现.docx
《头条高级面试题请谈谈Redis9种数据结构以及它们的内部编码实现.docx》由会员分享,可在线阅读,更多相关《头条高级面试题请谈谈Redis9种数据结构以及它们的内部编码实现.docx(8页珍藏版)》请在冰豆网上搜索。
头条高级面试题请谈谈Redis9种数据结构以及它们的内部编码实现
5种普通数据结构
这个没什么好说的,对Redis稍微有点了解的都知道5种最基本的数据结构:
String,List,Hash,Set,SortedSet。
不过,需要注意的是,这里依然有几个高频面试题。
∙Set和Hash的关系
答案就是Set是一个特殊的value为空的Hash。
Set类型操作的源码在t_set.c中。
以新增一个元素为例(
intsetTypeAdd(robj*subject,sdsvalue)
),如果编码类型是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。
∙谈谈你对SortedSet的理解
SortedSet的数据结构是一种跳表,即SkipList,如下图所示,红线是查找10的过程:
SkipList
∙如何借助Sortedset实现多维排序
SortedSet默认情况下只能根据一个因子score进行排序。
如此一来,局限性就很大,举个栗子:
热门排行榜需要按照下载量&最近更新时间排序,即类似数据库中的ORDERBYdownload_count,update_timeDESC。
那这样的需求如果用Redis的SortedSet实现呢?
事实上很简单,思路就是将涉及排序的多个维度的列通过一定的方式转换成一个特殊的列,即result=function(x,y,z),即x,y,z是三个排序因子,例如下载量、时间等,通过自定义函数function()计算得到result,将result作为SortedSet中的score的值,就能实现任意维度的排序需求了。
可以参考笔者之前的文章:
《》。
Redis内部编码
我们常说的String,List,Hash,Set,SortedSet只是对外的编码,实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样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个整型值(#defineOBJ_SHARED_INTEGERS10000),这就意味着,如果有10个不同的KEY,其value都是10000以内的值,事实上全部都是共享同一个对象:
127.0.0.1:
6379> set number "7890"
OK
127.0.0.1:
6379> object encoding number
"int"
接下来就是ebmstr和raw两种内部编码的长度界限,请看下面的源码:
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
也就是说,embstr和raw编码的长度界限是44,我们可以做如下验证。
长度超过44以后,就是raw编码类型,不会有任何优化,是多长,就要消耗多少内存:
127.0.0.1:
6379> set name "a1234567890123456789012345678901234567890123"
OK
127.0.0.1:
6379> object encoding name
"embstr"
127.0.0.1:
6379> set name "a12345678901234567890123456789012345678901234"
OK
127.0.0.1:
6379> object encoding name
"raw"
那么为什么有embstr编码呢?
它相比raw的优势在哪里?
embstr编码将创建字符串对象所需的空间分配的次数从raw编码的两次降低为一次。
因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能更好地利用缓存带来的优势。
并且释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码对象的字符串对象需要调用两次内存释放函数。
如下图所示,左边是embstr编码,右边是raw编码:
embstrV.S.raw
ziplist
由前面的图可知,List,Hash,SortedSet三种对外结构,在特殊情况下的内部编码都是ziplist,那么这个ziplist有什么神奇之处呢?
以Hash为例,我们首先看一下什么条件下它的内部编码是ziplist:
1.当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个);
2.所有值都小于hash-max-ziplist-value配置(默认64个字节);
如果是sortedset的话,同样需要满足两个条件:
1.元素个数小于zset-max-ziplist-entries配置,默认128;
2.所有值都小于zset-max-ziplist-value配置,默认64。
实际上,ziplist充分体现了Redis对于存储效率的追求。
一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。
这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。
而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。
它是一个表(list),但其实不是一个链表(linkedlist)。
ziplist的源码在ziplist.c这个文件中,其中有一段这样的描述--Thegenerallayoutoftheziplistisasfollows:
:
∙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。
intset
Set特殊内部编码,当满足下面的条件时Set的内部编码就是intset而不是hashtable:
1.Set集合中必须是64位有符号的十进制整型;
2.元素个数不能超过set-max-intset-entries配置,默认512;
验证如下:
127.0.0.1:
6379> sadd scores 135
(integer) 0
127.0.0.1:
6379> sadd scores 128
(integer) 1
127.0.0.1:
6379> object encoding scores
"intset"
那么intset编码到底是个什么东西呢?
看它的源码定义如下,很明显,就是整型数组,并且是一个有序的整型数组。
它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数采取了不同的编码,尽量对内存的使用进行了优化。
这样的数据结构,如果执行SISMEMBER命令,即查看某个元素是否在集合中时,事实上使用的是二分查找法:
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
//intset编码查找方法源码(人为简化),标准的二分查找法:
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
while(max >= min) {
mid = ((unsigned int)min + (unsigned int)max) >> 1;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 高级 试题 谈谈 Redis9 种数 结构 以及 它们 内部 编码 实现