首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Redis底层数据结构

Redis底层数据结构

原创
作者头像
Eulogy
发布2025-08-12 14:26:41
发布2025-08-12 14:26:41
14300
代码可运行
举报
文章被收录于专栏:笔记本笔记本
运行总次数:0
代码可运行

Redis底层数据结构

动态字符串SDS

Redis中的key都是字符串,而value往往是字符串或者是字符串的集合(List、hash里面保存的还是字符串)。可见字符串是Redis中最常见的数据结构。

Redis虽然是用C语言来实现的,但是并没有直接使用C语言中的字符串,因为C语言字符串有很多问题:

  • 获取字符串长度需要运算 (末尾有一个\0来标识字符串的结尾,不直接保存字符串的长度)
  • 非二进制安全 (不能包含特殊字符,其中如果保存\0,读取这个字符串就会出错)
  • 不可修改

所以Redis就构建了一个新的字符串结构,称为 简单动态字符串(Simple Dynamic String) ,简称是SDS。

Redis是C语言实现的,其中SDS是一个结构体,源码如下:

代码语言:c
代码运行次数:0
运行
复制
struct __attribute__ ((__packed__)) sdshdr8 { 
    uint8_t len; /* buf已保存的字符串字节数,不包含结束标示*/
    uint8_t alloc; /* buf申请的总的字节数,不包含结束标示*/
    unsigned char flags; /* 不同SDS的头类型,用来控制SDS的头大小 */
    char buf[];
};

还分别由有sdshdr5,sdshdr16,sdshdr32,sdshdr64。代表的是len这个字段的大小,意味着这个字符串的长度大小。flags属性就是标识是哪种头类型。例如,一个包含字符串“name”的sds结构如下:

SDS数据结构本身保存了长度,所以不需要计算就知道长度,也不会有二进制安全问题,同时是可以动态扩容,可修改的。

如果要追加字符串,如果内存不够,那么首先会先申请内存空间,由于申请内存空间的操作是很耗时的,涉及到内核态和用户态的转换,所以我们会预先申请比较多的内存。 内存预分配 如下

  • 如果新字符串小于1M,新空间为扩展后字符串长度的两倍+1,
  • 如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。

IntSet

顾名思义,IntSet是一个整数集合,是Redis中set集合的一种实现方式,基于整数数组来实现,具有长度可变、有序等特征。

代码语言:c
代码运行次数:0
运行
复制
// 整数集合结构体
typedef struct intset {
    uint32_t encoding;  // 编码方式,决定存储的整数类型
    uint32_t length;    // 集合中元素的数量
    int8_t contents[];  // 柔性数组,存储实际的整数数据
} intset;

// 编码方式定义(根据整数大小选择不同类型,节省空间)
#define INTSET_ENC_INT16 (sizeof(int16_t))   // 2字节,范围:-32768~32767
#define INTSET_ENC_INT32 (sizeof(int32_t))   // 4字节,范围:-2147483648~2147483647
#define INTSET_ENC_INT64 (sizeof(int64_t))   // 8字节,范围:-9223372036854775808~9223372036854775807

contents是指针,指向了IntSet用来保存数的数组中第一个元素的地址。而encoding决定了每个数的大小。

为了方便查找,Redis会兼顾IntSet中所有的整数按照升序依次保存在contents数组中(保存指针)。结构如图:

固定每个元素的encoding方式,方便我们可以通过数组角标寻址。

IntSet的编码方式会 自动升级 ,如果当前IntSet中的元素现在是int16_t的编码方式,如果加入了一个50000的元素,这个元素超出了int16_t的范围,那么IntSet就会自动将所有元素的编码方式都升级为int32_t,每个元素就变成了4字节。然后 倒序 地将数组中现有的元素拷贝到扩容后地正确位置,因为如果正序地话,因为元素占用字节扩充了,会往后延申,会覆盖掉后面的元素,倒序不会出现 数据覆盖 的问题。最后将待添加的元素加入数组末尾。

底层使用 二分查找 方式来查询。

Dict

Redis是键值型的数据库,我们可以根据键快速查找到对应的值,并且快速地进行增删改查。键和值地映射关系就是通过Dict这个数据结构来实现的。

Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)。

这里的字典其实就很类似Java中的HashMap,键值对保存在Entry里(哈希节点),键和值的类型一般都是 指针 ,指向 SDS对象 ,然后用数组(哈希表)来保存哈希节点,如果出现哈希冲突了,就使用拉链法也就是链表把冲突的哈希节点串起来保存。哈希表扩容的长度一定要是2^n次方长度,因为可以直接使用&运算来计算出Entry所在的桶位置。

Dict就是类似HashMap,其中有两个哈希表,主要就是使用其中一个,另外一个是rehash的时候使用的。

当哈希表中的元素越来越多的时候,哈希冲突也会很多,就会导致链表越来越长,查询效率就会很低。

Dict在每次新增键值对的时候都会检查 负载因子 (LoadFactor = used/size),满足以下两种情况时就会触发 哈希表扩容:

  • 哈希表的LoadFactor >= 1,并且服务器没有指向BGSAVE 或者 BGReWRITEAOF等进程。
  • 哈希表的 LoadFactor >5。

除了扩容之外,每次删除元素的时候,也会对负载因子进行检查,当LoadFactor<0.1的时候,就会做哈希表收缩。

rehash 不管是扩容还是收缩,一定会创建一个新的哈希表,导致哈希表的size和sizemask变化,而key的查询与sizemask有关。因此必须要对哈希表中的每一个key重新计算索引,插入到新的哈希表中,这个过程叫做 rehash 。还记得我们之前说过的每个dict都有两个哈希表吗,其中一个哈希表就是当前使用的,另外一个哈希表就是用来做扩容用的,将正在使用的哈希表中的所有entry都rehash到另外一个哈希表中,然后再把rehash后的哈希表复制回正在使用的哈希表,再把用来rehash的哈希表变为空。所以dict中两个哈希表的作用就是一个是用来 正常使用 的,另外一个是用来做 rehash 使用的。

上面对rehash的概括是粗略的,真实的reash为了保证Redis的性能(避免Redis的主线程阻塞)并不是一次性完成的,而是 渐进式 地,分多次完成的,也叫做 渐进式rehash:

  1. 计算新hash表的size,值取决于当前要做的是扩容还是收缩:。如果是扩容,则新size为第一个大于等于dict.ht0.used + 1的2ⁿ 。如果是收缩,则新size为第一个大于等于dict.ht0.used的2ⁿ(不得小于4)
  2. ②按照新的size申请内存空间,创建dictht,并赋值给dict.ht1
  3. 设置dict.rehashidx = 0,标示开始rehash
  4. 将dict.ht0中的每一个dictEntry都rehash到dict.ht1
  5. 每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于-1,如果是则将dict.ht0.tablerehashidx的entry链表rehash到dict.ht1,并且将rehashidx++。直至dict.ht0的所有数据都rehash到dict.ht1
  6. 将dict.ht1赋值给dict.ht0,给dict.ht1初始化为空哈希表,释放原来的dict.ht0的内存
  7. 将rehashidx赋值为-1,代表rehash结束
  8. 在rehash过程中,新增操作,则直接写入ht1,查询、修改和删除则会在dict.ht0和dict.ht1依次查找并执行。这样可以确保ht0的数据只减不增,随着rehash最终为空 。

ZipList

ZipList是一种特殊的 “双端链表” ,说它是特殊的链表,因为它不是链表,链表的内存空间往往是不连续的,且每个节点都会保存指针,指向下一个或者上一个节点,很浪费内存。而ZipList由一系列特殊编码的 连续内存块 组成。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1)。

ZipList的Entry并不像普通链表那样记录前后节点的指针,记录两个指针的话会占用16个字节,那样会很占用内存,ZipList的Entry由自己独特的结构:

previous_entry_length: 前一节点的长度,占1个或5个字节。如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值;如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据

encoding: 编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节

contents: 负责保存节点的数据,可以是字符串或整数

这样,我们可以很轻松地知道当前这个Entry的长度,就可以顺序遍历。借助previous_entry_length,我们可以很轻松地知道前一个Entry的长度,然后去进行逆序遍历。

ZipList的Entry没有前后节点指针可以节省很多内存空间,连续存储的特性可以加快访问速度。但是有一个问题,就是连续存储的内存是很稀有的,内存往往是碎片化的,所以ZipList比较适合存储比较少的数据。

QuickList

那么既然内存中连续内存比较稀有,ZipList只适合存储少量的数据,那么当我们需要存储大量数据的时候应该怎么做呢?我们可以创建多个ZipList来分片存储数据,然后把这些多个ZipList连起来就行了。

QuickList就是这么做的,它是一个双端链表,只不过链表中的每个节点都是一个ZipList。QuickList本身的双端链表是有前后指针的,就类似普通的双端链表了。

为了避免 QuickList 中的每个 ZipList 中 entry 过多,Redis 提供了一个配置项: list-max-ziplist-size 来限制。

  • 如果值为正,则代表 ZipList 的允许的 entry 个数的最大值。
  • 如果值为负,则代表 ZipList 的最大内存大小,分 5 种情况: ① -1: 每个 ZipList 的内存占用不能超过 4kb ② -2: 每个 ZipList 的内存占用不能超过 8kb ③ -3: 每个 ZipList 的内存占用不能超过 16kb ④ -4: 每个 ZipList 的内存占用不能超过 32kb ⑤ -5: 每个 ZipList 的内存占用不能超过 64kb

QuickList 的特点:

  • 是一个节点为 ZipList 的双端链表
  • 节点采用 ZipList,解决了传统链表的内存占用问题
  • 控制了 ZipList 大小,解决连续内存空间申请效率问题
  • 中间节点可以压缩,进一步节省了内存

SkipList

ZipList和QuickList都可以说是“双端链表”,那么我们知道双端链表是没办法做随机读取的,我们想要读取某个数据,只能顺序遍历然后取出该数据。那么我们需要随机读取怎么办呢,SkipList就可以实现这个功能。

SkipList(跳表) 首先是链表,但与传统链表相比有些差异:

  • 元素按照升序排列存储。
  • 节点可能包含多个指针,指针跨度不同。

SkipList 的特点:

  • 跳跃表是一个双向链表,每个节点都包含 score 和 ele 值
  • 节点按照 score 值排序,score 值一样则按照 ele 字典排序
  • 每个节点都可以包含多层指针,层数是 1 到 32 之间的随机数
  • 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
  • 增删改查效率与红黑树基本一致,实现却更简单

RedisObject

上面介绍的SDS、IntSet、Dict、ZipList、QuickList、SkipList都是底层数据结构。Redis中的任意数据类型的键和值都会被封装成一个RedisObject,也叫做Redis对象。

代码语言:c
代码运行次数:0
运行
复制
typedef struct redisObject {
    unsigned type:4;           // 数据类型(4位):OBJ_STRING, OBJ_LIST, OBJ_HASH, 等
    unsigned encoding:4;       // 编码方式(4位):OBJ_ENCODING_RAW, OBJ_ENCODING_INT, 等
    unsigned lru:LRU_BITS;     // LRU 时间戳或 LFU 计数器(24位,取决于配置)
    int refcount;              // 引用计数,用于内存回收
    void *ptr;                 // 指向实际数据的指针(如 SDS、字典、跳表等)
} robj;

// 数据类型定义
#define OBJ_STRING 0    // 字符串
#define OBJ_LIST 1      // 列表
#define OBJ_SET 2       // 集合
#define OBJ_ZSET 3      // 有序集合
#define OBJ_HASH 4      // 哈希

// 编码方式定义(部分)
#define OBJ_ENCODING_RAW 0        // 原始字符串(SDS)
#define OBJ_ENCODING_INT 1        // 整数(直接存储在 ptr 中,不使用 SDS)
#define OBJ_ENCODING_HT 2         // 哈希表(字典)
#define OBJ_ENCODING_ZIPMAP 3     // 压缩映射(已废弃)
#define OBJ_ENCODING_LINKEDLIST 4 // 双向链表(列表的旧编码)
#define OBJ_ENCODING_ZIPLIST 5    // 压缩列表
#define OBJ_ENCODING_INTSET 6     // 整数集合
#define OBJ_ENCODING_SKIPLIST 7   // 跳表(有序集合)
#define OBJ_ENCODING_EMBSTR 8     // 嵌入式字符串(短字符串优化)
#define OBJ_ENCODING_QUICKLIST 9  // 快速列表(列表的新编码)
#define OBJ_ENCODING_STREAM 10    // 流

数据类型

编码方式

OBJ_STRING

int、embstr、raw

OBJ_LIST

LinkedList 和 ZipList (3.2 以前)、QuickList(3.2 以后)

OBJ_SET

intset、HT

OBJ_ZSET

ZipList、HT、SkipList

OBJ_HASH

ZipList、HT

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis底层数据结构
    • 动态字符串SDS
    • IntSet
    • Dict
    • ZipList
    • QuickList
    • SkipList
    • RedisObject
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档