今天来讲list,相当于Java里的LinkedList,也就是链表,而且是双向链表。类似于Deque,两端都可以操作。
同样的,list在不同大小的情况下,和String一样,有不同的数据结构。
还记得么,embstr和raw,忘记了的话,去翻看讲解string的那一章吧。
好了,list对应的两个数据结构,分别叫做ziplist(压缩列表)与quicklist。
但不管怎样,别忘了我们的对象头
struct RedisObject {
int4 type;//4bit,对象类型,那最多最多16种对象咯
int4 encoding;//存储形式
int24 lru;//Least Recently Used,最近最少使用信息
int32 refcount;//暂未知,之后补上,todo
void *ptr;//8byte 指针对象,这个值是个指针啊,存的是目标的地址!
}
这回,ziplist没说跟对象头紧紧的贴在一起了。
struct RedisObject {
int4 type;//4bit,对象类型,那最多最多16种对象咯
int4 encoding;//存储形式
int24 lru;//Least Recently Used,最近最少使用信息
int32 refcount;//暂未知,之后补上,todo
void *ptr;//8byte 指针对象,这个值是个指针啊,存的是目标的地址!
}
//没说跟对象头贴在一起
struct ziplist<T>{
int32 zlbytes;//整个压缩列表占用字节数
int32 zltail_offset;//最后一个元素距离压缩列表起始位置的偏移量
int16 zllength;//元素的个数
T[] entries;//元素内容列表,依次紧凑存储
int8 zlend;//标志压缩列表的结束,值恒为0xFF
}
咱注意,entries是紧凑存储的,而且entries类型未定,多少长咱都不知道的啊。假如entries是字符串类型的,每个entries可能长度都不一样的。
我们看下entry的结构到底长啥样
struct entry{
int<var> prevlen;//前一个entry的长度
int<var> encoding;//表示content的类型和长度,如果content很短的话,甚至直接表示值了
optional byte[] content;//可选的
}
entry的prevlen是可变长的,如果前面的entry长度小于254,就是2的8次方-2呗,那么prevlen就是1字节长度。
如果超过254字节,那么prevlen就会占用5字节长度(突然变得这么长)!并且第一个字节的值始终为0xFE。
主要是为了倒着取值的时候,能快速取到上一个的位置。想起来了没,虽然list是双向链表,但是在ziplist的情况下,咱们没有前后指针,只能用这种标记长度的方法来了。
encoding存储了后面content的编码类型信息。
这个设计厉害了,encoding需要表达2个意思,类型+大小。
类型可以是字符串或者是数字
00,01,10开头的是字符串,00xx xxxx表示最大长度为63的字符串,后面那6位表示字符串的长度
01xx xxxx xxxx xxxx 表示中等长度的字符串,后面14位表长度
1000 0000 aaaaaaaa bbbbbbbb cccccccc dddddddd是特大字符串,不过这个数据类型是没机会使用的,因为压缩列表通常只用来存储小数据。(1100-1111)前面这4位+(0000)后四位,分别表示2、4、8、16个字节长的整数。
11110000表示3字节的整数
11111110表示1个字节的整数
11111111表示zlend的值,也就是0xFF
1111xxxx表示1~12的极小值。
我都震惊了。不过对于作者来说,应该是常规操作了。常规的通信设计。
这么紧挨着在一起,肯定是有问题的啊,新增,修改的时候,因为紧挨在一起,如果变动长度,免不了后面的数据全部移动。
所以,这个只能存点少的元素。级联更新太可怕了。最好不要变动。
也无怪,叫做压缩(zip)列表。
快速列表 quicklist
以前,如果内容超过了ziplist的范围,则会变成普通的双向链表linkedlist。
struct listNode{
listNode *pre;
listNode *next;
T value;
}
struct list{
listNode *head;
listNode *tail;
long length;
}
看listNode可知,两个指针太浪费了,64位操作系统,就要占去2*8字节,整整16字节啊,这太浪费了。而且listNode分散在内存里,加剧内存碎片化,影响内存管理效率。
所以现在quicklist代替了linkedlist。
quicklist其实就是把listNode类型,换成了ziplist。。。
将一个个ziplist串在了一起。。。
struct ziplist<T>{
int32 zlbytes;//整个压缩列表占用字节数
int32 zltail_offset;//最后一个元素距离压缩列表起始位置的偏移量
int16 zllength;//元素的个数
T[] entries;//元素内容列表,依次紧凑存储
int8 zlend;//标志压缩列表的结束,值恒为0xFF
}
struct ziplist_compressed {
int32 size;
byte[] compressed_data;
}
struct quicklistNode {
quicklistNode* pre;
quicklistNode* next;
ziplist* zl;//指向压缩列表
int32 size;//ziplist的字节总数
int16 count;//ziplist的元素数量
int2 encoding;//存储形式,原生数组还是LZF压缩存储
}
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count;//元素总数
int node;//ziplist节点个数
int compressDepth;//LZF算法压缩深度
}
LZF看起来很厉害的样子。。。
每个ziplist默认存储8KB,受list-max-ziplist-size决定
quicklist默认的压缩深度是0,也就是默认不压缩。压缩深度也是可控制的,list-compress-depth。压缩深度的意思,就是,除了0以外,从1开始,代表的都是从首尾开始,几个ziplist不压缩,其他的都压缩。
listpack
5.0版本引进了新的数据结构,是对ziplist的改进。
我们还记得,ziplist是一种紧凑的数组结构,每个entry都存了上一个entry的length,如果上一个entry的长度从253变成了254,那么prevlen 字段就要从1字节扩展到5字节,那么整个后面的entry都要更新prevlen。就是所谓的级联更新。
正是因为级联更新太可怕了,所以新来了listpack。
取消了entry里记录prevlen的操作。
struct listpack<T> {
int32 total_type;//占用的总字节数;
int16 size;//元素个数
T[] entries;//紧凑排列的元素列表
int8 end;//同zlend一样,恒为0xfFF
}
struct ziplist<T>{
int32 zlbytes;//整个压缩列表占用字节数
int32 zltail_offset;//最后一个元素距离压缩列表起始位置的偏移量
int16 zllength;//元素的个数
T[] entries;//元素内容列表,依次紧凑存储
int8 zlend;//标志压缩列表的结束,值恒为0xFF
}
比较一下,这两个数据结构少了一个zltail_offset,当然这个可以计算得到。
那listpack是如何消除级联更新的呢?
struct lpentry {
int<var> encoding;
optional byte[] content;
int<var> length;当前entry的长度
}
struct entry{
int<var> prevlen;//前一个entry的长度
int<var> encoding;//表示content的类型和长度,如果content很短的话,甚至直接表示值了
optional byte[] content;//可选的
}
对比一下,将prevlen改成了length,这样确实,上一个entry长度变化,对当前的entry的属性不会影响。