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

head指向具体 双向链表的头

tail指向具体 双向链表的尾

len双向链表的

长度

具体"实施方":

========

可以看出每个链表节点都有指向 前置节点prev 和 后置节点next 的指针,组成一个双向链表。每个链表结构,有表头表尾指针和链表长度等信息。另外表头节点和前置和表尾节点的 后置都是NULL ,所以是无环链表。

在总结下:

特点:

===

  • 双端 :链表节点带有prev和next指针,找到某个节点的前置节点和后置节点的 时间复杂度都是O(N)

  • 无环 :表头节点的prev指针和表尾节点的next 都指向NULL,对立案表的访问时以 NULL为截止

  • 表头与表尾 :因为链表带有head指针和tail指针,程序获取链表头结点和尾节点的时间复杂度为O(1)

  • 长度计数器 :链表中存有记录链表 长度 的属性**len

  • 多态 :链表节点使用void* 指针来保存节点值,并且可以通过list结构的dup 、free、match三个属性是节点值设置类型特定函数。

这时我们可以通过直接操作list来操作链表会更加的方便:

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

typedef struct list {

//表头节点

listNode * head;

//表尾节点

listNode * tail;

//链表长度

unsigned long len;

//节点值复制函数

void *(*dup) (void *ptr);

//节点值释放函数

void (*free) (void *ptr);

//节点值对比函数

int (*match)(void *ptr, void *key);

}

那么当元素越来越多之后,一个哈希桶所 对应的链表 就会越来越长,我们知道链表上的遍历时间复杂度是O(n)的,那么会严重 影响性能 ,Redis这种追求快的数据库看来是绝对不能够 容忍 的,那么要怎么处理,就是rehash操作。

rehash和渐进式rehash操作

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

redis会在 内部再新建 一个长度为原始长度2倍的空哈希表,然后原哈希表上的元素 重新rehash 到新的哈希表中去,然后我们再使用新的哈希表即可。

那么,这样还是有个问题要解决呀!

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

要知道redis中存储的数据可能是成百万上千万的,我们 重新rehash 一次未免太耗时了吧,因为redis中操作大部分是 单线程 的。

这个过程可能会阻断其他操作很长时间,这是不能忍受的,那要怎么处理呢?

首先redis是采用了 渐进式rehash 的操作,就是会有一个变量,指向第一个哈希桶,然后redis每执行一个添加key,删除key的类似命令,就顺便 copy一个哈希桶中的数据 到新的哈希表中去,这样细水长流的操作,是不会影响什么性能,就会所有的数据都被重新hash到新的哈希表中。

那么在这个过程中,当然再有写的操作,会直接把数据放到新的哈希表中,保证旧的肯定有 copy完 的时候,如果这段时间对数据库的操作比较少,也没有关系,redis 内部也有定时 任务,每隔一段时间也会copy一次。

动态字符串SDS

========

SDS的全称"simple dynamic string"。redis中所有场景中出现的字符串,基本都是由SDS来实现的。

所有 非数字的key 。例如 set msg “hello world” 中的key msg.

字符串数据类型的值 。例如 set msg “hello world"中的msg的值"hello wolrd”

最后是两者结合:

非字符串数据类型中的“字符串值”

。例如 RPUSH fruits “apple”“banana”“cherry"中的"apple” “banana” "cherry"都是;

上面的例子,我们可以很直观地看到我们在平常使用redis的时候,创建的字符串到底是一个什么样子的数据类型。除了用来保存字符串以外,SDS还被用作 缓冲区 (buffer)AOF模块中的 AOF缓冲区 。

SDS 的定义

=======

动态字符串的结构:

=========

/*

  • 保存字符串对象的结构

*/

struct sdshdr {

// buf 中已占用空间的长度

int len;

// buf 中剩余可用空间的长度

int free;

// 数据空间

char buf[];

};

SDS长这样:

=======

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

SDS示例

  • len 变量,用于记录buf 中已经 使用的空间长度 (这里指出Redis的长度为5);

  • free 变量,用于记录buf修改后的还有 空余的空间 ,一般初次分配空间的时候,是没有空余的,在对字符串修改的时候,就会有剩余空间出现;

  • buf 字符数组,用来记录 我们的字符串 (记录Redis)

SDS 与 C 字符串的区别

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

那么传统的C字符串使用长度为 N+1的字符串数组 来表示 长度为N的字符串 ,所以为了获取一个长度为C字符串的长度,必须遍历整个字符串。

这样做在获取字符串长度的时候,字符串扩展等操作的时候 效率比较低 。C语言用这种 简单 的字符串表示方式,但是并不能满足Redis对字符串在安全性、效率以及功能方面的要求:

获取字符串长度(SDS O(1)/C 字符串 O(n))

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

和C 字符串不同,SDS的数据结构中,有专门用于 保存字符串长度 的变量,我们可以通过获取len属性的值,如下图的,直接知道字符串长度:

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

杜绝缓冲区溢出

=======

C 字符串是不会记录字符串长度的,除了获取的时候复杂度高以外,还容易导致缓冲区溢出。

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

我们现在假设程序中有两个在内存中 紧邻着的字符串s1和s2 ,其中s1保存了字符串“ redis ”,而s2 则保存了字符串“ MongoDb ”:

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

如果我们现在将s1的内容修改为 redis cluster ,但是我又忘了重新为 s1分配足够的空间 ,这时候就会出现以下问题:

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

我们可以看到,原本s2中的内容已经 被S1的给占领 了,s2现在是cluster,而不是“Mongodb”。Redis 中SDS 的 空间分配策略 完全 杜绝 了发生缓冲区溢出的可能性:

我们需要对一个SDS进行修改的时候,redis会在执行 拼接操作 之前,预先检查给定SDS空间是否是足够的,如果不够,会先 拓展SDS 的空间 ,然后再执行拼接操作;

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

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

减少修改字符串时带来的内存重分配次数

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

C字符串在进行字符串的扩充和收缩的时候,都常常会面临着 内存空间 重新分配的问题。

  • 字符串拼接会产生字符串的内存空间的 扩充 ,在拼接的过程中,原来的字符串的大小很可能 小于 拼接后的字符串的大小,那么这样的话,就会导致一旦 忘记申请 分配空间,就会导致内存的溢出。

  • 字符串在进行收缩的时候,内存空间会 相应的收缩 ,而如果在进行字符串的切割的时候,没有对内存的空间进行一个重新分配,那么这部分多出来的空间就成为了内存泄露。*

下面我们对SDS进行拓展,那就需要进行 空间的拓展 ,redis会将SDS的长度修改为13字节,并且将未使用空间同样修改成1字节;

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

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

因为在上一次修改字符串的时候已经拓展了空间,再次进行修改字符串的时候你会发现空间是足够使用,因此就不要进行空间拓展了。

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

通过这种 预分配策略 ,SDS将连续增长N次字符串所需的 内存重分配次数 从必定N次会降低为最多N次

惰性空间释放

======

我们在观察SDS的结构的时候,可以看到里面有free属性,是用于 记录空余空间 的。我们除了在拓展字符串的时候会使用到free来进行记录空余空间以外,在对字符串进行收缩的时候,我们也可以使用free 属性来进行 记录剩余空间 ,这样做的好处就是避免下次对字符串进行再次修改的时候,我们 再对 字符串的空间进行拓展。

但是,我们并不是说不能释放SDS中空余的空间,SDS 提供了相应的API,让我们可以在 有需要 的时候,会 自行释放 SDS的空余空间;

通过惰性空间释放,SDS避免了缩短字符串时所需的 内存重分配 操作,并未将来可能有的增长操作提供了优化,嗯值得点赞!

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

二进制安全

=====

强调 的是C字符串中的字符必须符合某种 编码 ,并且除了字符串的末尾之外,字符串里面不包含 空字符 ,否则最先被程序读入的空字符将被误认为是字符串结尾,那就尴尬了,这些 限制 使得C字符串只能保存文本数据,而不能保存想图片,音频,视频,压缩文件这样的 二进制 数据。

但是在Redis中,不是靠空字符来判断字符串的结束的,而是通过 len 这个属性。那么,即便是中间出现了空字符对于SDS来说,读取该字符仍然是可以的。

如这样:

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

兼容部分C字符串函数

==========

虽然SDS的API都是二进制安全的,但他们同样要遵循C字符串以 空字符串结尾 的惯例。

再次总结

====

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中把跳表抽象成如下所示:

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Docker是一种流行的容器化技术,通过轻量级、隔离性强的容器来运行应用程序。下面我将通过十张图你深入理解Docker容器和镜像。 1. 第一张图展示了Docker容器和镜像的关系。镜像是Docker的基础组件,它是一个只读的模板,包含了运行应用程序所需的所有文件和配置。容器是从镜像创建的实例,它具有自己的文件系统、网络和进程空间。 2. 第二张图展示了Docker容器的隔离性。每个容器都有自己的文件系统,这意味着容器之间的文件互不干扰。此外,每个容器还有自己的网络和进程空间,使得容器之间的网络和进程相互隔离。 3. 第三张图展示了Docker镜像和容器的可移植性。镜像可以在不同的主机上运行,只需在目标主机上安装Docker引擎即可。容器也可以很容易地在不同的主机上迁移,只需将镜像传输到目标主机并在其上创建容器。 4. 第四张图展示了Docker容器的快速启动。由于Docker容器与主机共享操作系统内核,启动容器只需几秒钟的时间。这使得快速部署和扩展应用程序成为可能。 5. 第五张图展示了Docker容器的可重复性。通过使用Dockerfile定义镜像构建规则,可以确保每次构建的镜像都是相同的。这样,可以消除由于环境差异导致的应用程序运行问题。 6. 第六张图展示了Docker容器的资源隔离性。Docker引擎可以为每个容器分配一定数量的CPU、内存和磁盘空间,确保容器之间的资源不会互相干扰。 7. 第七张图展示了Docker容器的可扩展性。通过使用Docker Swarm或Kubernetes等容器编排工具,可以在多个主机上运行和管理大规模的容器群集。 8. 第八张图展示了Docker镜像的分层结构。镜像由多个只读层组成,每个层都包含一个或多个文件。这种分层结构使得镜像的存储和传输变得高效。 9. 第九张图展示了Docker容器的生命周期。容器可以通过创建、启动、停止和销毁等命令来管理。这使得容器的维护和管理变得简单。 10. 第十张图展示了Docker容器的应用场景。Docker容器广泛应用于开发、测试、部署和运维等领域。它可以提供一致的开发和运行环境,简化了应用程序的管理和交付过程。 通过这十张图,希望能让大家更深入地理解Docker容器和镜像的概念、特性和应用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值