42张图,带你真正搞懂redis数据类型的底层

本文详细介绍了C字符串SDS获取长度的优化,以及Redis中压缩表、哈希表、整数集合和跳跃表等数据结构的内存管理、实现原理和操作特点,展示了如何通过这些数据结构实现高效的内存使用和数据存储。
摘要由CSDN通过智能技术生成

C字符串

SDS

获取字符串长度的复杂度为O(N)

获取字符串长度的复杂度为O(1)

API 是不安全的,可能会造成缓冲区溢出

API 是安全的,不会造成缓冲区溢出

修改字符串长度N次必然需要执行N次内存重分配

修改字符串长度N次最多执行N次内存重分配

只能保存文本数据

可以保存二进制数据和文本文数据

可以使用所有<String.h>库中的函数

可以使用一部分<string.h>库中的函数

压缩表

===

压缩表是一种Redis用来 节省内存 的一系列特殊编码的 顺序性连续存储 的表结构,我们知道数组这种数据结构,每一个空间的大小都是一样的,这样我们存储较小元素节点的时候就会造成 内存的浪费 ,而压缩链表可以做到每一个元素的节点的 大小都不一样 。当一个哈希键只含少量键值对,并且每个键值对的键和值也是小整数值或者长度比较短的字符串的时候,Redis就采用压缩列表做底层实现;

长这样:

42张图,带你真正搞懂redis数据类型的底层

图里 entry 的结构是这样的:

42张图,带你真正搞懂redis数据类型的底层

previous_entry_length属性以字节为单位,记录了压缩列表中 前一个节点的长度 。该属性的长度可以是1字节或者是5字节。如果前一个节点的长度 小于

254 字节,那么该属性长度为1字节,保存的是小于 254的值。那如果前一节点的长度

大于等于

254字节,那么长度需要为5字节,属性的第一字节会被设置为0xFE (254)之后的4个字节保存其长度。

参数:

===

zlbytes :4 字节。记录整个压缩列表占用的内存字节数,在内存重分配或者计算 zlend 的位置时使用。

zltail :4 字节。记录压缩列表表尾节点记录压缩列表的起始地址有多少个字节,可以通过该属性直接确定表尾节点的地址,无需遍历。

zllen :2 字节。记录了压缩列表包含的节点数量,由于只有2字节大小,那么小于65535时,表示节点数量。等于 65535 时,需要遍历得到总数。

entry :列表节点,长度不定,由内容决定。

zlend

:1字节,特殊值 0xFF ,来标记压缩列表的结束。

压缩列表节点保存的是 一个字节数组 或者 一个整数值 :字节数组可以是下列值:

  • 长度小于等于 2^6-1 字节的字节数组

  • 长度小于等于 2^14-1 字节的字节数组

  • 长度小于等于 2^32-1 字节的字节数组整数可以是六种长度;

  • 4 位长,介于 0 到 12 之间的无符号整数

  • 1 字节长的有符号整数

  • 3 字节长的有符号整数

  • int16_t 类型整数

  • int32_t 类型整数

  • int64_t 类型整数

元素的遍历

=====

先找到列表尾部元素:

42张图,带你真正搞懂redis数据类型的底层

然后再根据 ziplist 节点元素中的 previous_entry_length 属性,来逐个来遍历:

42张图,带你真正搞懂redis数据类型的底层

连锁更新

====

再次看看 entry元素的结构,有一个 previous_entry_length 字段,它的长度要么都是1个字节,要么都是5个字节:

  • 前一节点的长度小于 254 字节,则 previous_entry_length长度为1字节

  • 前一节点的长度小于 254 字节,则 previous_entry_length长度为5字节假如现在有一组压缩列表,长度都在250字节至253字节之间,突然新增一新节点 new, 长度大于等于254字节,会出现:

42张图,带你真正搞懂redis数据类型的底层

g3

程序需要 不断 的对压缩列表进行 空间重分配 工作,直到结束。

除了增加操作,删除操作也有可能带来“连锁更新”。请看下面这张图, ziplist 中所有 entry 节点的长度都在250字节至253字节之间,big节点长度大于254字节,small节点小于254字节:

42张图,带你真正搞懂redis数据类型的底层

在我看来,压缩列表实际上 类似于一个数组 ,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在 表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的entry数;压缩列表在表尾还有一个 zlend ,表示列表结束罢了。

字典

==

又叫 符号表 或者关联数组、或映射(map),是一种用于 保存键值对 的抽象数据结构。字典中的每一个键key都是唯一的,通过key可以对值来进行 查找或修改 。C语言中没有内置这种数据结构的实现,所以字典依然是 Redis自己实现的;示例:

redis > SET msg “hello world”

OK

(“msg”,“hello world”)这个就是字典;

哈希表结构:

======

typedef struct dictht{

//哈希表数组

dictEntry **table;

//哈希表大小

unsigned long size;

//哈希表大小掩码,用于计算索引值

//总是等于 size-1

unsigned long sizemask;

//该哈希表已有节点的数量

unsigned long used;

}dictht

哈希表是数组(表)table 组成,table里面每个元素都是指向 dict.h 或者 dictEntry 这个结构,dictEntry 结构:

typedef struct dictEntry{

//键

void *key;

//值

union{

void *val;

uint64_tu64;

int64_ts64;

}v;

//指向下一个哈希表节点,形成链表

struct dictEntry *next;

}dictEntry

哈希表就略微有点复杂。哈希表的制作方法一般有两种,一种是: 开放寻址法 ,一种是拉链法。redis的哈希表的制作使用的是 拉链法 。

整体结构如图:

=======

42张图,带你真正搞懂redis数据类型的底层

哈希1

也是分为两部分:左边橘黄色部分和右边蓝色部分,同样,也是”统筹“和”实施“的关系。具体 哈希表的实现 ,都是在 蓝色部分 实现的,好!先来看看蓝色部分:

42张图,带你真正搞懂redis数据类型的底层

这也会分为左右两边“统筹”和“实施”的两部分。

右边部分很容易理解:就是通常用 拉链表实现 的哈希表的样式;数组就是bucket,一般不同的key首先会定位到不同的bucket,若key重复,就用链表把 冲突的key串 起来。

新建key的过程:

=========

42张图,带你真正搞懂redis数据类型的底层

哈3

假如重复了:

======

42张图,带你真正搞懂redis数据类型的底层

哈4

rehash

======

再来看看哈希表总体图中左边橘黄色的“统筹”部分,其中有两个关键的属性:ht和 rehashidx。ht是一个 数组 ,有且只有俩元素ht[0]和ht[1];其中,ht[0]存放的是 redis中使用的哈希表 ,而ht[1]和rehashidx和哈希表是有 rehash 有关系的。

rehash指的是 重新计算 键的哈希值和索引值,然后将键值对 重排 的过程。

加载因子

====

(load factor)=ht[0].used/ht[0].size;

扩容和收缩标准

=======

扩容:

===

  • 没有 执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于 等于1。

  • 正在执行 BGSAVE和BGREWRITEAOF指令的情况下,哈希表的加载因子大于 等于5 。

收缩:

===

  • 加载因子小于0.1时,程序自动开始对哈希表进行收缩操作;

扩容和收缩的数量;

=========

扩容:

===

第一个大于等于 ht[0].used*2的 2^n(2的n次方幂);

收缩:

===

第一个大于等于 ht[0].used的 2^n(2的n次方幂)。(以下部分属于细节分析,可以跳过直接看扩容步骤)

对于收缩,我当时陷入了疑虑:收缩标准是加载因子 小于0.1 的时候,也就是说假如哈希表中有4个元素的话,哈希表的长度只要大于40,就会进行收缩,假如有一个长度大于40,但是 存在的元素为4 即( ht[0].used为4)的哈希表,进行收缩,那收缩后的值为多少?

我想了一下:按照前文所讲的内容,应该是4。但是,假如是4, 存在和收缩后的长度相等 ,是不是又该扩容?

假如收缩后 长度为4

,不仅不会收缩,甚至还会

报错

42张图,带你真正搞懂redis数据类型的底层

我们回过头来再看看设定:题目可能成立吗?哈希表的扩容都是 2倍增长的 ,最小是4, 4 ===》 8 ====》 16 =====》 32 ======》 64 ====》 128

也就是说:不存在长度为 40多的情况,只能是64。但是如果是64的话,64 X 0.1(收缩界限)= 6.4 ,也就是说在减少到6的时候,哈希表就会收缩,会缩小到多少呢?是8。此时,再继续减少到4,也不会再收缩了。所以,根本不存在一个长度大于40,但是存在的元素为4的哈希表的。

扩容步骤

====

42张图,带你真正搞懂redis数据类型的底层

收缩步骤

====

42张图,带你真正搞懂redis数据类型的底层

渐进式refresh

==========

在"扩容步骤"和"收缩步骤" 两幅动图中每幅图的 第四步骤 “将ht[0]中的数据利用哈希函数 重新计算 ,rehash到ht[1]”,并不是一步完成的,而是分成N多步,循序渐进地完成的。因为hash中有可能存放几千万甚至上亿个key,毕竟Redis中每个 hash中可以存 2^32-1 键值对 (40多亿),假如一次性将这些键值rehash的话,可能会导致服务器在一段时间内停止服务,毕竟哈希函数就得 计算一阵子呢 (对吧(#.#))。

哈希表的refresh是分多次、渐进式进行的。

=======================

渐进式refresh和下图中左边橘黄色的“统筹”部分中的rehashidx密切相关:

  • rehashidx 的数值就是现在rehash的 元素位置

  • rehashidx 等于-1的时候说明没有在进行refresh

甚至在进行期间,每次对哈希表的增删改查操作,除了正常执行之外,还会顺带将ht[0]哈希表相关键值对 rehash 到ht[1]。

以扩容步骤 举例 :

42张图,带你真正搞懂redis数据类型的底层

intset

======

整数集合是 集合键 的底层实现方式之一。

42张图,带你真正搞懂redis数据类型的底层

跳表

==

跳表这种数据结构长这样:

42张图,带你真正搞懂redis数据类型的底层

redis中把跳表抽象成如下所示:

42张图,带你真正搞懂redis数据类型的底层

看这个图,左边“统筹”,右边实现。

统筹部分 有几点要说:

===========

  • header: 跳表表头

  • tail:跳表表尾

  • level:层数最大的那个节点的层数

  • length:跳表的长度

实现部分有以下几点说:

===========

  • 表头:是链表的哨兵节点,不记录主体数据。是个 双向链表分值 是有顺序的

  • o1、o2、o3是节点所保存的成员,是一个指针,可以指向一个SDS值。

  • 层级高度最高是32。没每次创建一个新的节点的时候,程序都会 随机生成 一个介于1和32之间的值作为level 数组的大小 ,这个大小就是“高度”

redis五种数据结构的实现

==============

redis对象

=======

redis中并没有 直接使用 以上所说的那种数据结构来实现键值数据库,而是基于一种对象,对象底层再 间接的引用 上文所说的 具体 的数据结构。

就像这样:

42张图,带你真正搞懂redis数据类型的底层

字符串

===

42张图,带你真正搞懂redis数据类型的底层

其中:embstr和raw都是由 SDS动态字符串 构成的。唯一区别是: raw 是分配内存的时候,redis object和sds各分配一块内存,而embstr是redisobject和raw 在一块儿内存 中。

列表

==

42张图,带你真正搞懂redis数据类型的底层

列表

hash

====

42张图,带你真正搞懂redis数据类型的底层

哈希

set

===

42张图,带你真正搞懂redis数据类型的底层

set

set

===

42张图,带你真正搞懂redis数据类型的底层

zset

跳跃表

===

是一种 有序 数据结构,它通过在 每个节点 中维持 多个指向其它节点 的指针,从而达到快速访问节点的目的。具有如下 性质 :

1、由很多层结构组成;

2、每一层都是一个有序的链表,排列顺序为由高层到底层,且至少包含两个链表节点,分别是前面的 head节点 和后面的 nil节点 ;

3、最底层的链表包含了所有的元素;

4、如果一个元素出现在某一层的链表中,那么在 该层之下 的链表也全 都会出现 (上一层的元素是当前层的元素的子集);

5、链表中的每个节点都包含两个指针,一个指向 同层的下一个链表节点 ,另一个指向 下层的同一个链表节点 ;

42张图,带你真正搞懂redis数据类型的底层

跳跃表节点定义如下:

==========

typedef struct zskiplistNode {

//层

struct zskiplistLevel{

//前进指针

struct zskiplistNode *forward;

//跨度

unsigned int span;

}level[];

//后退指针

struct zskiplistNode *backward;

//分值

double score;

//成员对象

robj *obj;

} zskiplistNode

多个跳跃表节点构成一个跳跃表:

typedef struct zskiplist{

//表头节点和表尾节点

structz skiplistNode *header, *tail;

//表中节点的数量

unsigned long length;

//表中层数最大的节点的层数

int level;

}zskiplist;

42张图,带你真正搞懂redis数据类型的底层

各个功能

====

①、搜索:从最高层的链表节点开始,如果比 当前节点要大 和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到 最底层的最后一个节点 ,如果找到则返回,反之则返回空。

②、插入:首先确定 插入的层数 ,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。当确定 插入的层数k 后,则需要将 新元素 插入到从底层到k层。

③、删除:在各个层中找到包含 指定值的节点

,然后将节点从链表中删除即可,如果删除以后

只剩下

头尾两个节点,则删除这一层。

整数集合

====

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

腾讯T3大牛总结的500页MySQL实战笔记意外爆火,P8看了直呼内行

腾讯T3大牛总结的500页MySQL实战笔记意外爆火,P8看了直呼内行
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-xgbDYQxs-1711955512888)]

[外链图片转存中…(img-hKRLGBai-1711955512889)]

[外链图片转存中…(img-Ei85NWjj-1711955512889)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

[外链图片转存中…(img-HEZKQu72-1711955512889)]

[外链图片转存中…(img-oKYN81m6-1711955512890)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值