基于redis5.0的版本。
redis链表(List)字符编码有:ziplist和quicklist,老版本也有linkedlis。
1. linkedlist
3.2
版本之后列表不再使用linkedlist,这里列出来是为了后面的对比。
struct list {
listNode *head; //表头节点
listNode *tail; //表尾节点
unsigned long len; //链表锁包含的节点数量
void *(*dup)(void *ptr); //节点值复制函数
void *(*free)(void *ptr); //节点值释放行数
void *(*match)(void *ptr, void *key); //节点值对比函数
}
struct listNode {
listNode *prev;
listNode *next;
void *value;
}
- 双向:链表带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
- 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对列表的范围以NULL为终点。
- 带表头指针和尾指针:通过head和taiil指针,获取头结点和尾节点的复杂度为O(1)。
- 带链表长度计数器:通过len获取节点数量的复杂度为O(1)。
- 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dump、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
StringObject就是type为string的RedisObject对象,在本文中都简称为StringObject。
2. ziplist
// ziplist.c
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
unsigned char *zl = zmalloc(bytes);
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
ZIPLIST_LENGTH(zl) = 0;
zl[bytes-1] = ZIP_END;
return zl;
}
字段 | 类型 | 长度 | 说明 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 记录整个压缩列表占用的内存字节数,包括 4 字节的 zlbytes 本身。在对压缩列表进行内存重分配时,或者计算zlend的位置时使用。 |
zltail | uint32_t | 4字节 | 记录压缩列表表尾节距离起始地址的字节偏移量,通过这个偏移量,可以快速确定最后一个节点的地址。 |
zllen | uint16_t | 2字节 | 记录压缩列表中节点的数量。当节点数量超过或者等于 UINT16_MAX(65535) 时,需要遍历整个列表才能知道节点的数量。 |
entry | zlentry | 根据节点内容 | 压缩列表包含的各个节点。 |
zlend | uint8_t | 1字节 | 固定值为0xFF(255),标识压缩列表的尾节点。其他普通的节点不会以 255 开头。因此可以通过检查节点的地一个字节是否等于 255 从而知道是否已经到达列表的末尾。 |
下图实例:
说明:
- zlbytes值为80,标识列表总长80个字节。
- zltail值为60,表示从首节点指针p加上偏移量60,即可得到尾节点entry3的起始地址。
- zllen值为3,表示entry节点数是3个。
entry:
2.1 prevrawlen:
prevrawlen记录前一个节点的长度,属性的长度是1字节或者5字节。
- 如果前一个节点的长度小于254字节,prevrawlen长度为1字节,值为前一个节点的长度。
- 如果前一个字节的长度大于254字节,则prevrawlen长度为5字节,第一个字节值是0xFE(十进制254),之后的四个字节值是前一个节点的长度。
因为prevrawlen记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。压缩列表的从表尾到表头遍历操作就是使用这一原理。
2.2 encoding
encoding记录的是data数据的类型和长度(github的ziplist.c上有详细说明)。
- 当最高位为00时,encoding长度为1字节,data字节数组长度小于63字节(2的6次方),除去最高两位后的值表示data的长度。
- 当最高位为01时,encoding长度为2字节,data字节数组长度小于16,383字节(2的14次方),除去最高两位后的值表示data的长度。
- 当最高位为10时,encoding长度为5字节,data字节数组长度小于4,294,967,295字节(2的36次方),除去最高两位后的值表示data的长度。
4.当最高位为11时,encoding长度为1字节,data保存当是整数值:
- 值为11000000,data数值类型是2个字节的int16_t编码。
- 值为11010000,data数值类型是4个字节的int32_t编码。
- 值为11100000,data数值类型是8个字节的int64_t编码。
- 值为11110000,data数值为3个字节(24位)当有符号整数编码。
- 值为1111XXXX,XXXX取值0001 ~ 1101,分别表示0~12整数值,0001表示0,以此类推,当encoding值为这个范围是,XXXX值即为data值,也就是entry没有data属性。
- 值为11111110,data数值类型是1个字节(24位)当有符号整数编码。
- 注:encoding没有11111111值,因为11111111固定为ziplist的zlend值(尾节点)。
例如:
- 值为“hello”的entry:
- 值为整数“2”的entry:
2.3 连锁更新
当ziplist插入新节点,或者节点内容变长时,需要追加申请需要的内存空间(ziplist.c文件下的ziplistResize函数,最终是调用object.c下的zrealloc函数),如果无法追加申请到足够的内存,则会重新申请一个完整的内存,并将当前ziplist数据复制到新内存空间。
prevlen属性记录了前一个节点的长度:假设entry2的前一个节点entry1节点长度小于254字节,则entry2的prevlen只需要1个字节来保存这个长度;如果entry1内容变更(或者在entry1和entry2之间插入新节点;或者删除entry1使得entry2的前置节点变成entry0),超出来254字节,则entry2的prevlen当前1个字节无法保存,需要扩展成5个字节,redis需要重新申请内存空间;如果刚好entry2原本的长度介于250~253字节之间,扩展之后,entry2的长度超出来254字节,会导致entry3也出现变更的情况。最坏情况下,如果每个节点都是类似于entry1和entry2的情况,redis需要不断地对压缩列表执行空间重新分配操作(ziplist.c下的__ziplistCascadeUpdate函数,while循环节点,每个节点都会重新一次申请内存空间)。
尽管连锁更新的复杂度高,会造成性能问题,但是它出现对几率很低。
2.4 优缺点
- linkedlist的prev和next指针会占用16个字节,每个listNode内存都是单独分配,会加剧内存的碎片化。
- ziplist是一块连续内存,存储效率很高,但是不利于修改,一次realloc可能会导致大批量的数据拷贝,特别是当ziplist长度很长时,进一步降低性能。
3. Quecklist
quicklist是redis list的内部实现,是一个ziplist的双向链表:quecklist的每个节点都是一个ziplist,结合了linkedlist和ziplist的优点。
// qicklist.h
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32 bytes.
* count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
* attempted_compress: 1 bit, boolean, used for verifying during testing.
* extra: 10 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
struct quicklistNode *prev; // 指向上一个ziplist节点
struct quicklistNode *next; // 指向下一个ziplist节点
unsigned char *zl; // 数据指针,如果没有被压缩,就指向ziplist结构,反之指向quicklistLZF结构
unsigned int sz; // 表示指向ziplist结构的总长度(内存占用长度)
unsigned int count : 16; // 表示ziplist中的数据项个数
unsigned int encoding : 2; // 编码方式,1--ziplist,2--quicklistLZF
unsigned int container : 2; // 预留字段,存放数据的方式,1--NONE,2--ziplist。本来设计是用来表明一个quicklist节点下面是直接存数据,还是使用ziplist存数据,或者用其它的结构来存数据(用作一个数据容器,所以叫container)。在目前的实现中,这个值是一个固定的值2,表示使用ziplist作为数据容器。
unsigned int recompress : 1; // 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩
unsigned int attempted_compress : 1; // 测试相关
unsigned int extra : 10; // 扩展字段,暂时没用
} quicklistNode;
/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
* 'sz' is byte length of 'compressed' field.
* 'compressed' is LZF data with total (compressed) length 'sz'
* NOTE: uncompressed length is stored in quicklistNode->sz.
* When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF { // 表示一个被压缩过的ziplist
unsigned int sz; // LZF压缩后占用的字节数
char compressed[]; // 柔性数组,存放压缩后的ziplist字节数组
} quicklistLZF;
/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
quicklistNode *head; // 指向quicklist的头部节点
quicklistNode *tail; // 指向quicklist的尾部节点
unsigned long count; // 列表中所有数据项的个数总和
unsigned int len; // 所有ziplist的个数总和
int fill : 16; // ziplist大小限定,由list-max-ziplist-size给定
unsigned int compress : 16; // 节点压缩深度设置,由list-compress-depth给定
} quicklist;
到底一个quicklist节点包含多长的ziplist合适呢?比如,同样是存储12个数据项,既可以是一个quicklist包含3个节点,而每个节点的ziplist又包含4个数据项,也可以是一个quicklist包含6个节点,而每个节点的ziplist又包含2个数据项。
这又是一个需要找平衡点的难题。我们只从存储效率上分析一下:
- 每个quicklist节点上的ziplist越短,则内存碎片越多。内存碎片多了,有可能在内存中产生很多无法被利用的小碎片,从而降低存储效率。这种情况的极端是每个quicklist节点上的ziplist只包含一个数据项,这就蜕化成一个普通的双向链表了。
- 每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大。有可能出现内存里有很多小块的空闲空间(它们加起来很多),但却找不到一块足够大的空闲空间分配给ziplist的情况。这同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实蜕化成一个ziplist了。
可见,一个quicklist节点上的ziplist要保持一个合理的长度。那到底多长合理呢?这可能取决于具体应用场景。实际上,Redis提供了一个配置参数list-max-ziplist-size,就是为了让使用者可以来根据自己的情况进行调整。
我们来详细解释一下这个参数的含义。它可以取正值,也可以取负值。
当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。
当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,每个值含义如下:
- -5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
- -4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
- -3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
- -2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
- -1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
另外,list的设计目标是能够用来存储很长的数据列表的。比如,Redis官网给出的这个教程:Writing a simple Twitter clone with PHP and Redis,就是使用list来存储类似Twitter的timeline数据。
当列表很长的时候,最容易被访问的很可能是两端的数据,中间的数据被访问的频率比较低(访问起来性能也很低)。如果应用场景符合这个特点,那么list还提供了一个选项,能够把中间的数据节点进行压缩,从而进一步节省内存空间。Redis的配置参数list-compress-depth
就是用来完成这个设置的。
这个参数表示一个quicklist两端不被压缩的节点个数。注:这里的节点个数是指quicklist双向链表的节点个数,而不是指ziplist里面的数据项个数。实际上,一个quicklist节点上的ziplist,如果被压缩,就是整体被压缩的。
参数list-compress-depth
的取值含义如下:
- 0: 是个特殊值,表示都不压缩。这是Redis的默认值。
- 1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
- 2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
- 3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。
- 依此类推…
由于0是个特殊值,很容易看出quicklist的头节点和尾节点总是不被压缩的,以便于在表的两端进行快速存取。
Redis对于quicklist内部节点的压缩算法,采用的LZF——一种无损压缩算法。
// server.h
/* List defaults */
#define OBJ_LIST_MAX_ZIPLIST_SIZE -2
#define OBJ_LIST_COMPRESS_DEPTH 0
以上内容参考自:
《redis设计与实现》
《Redis源码剖析系列》
《Redis内部数据结构详解系列》
《可能是目前最详细的Redis内存模型及应用解读》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。