目录
一:String类型
数据结构
底层是由SDS实现的。
struct sdshdr{ //记录buf数组中已经使用字节的数量 //等于SDS所保存字符串的长度 int len; //记录buf数组中未使用字节的数量 int free; //字节数组,用于保存字符串 char buf[]; };
SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,以及添加空字符到字符串末尾等操作,都是SDS函数自动完成的,所以这个空字符对于SDS的使用者来说是完全透明的。
空间预分配
空间预分配用于优化 SDS 的字符串增长操作: 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。注意,第一次创建的时候并不会预分配空间。
其中, 额外分配的未使用空间数量由以下公式决定:
- 如果对 SDS 进行修改之后, SDS 的长度(也即是
len
属性的值)将小于1 MB
, 那么程序分配和len
属性同样大小的未使用空间, 这时 SDSlen
属性的值将和free
属性的值相同。 举个例子, 如果进行修改之后, SDS 的len
将变成13
字节, 那么程序也会分配13
字节的未使用空间, SDS 的buf
数组的实际长度将变成13 + 13 + 1 = 27
字节(额外的一字节用于保存空字符)。 - 如果对 SDS 进行修改之后, SDS 的长度将大于等于
1 MB
, 那么程序会分配1 MB
的未使用空间。 举个例子, 如果进行修改之后, SDS 的len
将变成30 MB
, 那么程序会分配1 MB
的未使用空间, SDS 的buf
数组的实际长度将为30 MB + 1 MB + 1 byte
。
二: ZipList
ziplist 的设计目标是为了 节约内存,而链表的各项之间需要使用指针连接起来,这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存,这与 ziplist 的设计初衷不符。ziplist 实际上是一块连续的内存,是一个特殊的双向链表,特殊之处在于:没有维护双向指针,prev、next,而是存储了上一个 entry 的长度和当前 entry 的长度,通过长度推算下一个元素。
- 列表 zlbytes 属性的值为 0x50(十进制 80),表示压缩列表的总长为 80 字节。
- 列表 zltail 属性的值为 0x3c(十进制 60),这表示如果我们有一个指向压缩列表起始地址的指针 p,那么只要用指针 p 加上偏移量 60,就可以计算出表尾节点 entry3 的地址。
- 列表 zllen 属性的值为 0x3(十进制 3),表示压缩列表包含三个节点。
- zlend代表结束。
一个entry的表示:
previous_entry_length
字段表示前一个元素的字节长度,占 1 个或者 5 个字节:- 当前一个元素的长度小于 254 字节时,用 1 个字节表示;
- 当前一个元素的长度大于或等于 254 字节时,用 5 个字节来表示。而此时
previous_entry_length
字段的第一个字节是固定的 0xFE(十进制为 254),后面 4 个字节才真正表示前一个元素的长度。 - 假设已知当前元素的首地址为 p,那么
p-previous_entry_length
就是前一个元素的首地址,从而实现压缩列表从尾到头的遍历。
encoding
字段表示当前元素的编码,记录了节点的 content 字段所保存数据的类型以及长度:- 1 字节、2 字节或者 5 字节长,值的最高位为 00、01 或者 10 的是字节数组编码:这种编码表示节点的 content 属性保存着字节数组,数组的长度由编码除去最高两位之后的其他位记录;
- 1 字节长,值的最高位以 11 开头的是整数编码:这种编码表示节点的 content 字段保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录;
content
字段存储节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的 encoding 属性决定。
三: Quicklist
ziplist 切割大小
既然 quicklist 本质上是将 ziplist 连接起来,那么每个 ziplist 存放多少的元素,就成为了一个问题。
太小的话起不到应有的作用,极致小的话(为 1 个元素), 快速列表就退化成了普通的链表。
太大的话性能太差,极致大的话(整个快速列表只用一个 ziplist), 快速列表就退化成了 ziplist.
quickli 内部默认定义的单个 ziplist 的大小为 8k 字节. 超过这个大小,就会重新分配一个 ziplist 了。这个长度可以由参数list-max-ziplist-size来控制。
quicklist 压缩
默认情况下,list-compress-depth
参数为0,也就是不压缩数据;当该参数被设置为1时,除了头部和尾部之外的结点都会被压缩;当该参数被设置为2时,除了头部、头部的下一个、尾部、尾部的上一个之外的结点都会被压缩;当该参数被设置为2时,除了头部、头部的下一个、头部的下一个的下一个、尾部、尾部的上一个、尾部的上一个的上一个之外的结点都会被压缩;以此类推。
四: skipList
五: Zset
添加第一个元素到空 key
时, 程序通过检查输入的第一个元素来决定该创建什么编码的有序集。如果第一个元素符合以下条件的话, 就创建一个 REDIS_ENCODING_ZIPLIST
编码的有序集:
- 服务器属性
server.zset_max_ziplist_entries
的值大于0
(默认为128
)。 - 元素的
member
长度小于服务器属性server.zset_max_ziplist_value
的值(默认为64
)。
否则,程序就创建一个 REDIS_ENCODING_SKIPLIST
编码的有序集。
对于一个 REDIS_ENCODING_ZIPLIST
编码的有序集, 只要满足以下任一条件, 就将它转换为 REDIS_ENCODING_SKIPLIST
编码:
ziplist
所保存的元素数量超过服务器属性server.zset_max_ziplist_entries
的值(默认值为128
)- 新添加元素的
member
的长度大于服务器属性server.zset_max_ziplist_value
的值(默认值为64
)
用zipList实现的分析:
虽然元素是按 score
域有序排序的, 但对 ziplist
的节点指针只能线性地移动, 所以在 REDIS_ENCODING_ZIPLIST
编码的有序集中, 查找某个给定元素的复杂度为 O(N)O(N) 。
每次执行添加/删除/更新操作都需要执行一次查找元素的操作, 因此这些函数的复杂度都不低于 O(N)O(N) , 至于这些操作的实际复杂度, 取决于它们底层所执行的 ziplist
操作。
当使用 skipList 实现分析:
typedef struct zset { // 字典 dict *dict; // 跳跃表 zskiplist *zsl; }
通过使用字典结构, 并将 member
作为键, score
作为值, 有序集可以在 O(1)O(1) 复杂度内:
- 检查给定
member
是否存在于有序集(被很多底层函数使用); - 取出
member
对应的score
值(实现 ZSCORE 命令)。
另一方面, 通过使用跳跃表, 可以让有序集支持以下两种操作:
- 在 O(logN)O(logN) 期望时间、 O(N)O(N) 最坏时间内根据
score
对member
进行定位(被很多底层函数使用); - 范围性查找和处理操作,这是(高效地)实现 ZRANGE 、 ZRANK 和 ZINTERSTORE 等命令的关键。
通过同时使用字典和跳跃表, 有序集可以高效地实现按成员查找和按顺序查找两种操作。