本篇博客参考:html
Redis的压缩列表ZipListpython
上篇博客中,我给你们走马观花般的介绍了Redis中SDS的奥秘,说明Redis之因此那么快,还有一个很重要、可是常常被你们忽视的一点,那就是Redis精心设计的数据结构。本篇博客,仍是继续这个话题,给你们介绍下Redis另一种底层数据结构:ziplist。git
在Redis中,有五种基本数据类型,除了上篇博客提到的String,还有list,hash,zset,set,其中list,hash,zset都间接或者直接使用了ziplist,因此说理解ziplist也是至关重要的。github
我刚开始看ziplist的时候,总以为zip这个单词甚是熟悉,好像在平常使用电脑的时候常常看到,因而我百度了下:
哦哦,怪不得那么熟悉,原来就是“压缩”的意思,那ziplist就能够翻译成“压缩列表”了。web
有两点缘由:redis
zadd programmings 1.0 go 2.0 python 3.0 java
建立了一个zset,里面有三个元素,而后看下它采用的数据结构:segmentfault
debug object programmings "Value at:0x7f404ac30c60 refcount:1 encoding:ziplist serializedlength:36 lru:2689815 lru_seconds_idle:9"
HSET website google "www.g.cn
建立了一个hash,只有一个元素,看下它采用的数据结构:数组
debug object website "Value at:0x7f404ac30ac0 refcount:1 encoding:ziplist serializedlength:30 lru:2690274 lru_seconds_idle:14"
能够很清楚的看到,zset和hash都采用了ziplist数据结构。
当知足必定的条件,zset和hash就再也不使用ziplist数据结构了:
debug object website "Value at:0x7f404ac30ac0 refcount:1 encoding:hashtable serializedlength:180 lru:2690810 lru_seconds_idle:2"
能够看到,hash的底层数据结构变成了hashtable。
szet就不作实验了,感兴趣的小伙伴们能够本身实验下。
至于这个转换条件是什么,放到后面再说。
好奇的大家,确定会尝试看下list的底层数据结构是什么,发现并非ziplist:
LPUSH languages python debug object languages "Value at:0x7f404c4763d0 refcount:1 encoding:quicklist serializedlength:21 lru:2691722 lru_seconds_idle:22 ql_nodes:1 ql_avg_node:1.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:19"
能够看到,list采用的底层数据结构是quicklist,并非ziplist。
在低版本的Redis中,list采用的底层数据结构是ziplist+linkedList,高版本的Redis中,quicklist替换了ziplist+linkedList,而quicklist也用到了ziplist,因此能够说list间接使用了ziplist数据结构。这个quicklist是什么,不是本篇博客的内容,暂且不表。
ziplist源码:ziplist源码
ziplist源码的注释写的很是清楚,若是英语比较好,能够直接看上面的注释,若是你英语不是太好,或者没有必定的钻研精神,仍是看看我写的博客吧。
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
这是在注释中说明的ziplist布局,咱们一个个来看,这些字段是什么:
Redis经过如下宏定义实现了对ziplist各个字段的存取:
// 假设char *zl 指向ziplist首地址 // 指向zlbytes字段 #define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl))) // 指向zltail字段(zl+4) #define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t)))) // 指向zllen字段(zl+(4*2)) #define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2))) // 指向ziplist中尾元素的首地址 #define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) // 指向zlend字段,指恒为255(0xFF) #define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
从ziplist布局中,咱们能够很清楚的知道,咱们的数据被保存在ziplist中的一个个entry中,咱们下面来看看entry的构成。
<prevlen> <encoding> <entry-data>
咱们再来看看这三个字段是什么:
prevlen字段是变长的:
下面就要介绍下encoding这个字段了,在此以前,你们能够到阳台吹吹风,喝口热水,再作个深呼吸,最后再作一个心理准备,由于这个字段实在是太复杂了,搞很差,看的时候,一会儿吐了。。。若是实在没法理解,直接略过这一段吧。
Redis为了节约空间,对encoding字段进行了至关复杂的设计,Redis经过encoding来判断存储数据的类型,下面咱们就来看看Redis是如何根据encoding来判断存储数据的类型的:
00xxxxxx
最大长度位 63 的短字符串,后面的6个位存储字符串的位数;01xxxxxx xxxxxxxx
中等长度的字符串,后面14个位来表示字符串的长度;10000000 aaaaaaaa bbbbbbbb cccccccc dddddddd
特大字符串,须要使用额外 4 个字节来表示长度。第一个字节前缀是10
,剩余 6 位没有使用,统一置为零;11000000
表示 int16;11010000
表示 int32;11100000
表示 int64;11110000
表示 int24;11111110
表示 int8;11111111
表示 ziplist 的结束,也就是 zlend 的值 0xFF;1111xxxx
表示极小整数,xxxx 的范围只能是 (0001~1101
), 也就是1~13
。若是是第10种状况,那么entry的构成就发生变化了:
<prevlen> <encoding>
由于数据已经存储在encoding字段中了。
能够看出Redis根据encoding字段的前两位来判断存储的数据是字符串(字节数组)仍是整型,若是是字符串,还能够经过encoding字段的前两位来判断字符串的长度;若是是整形,则要经过后面的位来判断具体长度。
咱们上面说了那么多关于entry的点点滴滴,下面将要说的内容可能会颠覆你三观,咱们在源码中能够看到entry的结构体,上面有一个注释很是重要:
/* We use this function to receive information about a ziplist entry. * Note that this is not how the data is actually encoded, is just what we * get filled by a function in order to operate more easily. */ typedef struct zlentry { unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/ unsigned int prevrawlen; /* Previous entry len. */ unsigned int lensize; /* Bytes used to encode this entry type/len. For example strings have a 1, 2 or 5 bytes header. Integers always use a single byte.*/ unsigned int len; /* Bytes used to represent the actual entry. For strings this is just the string length while for integers it is 1, 2, 3, 4, 8 or 0 (for 4 bit immediate) depending on the number range. */ unsigned int headersize; /* prevrawlensize + lensize. */ unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on the entry encoding. However for 4 bits immediate integers this can assume a range of values and must be range-checked. */ unsigned char *p; /* Pointer to the very start of the entry, that is, this points to prev-entry-len field. */ } zlentry;
重点看上面的注释。一句话解释:这个结构体虽然定义出来了,可是没有被使用,由于若是真的这么使用的话,那么entry占用的内存就太大了。
Redis并无像上篇博客介绍的SDS同样,封装一个结构体来保存ziplist,而是经过定义一系列宏来对数据进行操做,也就是说ziplist是一堆字节数据,上面所说的ziplist的布局和ziplist中的entry的布局只是抽象出来的概念。
在文章比较前面的部分,咱们作了实验来证实,知足必定的条件后,zset、hash的底层存储结构再也不是ziplist,既然ziplist那么牛逼,Redis的开发者也花了那么多精力在ziplist的设计上面,为何zset、hash的底层存储结构不能一直是ziplist呢?
由于ziplist是紧凑存储,没有冗余空间,意味着新插入元素,就须要扩展内存,这就分为两种状况:
因此ziplist 不适合存储大型字符串,存储的元素也不宜过多。
那么知足什么条件后,zset、hash的底层存储结构再也不是ziplist呢?在配置文件中能够进行设置:
hash-max-ziplist-entries 512 # hash 的元素个数超过 512 就必须用标准结构存储 hash-max-ziplist-value 64 # hash 的任意元素的 key/value 的长度超过 64 就必须用标准结构存储 zset-max-ziplist-entries 128 # zset 的元素个数超过 128 就必须用标准结构存储 zset-max-ziplist-value 64 # zset 的任意元素的长度超过 64 就必须用标准结构存储
对于这个配置,我只是一个搬运工,并无去实验,毕竟没有人会去修改这个吧,感兴趣的小伙伴能够试验下。
在介绍ziplist布局的时候,说到ziplist用两个位来记录ziplist的元素个数,若是元素个数实在太多,两个位不够怎么办呢?这种状况下,求ziplist元素的个数只能遍历了。
看到了吧,Redis真不是想象中的那么简单,须要研究的东西仍是挺多,也挺复杂的,若是咱们不去学习,可能以为本身彻底掌握了Redis,可是一旦开始学习了,才发现咱们先前掌握的只是皮毛。验证了一句话,知道的越多,不知道的越多。
本篇博客到这里就结束了。