redis数据结构的整理

目录

一:String类型

二: ZipList

三: Quicklist

四: skipList

五: Zset


一: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 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 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(log⁡N) 期望时间、 O(N)O(N) 最坏时间内根据 score 对 member 进行定位(被很多底层函数使用);
  • 范围性查找和处理操作,这是(高效地)实现 ZRANGE 、 ZRANK 和 ZINTERSTORE 等命令的关键。

通过同时使用字典和跳跃表, 有序集可以高效地实现按成员查找和按顺序查找两种操作。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值