基于redis5.0的版本。
redis链表(List)字符编码有:ziplist和quicklist,老版本也有linkedlis。html
3.2
版本以后列表再也不使用linkedlist,这里列出来是为了后面的对比。node
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; }
StringObject就是type为string的RedisObject对象,在本文中都简称为StringObject。
// 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 从而知道是否已经到达列表的末尾。 |
下图实例:
说明:git
entry:github
prevrawlen记录前一个节点的长度,属性的长度是1字节或者5字节。redis
由于prevrawlen记录了前一个节点的长度,因此程序能够经过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。压缩列表的从表尾到表头遍历操做就是使用这一原理。算法
encoding记录的是data数据的类型和长度(github的ziplist.c上有详细说明)。数组
4.当最高位为11时,encoding长度为1字节,data保存当是整数值:数据结构
例如:函数
当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循环节点,每一个节点都会从新一次申请内存空间)。
尽管连锁更新的复杂度高,会形成性能问题,可是它出现对概率很低。post
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要保持一个合理的长度。那到底多长合理呢?这可能取决于具体应用场景。实际上,Redis提供了一个配置参数list-max-ziplist-size,就是为了让使用者能够来根据本身的状况进行调整。
咱们来详细解释一下这个参数的含义。它能够取正值,也能够取负值。
当取正值的时候,表示按照数据项个数来限定每一个quicklist节点上的ziplist长度。好比,当这个参数配置成5的时候,表示每一个quicklist节点的ziplist最多包含5个数据项。
当取负值的时候,表示按照占用字节数来限定每一个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,每一个值含义以下:
另外,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是个特殊值,很容易看出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内存模型及应用解读》