redis中压缩列表ziplist相关的文件为:ziplist.h与ziplist.c

压缩列表是redis专门开发出来为了节约内存的内存编码数据结构。源码中关于压缩列表介绍的注释也写得比较详细。

一、数据结构

压缩列表的整体结构如下(借用redis源码注释):

 /*
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
*/

各个部分的含义:

类型 长度 用途
zlbytes uint32_t 4B ziplist总字节数,包括zlbytes
zltail uint32_t 4B 最后一个entry的偏移量
zllen uint16_t 2B entry数量
zlend uint8_t 1B ziplist固定结尾,值固定为0xFF
entry 不定 不定 ziplist的各节点,具体结构不定

关于entry,借用redis源码注释的结构改造一下:

 /*
<prevlen> <encoding> [<entry-data>]
*/

prevlen表示的是前一个entry的长度,用于反向遍历,即从最后一个元素遍历到第一个元素。因每个entry的长度是不确定的,所以要记录一下前一个entry的长度。prevlen本身的长度也是不定的,与前一entry的实际长度有关。若长度小于254,只需要1B就可以了。若实际长度大于等于254,则需要5B,第1B固定为254,后面4B存储实际长度。

encoding则与entry存储的data有关。

encoding前两位 encoding内容 encoding长度 entry-data类型 entry-data长度
00 |00pppppp| 1B string 6b能表示的数字,0~63,encoding中存储的长度为大端字节序
01 |01pppppp|qqqqqqqq| 2B string 14b能表示的数字,64~16383,encoding中存储的长度为大端字节序
10 |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| 5B string int32能表示的数字,16384~2^32-1,encoding中存储的长度为大端字节序
11 |11000000| 1B int16 2B
11 |11010000| 1B int32 4B
11 |11100000| 1B int64 8B
11 |11110000| 1B int24 3B
11 |11111110| 1B int8 1B
11 |1111xxxx| 1B xxxx在[0001,1101]之间,表示0~12的数字,存储时进行+1操作
11 |11111111| 1B End of ziplist special entry(源码注释)

如一个具体的ziplist,有两个成员“2”与“5”:

/*
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" end
*/

zlbytes值为15,表示这个ziplist总长为15B

zltail的值为12,表示最后一个entry的偏移量为12

zllen的值为2,表示一共有两个entry

第一个entry的prevlen为0。因为第一个成员之前没有其它成员了,所以是0,占1B。值为“2”,可以用数字表示,且是介于[0,12]之间,故使用1111xxxx的encoding方式,无entry-data。2的二进制编码为0010,+1后为0011,实际为11110011,即0xF3。同理,5的encoding为0xF6。做为第二个entry,其前一个entry的总长为2,故其prevlen值为2。

zlend固定是0xFF。

二、基本操作

redis中使用了大量的宏定义与函数配合操作ziplist。

1、创建

 #define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
#define ZIP_END 255 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) = ;
zl[bytes-] = ZIP_END;
return zl;
}

新创建的ziplist,没有entry,只有zlbytes、zltail、zllen与zlend:

 /*
[0b 00 00 00] [0a 00 00 00] [00 00] [ff]
| | | |
zlbytes zltail zllen end
*/

2、插入

假设有以下ziplist:

 /*
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" end
*/

要在"2"与"5"之间插入节点“3”,则:

a.获取所要插入位置当前节点“5”的prevlen=2,prevlen_size=1

若要插入的位置是end处,则取出zltail进行偏移,取到“5”节点,直接进行计算。而如果当前是个空ziplist,直接就是0了。

b.获取节点“3”的实际长度,若其为纯数字,则可以使用数字存储,节约内存。否则直接使用外部传入的,string的长度。

这里有一点:

 int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
long long value; if (entrylen >= || entrylen == ) return ;
if (string2ll((char*)entry,entrylen,&value)) {
/* Great, the string can be encoded. Check what's the smallest
* of our encoding types that can hold this value. */
if (value >= && value <= ) {
*encoding = ZIP_INT_IMM_MIN+value;
} else if (value >= INT8_MIN && value <= INT8_MAX) {
*encoding = ZIP_INT_8B;
} else if (value >= INT16_MIN && value <= INT16_MAX) {
*encoding = ZIP_INT_16B;
} else if (value >= INT24_MIN && value <= INT24_MAX) {
*encoding = ZIP_INT_24B;
} else if (value >= INT32_MIN && value <= INT32_MAX) {
*encoding = ZIP_INT_32B;
} else {
*encoding = ZIP_INT_64B;
}
*v = value;
return ;
}
return ;
}

在尝试使用数字编码的时候,如果len >= 32,则直接不尝试,并不清楚这个32是怎么来的。

本例中,“3”可以直接使用数字编码,且在[0,12]之间,故没有entry-data

c.获得本entry的总长度,即prevlen、encoding、entry-data长度和。本处为1+1=2

d.判断一下插入后,后一个entry的prevlen是否足够存储新entry的长度。新长度为2,原entry的prevlen只有1B,足够。

此处需要注意,如果原本是5B的prevlen,当前1B就足够存储,则不做任何处理,强制使用5B来存储1B能存储的数字。而如果原来是1B,当前要5B,则还需要4B空间。

e.重新分配ziplist空间。新增加的字节数,为c、d两步之和。此处只需要额外2B的空间。

分配空间后:

 /*
[11 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff] [00 ff]
| | | | | |
zlbytes zltail zllen "2" "5" end
*/

重新分配空间会自动设置zlend与zlbytes

f.将“5”及之后的节点(不包括zlend)往后移:

 /*
[11 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" "5"
*/

g.修正当前“5”所在位置的prevlen=2:

 /*
[11 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" "5"
*/

h.修改zltail:

 /*
[11 00 00 00] [0e 00 00 00] [02 00] [00 f3] [02 f6] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" "5"
*/

i.填写新entry:

 /*
[11 00 00 00] [0e 00 00 00] [02 00] [00 f3] [02 f4] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "3" "5"
*/

j.更新zllen:

 /*
[11 00 00 00] [0e 00 00 00] [03 00] [00 f3] [02 f4] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "3" "5"
*/

若在此基础上,在“3”前,插入的是一个长度为256的string X,则:

a.获取“3”的prevlen与prevlen_size

prevlen=2,prevlen_size=1

b.长度大于32,使用string进行存储,实际长度data_len=256

c.获取entry总长度

此处prevlen长度为1B,encoding长度为2B ,entry-data长度为256B,共1+2+256=259

d.判断一下插入后,后一个entry的prevlen是否足够存储新entry的长度。新长度为259,超过了254,需要5B,而原本只有1B,还差了4B。即,nextdiff=4

e.分配空间。新增加字节数为259+4=263,共280B,即0x118

分配空间后:

 /*
[0x118] [0xe] [03 00] [00 f3] [02 f4] [02 f6] [...] [ff]
| | | | | | |
zlbytes zltail zllen "2" "3" "5" 263B
4B 4B
*/

f.memmove操作

ziplist中的memmove操作:

 memmove(p+reqlen,p-nextdiff,curlen-offset-+nextdiff);

操作完之后:

 /*
[...] [00 f3] [02 f4] [02 f6] [...] [03 00] [00 f3] [02 f4] [02 f6] [ff]
| | | | | | | |
header "2" "3" "5" 255B "2" "3" "5"
10B
*/

其中header为zlbytes、zltail与tllen

其实与以下写法相同效果:

 memmove(p+reqlen+nextdiff,p,curlen-offset-+nextdiff);

这种写法操作完之后:

 /*
[0x118] [0xe] [03 00] [00 f3] [02 f4] [02 f6] [...] [02 f4] [02 f6] [ff]
| | | | | | | | |
zlbytes zltail zllen "2" "3" "5" 259B "3" "5"
4B 4B
*/

目的是一样的,把原来的节点移至正确的位置上。

g.修正当前“3”所在位置的prevlen=259,即0X103:

 /*
[0x118] [0xe] [03 00] [00 f3] [...] [FE 03 01 00 00 f4] [02 f6] [ff]
| | | | | | |
zlbytes zltail zllen "2" 259B "3" "5"
4B 4B
*/

h.此时节点"3"的长度发生变化,需要更新其后一个节点"5"的prevlen:

 /*
[0x118] [0xe] [03 00] [00 f3] [...] [FE 03 01 00 00 f4] [06 f6] [ff]
| | | | | | |
zlbytes zltail zllen "2" 259B "3" "5"
4B 4B
*/

i.修改zltail:

 /*
[0x118] [0x115] [03 00] [00 f3] [...] [FE 00 00 01 03 f4] [06 f6] [ff]
| | | | | | |
zlbytes zltail zllen "2" 259B "3" "5"
4B 4B
*/

j.填写新entry:

encoding值为:01000001 00000000 即0x4100,大端字节序

填写后:

 /*
[0x118] [0x115] [03 00] [00 f3] [02 41 00 ...] [FE 00 00 01 03 f4] [06 f6] [ff]
| | | | | | |
zlbytes zltail zllen "2" X "3" "5"
4B 4B 259B
*/

k.更新zllen:

 /*
[0x118] [0x115] [04 00] [00 f3] [02 41 00 ...] [FE 00 00 01 03 f4] [06 f6] [ff]
| | | | | | |
zlbytes zltail zllen "2" X "3" "5"
4B 4B 259B
*/

若有连续几个entry的长度在[250,253]B之间,在插入新节点后可能存在连锁更新的情况。

如以下ziplist(只保留部分entry,其余节点省略):

 /*
... [FD 40 FA ...] [FD 40 FA ...] ...
| |
E1 253B E2 253B
*/

E1的prevlen为FD,即长度为253。此时在E1之前插入一个长度为256的节点,E1需要增加prevlen的长度,从而导致E1整体长度增加。

E2的prevlen为FD,即E1的长度为253。增加4个节点之后为257,E2也需要增加prevlen的长度。

之后还可能会有E3,E4等entry需要处理,产生了连锁反应,直到到了以下情况才会停止:

i.到了zlend

ii.不需要继续扩展

iii.需要减少prevlen字节数时

连锁更新时需要多次重新分配空间,最坏情况下有n个节点的ziplist,需要分配n次空间,而每次分配的最坏情况时间复杂度为O(n),故连锁更新的最坏情况时间复杂度为O(n^2)。

3、查找

ziplist的查找过程其实是一次遍历,依次解析出prevlen、encoding与entry-data,然后根据encoding类型,决定是要用strcmp,还是直接使用数字的比较。在首次进行数字比较的时候,会把传入要查找的串,尝试一次转换成数字的操作。如果无法转换,就会跳过数字比较操作。

查找操作支持每隔几个entry才做一次比较操作。如,查找每5个entry中,值为“1”的entry。

4、删除

如有以下ziplist:

 /*
[0x118] [0x115] [04 00] [00 f3] [02 41 00 ...] [FE 00 00 01 03 f4] [06 f6] [ff]
| | | | | | |
zlbytes zltail zllen "2" X "3" "5"
4B 4B 259B
*/

删除的是节点“5”,因是最后一个节点,则只要先修改zltail:

 /*
[0x118] [0x10F] [04 00] [00 f3] [02 41 00 ...] [FE 00 00 01 03 f4] [06 f6] [ff]
| | | | | | |
zlbytes zltail zllen "2" X "3" "5"
4B 4B 259B
*/

然后resize:

 /*
[0x116] [0x10F] [04 00] [00 f3] [02 41 00 ...] [FE 00 00 01 03 f4] [ff]
| | | | | |
zlbytes zltail zllen "2" X "3"
4B 4B 259B
*/

最后修改zllen即可:

 /*
[0x116] [0x10F] [03 00] [00 f3] [02 41 00 ...] [FE 00 00 01 03 f4] [ff]
| | | | | |
zlbytes zltail zllen "2" X "3"
4B 4B 259B
*/

如果是这个ziplist:

 /*
[0x118] [0x115] [04 00] [00 41 00 ...] [FE 00 00 01 03 f4] [06 f3] [02 f6] [ff]
| | | | | | |
zlbytes zltail zllen X "3" "2" "5"
4B 4B 259B
*/

如果删除是的节点"3",则先要计算删除后,"3"节点后的"2"节点的prevlen长度是否足够,然后直接写入。此时长度不够,并不会直接重新分配空间,而是直接使用之前"3"节的最后4B空间:

 /*
[0x118] [0x115] [04 00] [00 41 00 ...] [FE 00] [FE 00 00 01 03 f3] [02 f6] [ff]
| | | | | | |
zlbytes zltail zllen X 2B "2" "5"
4B 4B 259B
*/

然后修改zltail:

 /*
[0x118] [0x113] [04 00] [00 41 00 ...] [FE 00] [FE 00 00 01 03 f3] [02 f6] [ff]
| | | | | | |
zlbytes zltail zllen X 2B "2" "5"
4B 4B 259B
*/

接着进行memmove操作:

 /*
[0x118] [0x113] [04 00] [00 41 00 ...] [FE 00 00 01 03 f3] [02 f6] [02 f6] [ff]
| | | | | | |
zlbytes zltail zllen X "2" "5" "5"
4B 4B 259B
*/

resize操作:

 /*
[0x116] [0x113] [04 00] [00 41 00 ...] [FE 00 00 01 03 f3] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen X "2" "5"
4B 4B 259B
*/

最后要更新节点"2"及其之后entry的prevlen:

 /*
[0x116] [0x113] [04 00] [00 41 00 ...] [FE 00 00 01 03 f3] [06 f6] [ff]
| | | | | |
zlbytes zltail zllen X "2" "5"
4B 4B 259B
*/

注意此时更新也是有可能产生连锁反应。

删除操作支持删除从指定位置开始,连续n个entry,操作类似。

redis 5.0.7 源码阅读——压缩列表ziplist的更多相关文章

  1. redis 5.0.7 源码阅读——整数集合intset

    redis中整数集合intset相关的文件为:intset.h与intset.c intset的所有操作与操作一个排序整形数组 int a[N]类似,只是根据类型做了内存上的优化. 一.数据结构 ty ...

  2. redis 5.0.7 源码阅读——跳跃表skiplist

    redis中并没有专门给跳跃表两个文件.在5.0.7的版本中,结构体的声明与定义.接口的声明在server.h中,接口的定义在t_zset.c中,所有开头为zsl的函数. 一.数据结构 单个节点: t ...

  3. redis 5.0.7 源码阅读——字典dict

    redis中字典相关的文件为:dict.h与dict.c 与其说是一个字典,道不如说是一个哈希表. 一.数据结构 dictEntry typedef struct dictEntry { void * ...

  4. redis 5.0.7 源码阅读——双向链表

    redis中双向链表相关的文件为:adlist.h与adlist.c 一.数据结构 redis里定义的双向链表,与普通双向链表大致相同 单个节点: typedef struct listNode { ...

  5. redis 5.0.7 源码阅读——动态字符串sds

    redis中动态字符串sds相关的文件为:sds.h与sds.c 一.数据结构 redis中定义了自己的数据类型"sds",用于描述 char*,与一些数据结构 typedef c ...

  6. redis源码之压缩列表ziplist

    压缩列表ziplist1.简介连续,无序的数据结构.压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构. 2.组成 属性 类型 长 ...

  7. Linux 0.11源码阅读笔记-文件管理

    Linux 0.11源码阅读笔记-文件管理 文件系统 生磁盘 未安装文件系统的磁盘称之为生磁盘,生磁盘也可以作为文件读写,linux中一切皆文件. 磁盘分区 生磁盘可以被分区,分区中可以安装文件系统, ...

  8. Linux 0.11源码阅读笔记-中断过程

    Linux 0.11源码阅读笔记-中断过程 是什么中断 中断发生时,计算机会停止当前运行的程序,转而执行中断处理程序,然后再返回原被中断的程序继续运行.中断包括硬件中断和软件中断,硬中断是由外设自动产 ...

  9. Linux 0.11源码阅读笔记-总览

    Linux 0.11源码阅读笔记-总览 阅读源码的目的 加深对Linux操作系统的了解,了解Linux操作系统基本架构,熟悉进程管理.内存管理等主要模块知识. 通过阅读教复杂的代码,锻炼自己复杂项目代 ...

随机推荐

  1. 图像矫正技术深入探讨(opencv)

    刚进入实验室导师就交给我一个任务,就是让我设计算法给图像进行矫正.哎呀,我不太会图像这块啊,不过还是接下来了,硬着头皮开干吧! 那什么是图像的矫正呢?举个例子就好明白了. 我的好朋友小明给我拍了这几张 ...

  2. 2020年薪30W的Java程序员都要求熟悉JVM与性能调优!

    前言 作为Java程序员,你有没有被JVM伤害过?面试的时候是否碰到过对JVM的灵魂拷问?   一.JVM 内存区域划分 1.程序计数器(线程私有) 程序计数器(Program Counter Reg ...

  3. Spring注解开发系列VII --- Servlet3.0

    Servlet3.0简介 Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布.该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用 ...

  4. java中符号类型和无符号类型的问题分析

    一 参考博文 java中无符号类型的解决方案 二 java中的无符号数和有符号数 在计算机中,可以区分正负的类型,称为有符号类型,无正负的类型,称为无符号类型. 使用二进制中的最高位表示正负 计算机中 ...

  5. Linux下安装Oracle后重启无法登录数据库ORA-01034:ORACLE not available

    Linux下安装了数据库,安装完成后可以用,今天启动就不能用了,提示Oracle not available,后来查找资料,据说是oracle服务没有打开.如下方式可以解决问题. [root@root ...

  6. (二)Mybatis动态sql

    首先动态sql简单来讲就是可以根据传入参数的不同来动态的生成sql语句,拼接where语句,这样你就不用写很多个sql语句了,并且它里面有一些特性也可以帮助你避免sql语句的拼接错误,主要分为4个: ...

  7. 真正解决百度编辑器UEditor上传图片跨域问题

    做前后端分离的项目用到UEditor,把上传图片程序拿出来放到了接口程序中,上传图片接口已经做了跨域处理,按理说编辑器中上传图片应该不会有问题.可是配置好图片上传路径后,运行,打开调试,测试一下,报错 ...

  8. 为什么尽量不要使用Executors创建线程池

    看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,通过源码分析禁用的原因. 线程池的优点 管理一组工作线程,通过线程池 ...

  9. SpringBoot Jpa 分页查询最新配置方式

    这是已经被废弃的接口 Sort sort = new Sort(Sort.Direction.DESC,"bean类中字段"); //创建时间降序排序 Pageable pagea ...

  10. 1276:【例9.20】P2758 编辑距离

    题目传送门[(https://www.luogu.com.cn/problem/P2758)] 题目描述 设A和B是两个字符串.我们要用最少的字符操作次数,将字符串A转换为字符串B.这里所说的字符操作 ...