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

本文深入解析Redis如何通过内存管理优化,如使用哈希表、链表、压缩列表等数据结构,解决数据库性能瓶颈,以及SDS和字典等核心组件的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

思维导图:

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

我是redis

你好,我是 redis

一个叫Antirez的男人带我来到这个充满复杂的世界上。

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

聊到我的出生,那跟MYSQL大哥脱不了关系呀,我是来帮助他的,所谓天降猛男redis就是我了,真想对他说:

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

我还没有来到这个世界上的时候,刚开始挺好的,互联网前期,咱们的用户请求也不多,主要是一些静态网站和小游戏,这 有啥难的 ,MYSQL大哥一个顶俩好吧。但天有不测风云,历史不会停止步伐的。用户请求的数据也随之暴涨,每天用户的 每一次请求 都变成了对它的一个又一个的 读写操作 ,MYSQL直呼“快杀了我吧”,每年的“618”或者过年买票的时候,都是MYSQL受苦受累的日子啊!

反思 问题 出现在哪里,MYSQL大哥开始头头分析起来,原来有一大半的用户请求tm都是 读操作 ,而且经常又是查一个东西,简直浪费我 磁盘IO 的时间。后来我的爸爸突发奇想,你看看别人 CPU的缓存 是很快的呀,给数据库加个缓存就解决了呗,于是我就出生了!出生没几天,我就来给MYSQL大哥解决问题,效果也还不错,我们也成为了 好基友 ,常常手拉手在后端服务器中配合,大致如图:

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

为了方便与内存对接,我支持好几种数据结构的存储:

String

Hash

List

Set

SortedSet

Bitmap

......

因为把MYSQL里 登记的数据 都放到我的内存里来,就不去执行慢如蜗牛的I/O操作,所以下次找我肯定比找MYSQL要 省去 不少的时间呢。

可别小瞧这一个微小的操作,可帮MYSQL大哥减轻了不小的负担!我缓存的数据也逐渐增长,有

相当部分

的时间都给他挡住了用户请求,这下他可就清闲自在了!

那今天就给大家聊一聊,咱的看家本领,内存是如何 管 上面的数据类型的。说着,我拿笔给大家 画 一张图:

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

看看,我的肚子里用了一个redisObject对象来展示所有的 value和key 。type就是value对象是 那种 数据类型,第二个encoding就是不同的数据类型在Redis内部是如何 放 的。

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

譬如: type=String 则表示value存储的是一个普通字符串,那我推断出 encoding 可以是raw或者是int。但是我稍加思索,哎,还要解释下 底层数据结构 ,你知道吗?底层的底层,你有了解过吗?跟着我摆起来哈。

下面是各自对应的数据结构;

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

可以看出来,有很多类型都是使用 两种结构 ,就像set在元素比较少的时候,且都是数字的情况下会用 整数列表 ,随着数据增加就变成 哈希表 。但是我有一个比较大的疑问,这些类型都对应不同的数据结构,想想redis 整体的结构

是啥样的呢?再举个例子,现在有一个

key对应的value

是我们的list类型的,那就对应着上面的双向链表啊?那咱第一步就要找到这个key,就可找到双向链表了噻;

看看全局哈希表

其实啊,我这里是有一个大的哈希表的,在 你们的眼 里可能就是类似的长数组,在它上面的每个空间,都可以看做一个哈希桶,长这样:

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

每一个key经过我肚子里面的hash算法就会给你算出一个位置,怎么来算请看下面:

哈希算法:Redis计算哈希值和索引值方法如下:

//1、使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
//2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值
index = hash & dict->ht[x].sizemask;

那它就对应一个哈希桶呗。 壮士请注意 ,这个过程是很快的哈。只有O(1)哦,这个过程咱就算出 内存地址 ,那你就直接去找就行了。然后我们把这个key放到hash桶里面去。记住啊壮士,这个桶里面 保存的是引用 哈,不是你的 那个对象(狗头) 。就好比哈希桶里面保存的是key家的门牌号一样(实质内存地址),那我们就用了0(1)的复杂度就这样找到这个key。,因为用key去查找value的时候,只需要在经过依次hash能马上找到哈希桶,就能定位到这个 键值对 的内存地址了。

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

那问题来了

hash算法是不能保证 所有的key 经过算法出来的值都一样,那就是会有哈希冲突的存在,就是 两个key放到了同一个桶 中,这可怎么办呢?

我们就用 链表 来解决这个问题,就是两个在一个桶中的元素,我们就用一个next指针把它们 连在一起 ,经过hash算出来之后找到一个键值对, 对比看着 如果不是,根据next指针再找下一个比就行。我们都知道链表是一种常用的数据结构,而C语言内部是 没有内置 这种数据结构的实现,所以我redis会自己构建了 链表 的实现。

其中每个

链表节点

使用一个listNode结构表示:(下图的右部分):

//链表节点
typedef struct listNode{
     struct listNode * prev;
     struct listNode * next;
     void * value;  
}

下图是有两部分,由list和listNode两个数据结构构成。一部分是“ 统筹部分 ”是左边,一部分是“ 具体实施 “:右边;

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

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层。

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

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

只剩下

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

整数集合

用于保存 整数值 的集合抽象数据类型,它可以保存类型为int16_t、int32_t 或者int64_t 的整数值,并且保证集合中 不会出现重复 元素。定义如下:

typedef struct intset{
     //编码方式
     uint32_t encoding;
     //集合包含的元素数量
     uint32_t length;
     //保存元素的数组
     int8_t contents[];
 
}intset;

整数集合的每个元素都是 contents 数组 的一个数据项,它们按照从小到大的 顺序排列 ,并且不包含任何重复项。

length属性记的是 contents数组的大小。

需要注意的是虽然 contents 数组声明为 int8_t 类型,但是实际 上contents数组并不保存 任何 int8_t 类型的值 ,其真正类型有 encoding 来决定。

①、升级

当我们新增的元素类型比原集合元素类型的长度要大时,需要对 整数集合 进行升级,才能将新元素放入整数集合中。具体步骤:

1、根据新元素类型,扩展整数集合底层数组的大小,并为 新元素 分配空间。

2、将底层数组现有的所有元素都转成与新元素相同类型的元素,并将 转换后的元素 放到正确的位置,放置过程中,维持整个元素 顺序 都是有序的。

3、将新元素添加到整数集合中是有序的呀!

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

升级能极大地节省内存。

②、降级

整数集合不支持降级操作,一旦对数组进行了升级, 编码 就会一直保持升级后的状态

总结

大多数情况下,Redis使用 简单字符串SDS作为字符串 的表示,相对于C语言字符串,SDS具有常数复杂度获取字符串长度,杜绝了 缓存区的溢出 ,减少了修改字符串长度时所需的 内存重分配次数 ,以及二进制安全能存储各种类型的文件,并且还兼容部分C函数。

通过为链表设置不同类型的特定函数,Redis链表可以保存各种 不同类型的值 ,除了用作列表键,还在发布与订阅、慢查询、监视器等方面发挥作用(后面会介绍)。

Redis的字典底层使用 哈希表实现 ,每个字典通常有两个哈希表,一个平时使用,另一个用于rehash时使用,使用 链地址法 解决哈希冲突。

跳跃表通常是有序集合的底层实现之一,表中的节点按照 分值大小 进行排序。

整数集合是 集合键 的底层实现之一,底层由数组构成, 升级特性 能尽可能的节省内存。

压缩列表是Redis为 节省内存 而开发的顺序型数据结构,经常作为列表键和哈希键的底层实现。

原文链接
https://mp.weixin.qq.com/s?__biz=MzAwMDg2OTAxNg==&mid=2652049856&idx=1&sn=05d77ad89bb84bf55cea9e17e6192a04&utm_source=tuicool&utm_medium=referral

### Flink Exactly-Once Semantics Explained In the context of stream processing, ensuring that each record is processed only once (exactly-once) without any loss or duplication becomes critical for applications requiring high accuracy and reliability. For this purpose, Apache Flink implements sophisticated mechanisms to guarantee exactly-once delivery semantics. #### Importance of Exactly-Once Processing Exactly-once processing ensures every message is consumed precisely one time by downstream systems, preventing both data loss and duplicate records[^3]. This level of assurance is particularly important when dealing with financial transactions, billing information, or other scenarios where even a single error can lead to significant issues. #### Implementation Mechanisms To achieve exactly-once guarantees, Flink employs several key technologies: 1. **Checkpointing**: Periodic snapshots are taken across all operators within a job graph at consistent points in time. These checkpoints serve as recovery states which allow jobs to resume from these saved positions upon failure. 2. **Two-phase commit protocol**: When interacting with external systems like databases or messaging queues through sinks, Flink uses an extended version of the two-phase commit transaction mechanism. During checkpoint creation, pre-commit actions prepare changes; after successful completion of the checkpoint process, global commits finalize those operations[^4]. ```mermaid graph LR; A[Start Transaction] --> B{Prepare Changes}; B --> C(Pre-Commit); C --> D{All Pre-commits Succeed?}; D -->|Yes| E(Global Commit); D -->|No| F(Abort); ``` This diagram illustrates how the two-phase commit works during sink operations. Each operator prepares its part before confirming globally whether everything has been successfully prepared. Only then does it proceed with committing or aborting based on consensus among participants. #### Barrier Insertion & Propagation For maintaining consistency between different parts of computation while taking periodic snapshots, barriers play a crucial role. They act as synchronization markers inserted into streams periodically according to configured intervals. As they propagate along with events throughout the topology, they ensure that no new elements enter until previous ones have completed their respective stages up till the barrier point. ```mermaid sequenceDiagram participant Source participant OperatorA participant OperatorB Note over Source: Time advances... Source->>OperatorA: Data Element 1 Source->>OperatorA: Checkpoint Barrier X Source->>OperatorA: Data Element 2 OperatorA->>OperatorB: Forwarded Elements + Barrier X Note right of OperatorB: Process pending items\nbefore handling next element post-barrier ``` The sequence above shows how barriers travel alongside regular data flow but enforce order so that computations remain synchronized despite asynchronous nature inherent in distributed environments. --related questions-- 1. What challenges arise when implementing exactly-once semantics in real-world applications? 2. How do checkpointing frequencies impact performance versus fault tolerance trade-offs? 3. Can you explain what happens if some nodes fail midway through a two-phase commit operation? 4. Are there alternative methods besides using barriers for achieving similar levels of consistency? 5. In practice, under what circumstances might at-least-once be preferred over exactly-once semantics?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值