redis 底层数据结构简介

一、字符串

Redis 的字符串叫着「SDS」,也就是 Simple Dynamic String。它的结构是一个带长度信息的字节数组。

capacity 表示所分配数组的长度,len 表示字符串的实际长度

Redis 的字符串有两种存储方式,在长度特别短时,使用 emb 形式存储 (embeded),当 长度超过 44 时,使用 raw 形式存储。

两种形式的存储方式

embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的

为什么分界线是 44 呢 64-16-3-1

RedisObject 对象头需要占据 16 字节的存储空 间,SDS 对象头的大小是capacity+3,至少是 3。意味着分配一个字符串的最小空间占用为 19 字节

为了能容纳一个完整的 embstr 对象,jemalloc 最少会分配 32 字节的空间,如果字符 串再稍微长一点,那就是 64 字节的空间。如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式

留给 content 的长度最多只有 45(64-19) 字节了。字符串又是以\0 结尾,所以 embstr 最大能容纳的字符串长度就是 44

 

二、字典

除了 hash 结构的数据会用到字典外,整个 Redis 数据库的所有 key 和 value 也组成了一个全局字典

结构

dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。但是在

dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存

储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的 hashtable 被删除,新的

hashtable 取而代之

通过分桶的方式解决 hash 冲突。第一维是数组,第二维是链表

 

三、压缩列表

zset 和 hash 容器对象在元素个数较少的时候,采用压

缩列表 (ziplist) 进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任

何冗余空隙

压缩列表为了支持双向遍历,所以才会有 ztail_offset 这个字段,用来快速定位到最后一 个元素,然后倒着遍历

struct entry {

int<var> prevlen; // 前一个 entry 的字节长度 int<var> encoding; // 元素类型编码

optional byte[] content; // 元素内容

}

prevlen 字段表示前一个 entry 的字节长度,当压缩列表倒着遍历时,需要通过这 个字段来快速定位到下一个元素的位置

encoding 字段存储了元素内容的编码类型信息,ziplist 通过这个字段来决定后面的 content 内容的形式

 

四、快速列表

考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的

指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管

理效率。后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist

quicklist 是 ziplist 和 linkedlist 的混合体,它 将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串 接起来。

 

quicklist 内部默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个 ziplist。ziplist 的长度由配置参数 list-max-ziplist-size 决定

 

五、跳跃列表

zset 的内部实现是一个 hash 字典加一个跳跃列表 (skiplist)。hash 结构在讲字典结构时 已经详细分析过了,它很类似于 Java 语言中的 HashMap 结构。本节我们来讲跳跃列表

 

上图就是跳跃列表的示意图,图中只画了四层,Redis 的跳跃表共有 64 层,意味着最

多可以容纳 2^64 次方个元素。每一个 kv 块对应的结构如下面的代码中的 zslnode 结构,kv

header 也是这个结构,只不过 value 字段是 null 值——无效的,score 是

Double.MIN_VALUE,用来垫底的。kv 之间使用指针串起来形成了双向链表结构,它们是

有序 排列的,从小到大。不同的 kv 层高可能不一样,层数越高的 kv 越少。同一层的 kv

会使用指针串起来。每一个层元素的遍历都是从 kv header 出发

查找

 

 

如图所示,我们要定位到那个紫色的 kv,需要从 header 的最高层开始遍历找到第一个 节点 (最后一个比「我」小的元素),然后从这个节点开始降一层再遍历找到第二个节点 (最 后一个比「我」小的元素),然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最 后一个比我「小」的元素)。

我们将中间经过的一系列节点称之为「搜索路径」,它是从最高层一直到最底层的每一 层最后一个比「我」小的元素节点列表。

插入

首先我们在搜索合适插入点的过程中将「搜索路径」摸出来了,然后就可以开始创建新 节点了,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节 点通过前向后向指针串起来。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需 要更新一下跳跃列表的最大高度。

删除

删除过程和插入过程类似,都需先把这个「搜索路径」找出来。然后对于每个层的相关 节点都重排一下前向后向指针就可以了。同时还要注意更新一下最高层数 maxLevel。

更新

假设这个新的score 值不会带来排序位置上的改变,那么就不需要调整位置,直接修改元素的 score 值就可以了。但是如果排序位置改变了,那就要调整位置,就是先删除这个元素,再插入这个元素,需要经过两次路径搜索

元素排名rank

那这个 rank 是如何算出来的?如果仅仅使用上面的结构,rank 是不能算出来的。

Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 span 属

性,span 是「跨度」的意思,表示从前一个节点沿着当前层的 forward 指针跳到当前这个节

点中间会跳过多少个节点。Redis 在插入删除操作时会小心翼翼地更新 span 值的大小

 

六、紧凑列表

listpack,它是对 ziplist 结构的改进,在存储空间上会更加节省,而且结构上也比 ziplist 要精简

struct lpentry { int<var> encoding;

optional byte[] content;

int<var> length; }

元素的结构和 ziplist 的元素结构也很类似,都是包含三个字段。不同的是长度字段放在 了元素的尾部,而且存储的不是上一个元素的长度,是当前元素的长度。正是因为长度放在 了尾部,所以可以省去了 zltail_offset 字段来标记最后一个元素的位置,这个位置可以通过 total_bytes 字段和最后一个元素的长度字段计算出来

级联更新

listpack 的设计彻底消灭了 ziplist 存在的级联更新行为,元素与元素之间完全独立,不 会因为一个元素的长度变长就导致后续的元素内容会受到影响

 

七、基数树Radix Tree

Rax 是 Redis 内部比较特殊的一个数据结构,它是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操作

zset 则是按照 score 进 行排序的。

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值