前言
Redis 是一种非关系类型数据库,以(k, v)的形式储存数据信息。由于读写速度很快,常被应用于缓存方向。Redis 使用对象来代表数据库中的键和值。Redis 有 5 种数据对象,分别是字符串对象( String 对象)、列表对象( List )、哈希对象( Hash )、集合对象、以及有序集合对象(Sorted Set)五种。其 key 值的形式都是使用的字符串形式,value 的形式可以是上面五种对象中的任意一种。 Redis 对象内存结构如图1所示:
这里 type 代表对象的种类。encoding 代表这个对象的编码形式。ptr 代表着指向储存数据的指针,针对不同的数据对象,有着不同的编码形式,其底层的数据结构也是不尽相同的。下图为 Redis 数据对象编码方式与数据结构的对应关系。
1、字符串对象
Redis 的键对象都是字符串对象。并且值也可以使用字符串对象创建。根据编码形式的不同,分为 int 编码,raw 编码以及 embstr 三种编码形式。
1.1 int编码
当 Redis 的对象中的值储存整数值时,其使用的便是 int 编码。其数据直接储存在内存当中,无需特殊的数据结构储存数据。
1.2 raw编码
Raw 编码底层的数据结构使用的是 SDS(简单动态字符串)结构。Redis 底层是用 c 语言实现的。但是在实现字符串对象的数据结构时,并没有使用简单的字符串的数据结构形式。而是使用 SDS 的结构进行储存。相比较普通字符串,SDS 对象结构如下所示
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
这里 len 用来记录数据空间已经使用的长度,free 表示数据空间剩余的长度。buf 用来表示储存数据的数据空间。由此对象可知,SDS 结构的好处有:
1、获取对象长度的时间负杂度为 O(1)。
2、减少更改字符串时内存的重新分配次数 。SDS 在进行更改操作时,会进行预检查,查看剩余空间是否足够,如不够的话,会进行扩展,然后进行字段的拼接或者其它操作。并且由于含有 len 字段以及 free 字段。在进行内存的重新分配,一般来说。进行 n 次字符串的增长会进行n次的扩容,这里使用 SDS 扩容的次数哦可以小于 n 次。
3、能够保存特殊的字符。普通字符串由于需要判断字符串截止位置,会以第一个出现空字符的位置认为是字符串的截止位置。所以只能用来保存文本数据,这里 SDS 记录的字符串的使用长度 len,因此就算数据中含有空字符,仍然是可以储存的。
1.3 embstr 编码
同 raw 一样,embstr 编码底层储存数据的结构也是 SDS 形式。但是两者储存字符串的长度不同,并且内存的分配策略也有所不同,对于 embstr 使用于储存小于32字节的字符串值,分配一个连续的空间用来储存 redisObject 结构与数据结构,如下图所示:
而对于 raw 来说,需要进行两次内存分配,创建对象时需要为 redisObject 以及 SDS 分别分配一块儿内存,两者使用 ptr 进行链接
这里,当 int 编码加入非整数字符或者 embstr 的编码长度增加超过 32 字节时,会转换为 raw 编码。
2、列表对象
列表对象有两种数据储存的结构,其中 zipList 编码的底层数据结构是压缩列表,而 linklist 编码使用饿底层数据结构为链表。
2.1 zipList 编码
使用压缩列表的前提条件:一是所有字符串长度都小于 64 字节,二是元素数量小于 512,这两个条件可以在 Redis 的配置文件中修改, list-max-ziplist-value 选项和 list-max-ziplist-entries 选项。
压缩列表是 Redis 为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型( sequential )数据结构。一个压缩列表可以包含任意多个节点( entry ),每个节点可以保存一个字节数组或者一个整数值,如下图。
示例:
这里列表总长为 0x5c(80),初始节点与末尾节点的偏移量为 0x3c(60).当我们知道初始节点的地址 p 时,就可以计算出末尾节点的地址为 p+60。
2.2 linkedlist 编码
当 Redis 需要储存的对象不满足压缩列表所要求的对象时,此时使用 linkedlist 编码形式储存对象,linkedlist 底层使用的是双端链表。每个节点都会储存一个字符串对象,而每个字符串对象中都储存了一个列表元素,如图所示
图1.7 linkedlist 对象结构
双端列表是由多个链表节点构成,节点含有指向前方的指针 prev 以及指向后方的节点 next
图8 双端链表对象结构
对外提供节点值的复制,释放以及对比函数来设置类型特定的函数。使用列表对象适用于做简单的消息队列功能;最新消息排行等功能。
3、哈希对象
3.1 zipList 编码
当每一个对象的大小都比较小并且整体的个数不大的情况下,可以使用压缩列表的方式来实现 Redis 的 hash 对象的储存如下图所示:
整体组成结构和上边列表相似,只不过压缩列表相邻位置以 key-value 形式来储存对象
3.2 hashtable 编码
hashtable 编码方式底层使用字典的方式进行储存数据,其中 hash 表使用 dict 结构定义
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}
其中 table 指向 dictEntry 数组,数组中储存的 key 以及 value 的值,但是我们存入时并不是简单的将 key 存入对应的位置。这里会通过 hash 算法将字符串转换为对应的 hash 值,然后储存的 dictentry 对应的位置上。这里,如果两个 key 的 hash 值相同时,这里会以链表的形式将两个 key 值链接起来,如下图所示:
hash表节点解决hash冲突
3.2 rehash 操作
当 hash 表保存的键值对不断增加或者减少,为了让哈希表的负载因子维持在一个合理的范围之内,这时候就需要通过 rehash 操作完成
这里,当 hash 表里的内容超过负载因子之后,会进行 rehash 操作,对 hash 表进行扩展,扩展规则:
1、ht(1) 的大小是超过 h(0) 大小的第一个 2^n 次幂。就比如现在 h(0) 是 4 时,大于 4 的第一个 2^n 为 8,这时候 h(1) 的大小就会扩容成 8.
2、此时将 h(0) 的数据转移到 h(1) ,对应位置重新通过 hash 值进行计算
3、最后将 h(0) 释放,h(1) 设置成为 h(0), 为 h(1) 分配一个空白的 hash 表
4、集合对象
4.1 Intset
当需要储存的底层储存对象都是整数,并且整体个数小于 512 (此值可以通过修改Redis配置文件中的 et-max-intset-entries 修改)时,此时使用的是 intset 编码,ptr 指向整数集合当中
4.2 hashTable
集合对象也可以使用 hashtable 的结构进行储存,与前面 hash 对象不同的是,这里只是使用了键来保存集合对象,key 值指向 null.
5、有序集合对象
5.1 ziplist
当有序集合对象需要储存的数据所有元素长度小于 64 字节,并且元素个数小于 128 个时,使用压缩列表来储存有序集合对象,这里两个相邻的数据分别储存元素值以及对应的分值,分支小的靠近表头,分支大的靠近表尾,这样的储存形式保证了数据的有序性。
5.2 skiplist
skiplist 编码方式是将字典的编码方式与跳跃表的方式相结合,字典在上面已经提到了,有点是查询的负责度为 O(1) ,但是保证不了有序,这里就要引入跳跃表来实现集合的顺序性。跳跃表拥有指向头部以及尾部的指针,以及元素个数的 length 和表示最高层高的 level。下面代表一个节点的数据结构
typedef struct zskiplistNode{
//每一层带有两个属性 前进指针 跨度
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}
level[];
//后退指针 BW
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
}
从结构来看,跳表有着前进指针以及后退指针,并且针对前进指针有着对应的跨度。节点对象包含分值,节点按照分支的大小从小向大排序。这里使用两种结构来保存数据,这里分值和成员都是共享的,指针指向统一地址,因此不会占用内存。
总结
Redis 提供了多种数据类型用来支持不同的业务场景,比如微博粉丝的关注列表、排行榜等。不同的数据类型适用于不同的场景。本文讲解了 Redis 的数据对象以及其底层实现的数据结构。感受一下 Redis 对于内存以及查询速度进行的设计以及优化形式。
参考文献
《Redis设计与实现(第二版)》
推荐阅读
招贤纳士
政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。
如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注