文章目录

Redis 速度为什么快?Redis 对象类型那么多,都是怎么实现的?今天我们就来探探 Redis 的底层数据结构,了解Redis的底层设计。废话不多说,我们来一个个看。

一、简单动态字符串(SDS, Simple Dynamic String)

1. SDS 的概念

SDS 是 Redis 中字符串的底层实现,用于替代 C 语言的传统字符串(以 \0 结尾的字符数组)。SDS 通过优化内存管理、支持二进制数据和高效操作,成为 Redis 字符串的核心数据结构。

2. SDS 的结构

Redis 的 SDS 并不是直接使用 C 语言的字符数组(char[]),而是通过一个结构体来管理字符串的元数据和实际数据。以下是 SDS 的基本结构:

2.1 SDS 基本结构

struct sdshdr {
    int len;     // 当前字符串的实际长度(不包含末尾的 '\0')
    int free;    // 当前字符串的未使用空间(剩余可追加的空间)
    char flags;    // 不同SDS的头类型,用来控制SDS的头大小
    char buf[];  // 实际存储字符串内容的字符数组(柔性数组)
};
  • len:记录 buf 数组中已使用的字节数,即字符串的实际长度。

  • free:记录 buf 数组中未使用的字节数,用于后续的字符串拼接或修改操作。

  • flags:记录 SDS 头类型,用来控制SDS的头大小。

     # flags对应的SDS头类型如下:
     #define SDS_TYPE_5  0
     #define SDS_TYPE_8  1
     #define SDS_TYPE_16 2
     #define sDS_TYPE_32 3
     #define sDS_TYPE_64 4
    
  • buf[]:字符数组,存储字符串内容,并以 \0 结尾(兼容部分 C 字符串函数)。

2.2 不同版本的 SDS 结构

Redis 根据字符串长度的不同,定义了多种 SDS 结构体(sdshdr5sdshdr64),根据字符串长度选择不同的 Header 结构,减少内存浪费:

  • sdshdr5:适用于非常短的字符串(长度小于 32 字节)。
  • sdshdr8:适用于长度小于 2^8(256)的字符串。
  • sdshdr16:适用于长度小于 2^16(65536)的字符串。
  • sdshdr32:适用于长度小于 2^32 的字符串。
  • sdshdr64:适用于超长字符串(最大支持 2^64 字节)。

这些结构体的字段类型不同,但核心思想一致:通过 lenfree 管理字符串的动态扩展。

2.3 SDS 结构图

在这里插入图片描述


3. 为什么实现 SDS 结构

Redis 底层是基于C语言开发,选择实现 SDS 而非直接使用 C 字符串,主要是为了解决以下问题:

3.1 获取字符串长度的性能问题

  • C 字符串:获取长度需要遍历字符数组直到遇到 \0,时间复杂度为 O(n)
  • SDS:通过 len 字段直接获取长度,时间复杂度为 O(1)

3.2 缓冲区溢出风险

  • C 字符串:在拼接操作(如 strcat)时,如果未分配足够的内存,可能导致缓冲区溢出,破坏相邻内存数据。
  • SDS:在 API 操作中会检查内存是否足够,自动扩容以避免溢出。

3.3 内存分配的频繁性

  • C 字符串:每次修改字符串(如拼接、截断)都可能需要重新分配内存。
  • SDS:通过 空间预分配惰性空间释放 策略,减少内存重分配的次数。

3.4 二进制安全性

  • C 字符串:以 \0 作为字符串结束标志,无法存储包含 \0 的二进制数据(如图片、视频)。
  • SDS:通过 len 字段判断字符串结束,支持存储任意二进制数据。

3.5 兼容 C 字符串函数

  • SDS:保留 buf 数组的 \0 结尾,兼容部分 C 字符串函数(如 strlenmemcpy),方便与 C 库集成。

4. SDS 的核心特性

4.1 O(1) 时间复杂度获取长度

通过 len 字段直接获取字符串长度,无需遍历字符数组。例如:

// 获取 SDS 字符串长度
size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s - sizeof(struct sdshdr));
    return sh->len;
}

4.2 空间预分配(Space Preallocation)

当字符串需要扩容时,SDS 会分配额外的未使用空间,减少后续操作的内存分配次数:

  • 小字符串(< 1MB):分配与当前长度相等的未使用空间。例如,字符串长度从 10B 扩展到 20B,free 会增加 10B。
  • 大字符串(≥ 1MB):固定分配 1MB 的未使用空间。

这种策略减少了频繁的内存分配,提高了性能。

4.3 惰性空间释放(Lazy Space Free)

当字符串被缩短时,SDS 不会立即释放多余的空间,而是通过 free 字段记录未使用空间,等待后续操作复用。例如:

// 截断字符串后,free 增加
sds s = sdsempty();
s = sdscat(s, "hello");  // len=5, free=0
s = sdstrim(s, "h");     // len=4, free=1

4.4 二进制安全

SDS 的 buf 数组以 len 字段为判断依据,而非 \0,因此可以安全存储任意二进制数据(如图片、音频)。例如:

// 存储包含 \0 的二进制数据
sds s = sdsnewlen("\x00\x01\x02", 3);  // len=3, free=0

4.5 兼容 C 字符串函数

SDS 的 buf 数组以 \0 结尾,可以直接调用部分 C 字符串函数(如 printfstrlen),但需注意:

  • strlen:返回的是 buf 中实际字符数(包含 \0),而非 len
  • strcpy:需确保目标缓冲区足够大,否则可能导致溢出。

5. SDS 在 Redis 中的应用

SDS 是 Redis 的核心数据结构之一,广泛应用于以下场景:

  1. 键值对存储:Redis 中所有字符串键和值均基于 SDS 实现。
  2. AOF 缓冲区:Redis 的 AOF(Append-Only File)持久化机制使用 SDS 缓存写入命令。
  3. 客户端输入缓冲区:客户端发送的命令和参数通过 SDS 解析和处理。
  4. 其他数据结构的底层实现:如哈希表、列表、集合等,均可能依赖 SDS 存储字符串类型的键或值。

二、链表(Linked List)

1. Linked List 的概念

Redis链表是一种基于双向无环链表结构实现的高效数据结构,支持O(1)时间复杂度的头尾节点快速访问与增删操作,通过多态设计,链表可以存储任意类型的数据(如字符串、整数等)。


2. Linked List 的结构

Redis 的链表由 listNodelist 两个结构体组成:

2.1 链表节点(listNode

typedef struct listNode {
    struct listNode *prev;  // 指向前一个节点
    struct listNode *next;  // 指向后一个节点
    void *value;            // 节点值(可以是任意类型)
} listNode;
  • prevnext:双向链表的指针,允许正向和反向遍历。
  • value:节点存储的值,使用 void* 指针实现多态,支持存储任意类型的数据(如字符串、整数、其他结构体等)。

2.2 链表结构(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); // 节点值比较函数
} list;
  • headtail:指向链表的头节点和尾节点,支持 O(1) 的头尾操作。
  • len:记录链表的节点数量,获取长度的时间复杂度为 O(1)。
  • dup:此函数用于复制链表节点所保存的值。
  • free:此函数用于释放链表节点所保存的值。
  • match:此函数则用于对比链表节点所保存的值和另一个输入值是否相
    等。

2.3 Linked List 的结构图

下图为一个list结构和三个listNode结构组成的链表结构体:

在这里插入图片描述


3. Linked List 的实现原理

Redis 的链表基于 listNodelist 结构实现,以下是关键操作的实现逻辑:

3.1 插入操作

  • LPUSH / RPUSH
    • 向链表头部(head)或尾部(tail)插入新节点。
    • 更新 headtail 指针,并增加 len 计数。
  • LINSERT
    • 在指定节点前后插入新节点,需遍历链表查找目标节点。

3.2 删除操作

  • LPOP / RPOP
    • 从链表头部(head)或尾部(tail)删除节点。
    • 更新 headtail 指针,并减少 len 计数。
  • LREM
    • 删除指定值的节点,需遍历链表并调整指针。

3.3 遍历操作

  • LRANGE
    • 返回指定范围内的节点值,需从 head 开始遍历。
  • LINDEX
    • 根据索引获取节点值,需从 headtail 遍历。

3.4 阻塞操作

  • BLPOP / BRPOP
    • 当链表为空时,客户端进入阻塞状态,等待新元素插入。
    • 实现基于 Redis 的事件驱动模型(如 I/O 多路复用)。

4. Linked List 的核心特性

Redis 链表的设计目标是高效、灵活,以下是其核心特性:

4.1 双向链表

  • 双向指针:每个节点包含 prevnext 指针,支持正向和反向遍历。
  • 无环结构:链表的头节点 prev 和尾节点 next 指向 NULL,链表访问以 NULL 为终点。

4.2 O(1) 时间复杂度操作

  • 头尾插入/删除:通过 headtail 指针,插入和删除操作的时间复杂度为 O(1)
  • 长度获取:通过 len 字段直接获取链表长度,无需遍历节点。

4.3 多态性

  • 通用值类型:节点值使用 void* 指针,支持存储任意类型的数据。
  • 自定义操作函数:通过 dupfreematch 函数,用户可以定义节点值的复制、释放和比较逻辑。

4.4 内存管理

  • 动态分配:每个节点独立分配内存,适合存储大小不一的数据。
  • 内存碎片:由于节点内存不连续,可能加剧内存碎片化,影响缓存效率。

5. Linked List 的优化

为了优化内存占用和性能,Redis 在 Redis 3.2 版本引入了 QuickList 数据结构,将链表与压缩列表(Ziplist)结合:

5.1 QuickList 的结构

  • 双向链表 + Ziplist
    • 每个链表节点(quicklistNode)存储一个 Ziplist。
    • Ziplist 是紧凑的连续内存结构,节省内存。
  • 节点压缩
    • 支持中间节点的压缩(通过 LZF 算法),进一步减少内存占用。

5.2 QuickList 的优势

  • 内存效率:Ziplist 减少了链表节点指针的内存开销。
  • 性能平衡:避免了传统链表的高内存碎片问题,同时保留双向链表的高效操作。
  • 动态调整
    • 通过配置参数(如 list-max-ziplist-sizelist-compress-depth)控制 Ziplist 的大小和压缩深度。

6. Linked List 在 Redis 中的应用

Redis 链表广泛应用于以下场景:

  1. 列表对象(List):Redis 的 LPUSHRPOP 等命令的底层实现。
  2. 发布订阅(Pub/Sub)消息队列:支持消息的快速入队和出队。
  3. 任务调度(Task Scheduling):任务按优先级或时间顺序插入链表,调度器按需执行任务(如延迟队列)。
  4. 聊天记录存储(Chat History):聊天室中,新消息追加到链表尾部,用户可读取最新的 N 条消息。
  5. 缓存最近访问记录(LRU Cache):缓存最近访问的页面或数据,使用 LRU(Least Recently Used)算法淘汰旧记录。
  6. 实现栈和队列(Stack & Queue)
    • 栈(Stack):后进先出(LIFO),通过 LPUSHLPOP 实现。
    • 队列(Queue):先进先出(FIFO),通过 RPUSHLPOP 实现。
  7. 慢查询日志(Slow Log):Redis 使用链表存储慢查询日志,便于后续分析和监控。
  8. 客户端输出缓冲区(Output Buffer):Redis 服务器使用链表构建客户端的输出缓冲区,存储待发送的响应数据。

三、字典(Dictionary)

1. Dictionary 的概念

字典是 Redis 的核心数据结构之一,用于实现键空间(Key Space)和哈希对象(Hash)。通过哈希表(dictht)和渐进式 Rehash 优化性能。


2. Dictionary 的结构

Redis 的字典由三个主要结构体组成:dictEntrydicthtdict

2.1 dictEntry(哈希表节点)

每个 dictEntry 结构体表示一个键值对(Key-Value Pair),用于解决哈希冲突。

typedef struct dictEntry {
    void *key;                // 键(可以是字符串、整数等)
    union {
        void *val;            // 值(可以是任意类型)
        uint64_t u64;         // 无符号64位整数
        int64_t s64;          // 有符号64位整数
        double d;             // 浮点数
    } v;
    struct dictEntry *next;   // 指向下一个节点的指针(解决哈希冲突)
} dictEntry;
  • key:键的指针,支持任意类型(字符串、整数等)。
  • v:值的联合体,支持多种数据类型。
  • next:指针,用于链式存储(解决哈希冲突)。

2.2 dictht(哈希表)

dictht 是哈希表的主体结构,包含一个数组和元数据。

typedef struct dictht {
    dictEntry **table;        // 哈希表数组(每个元素指向一个 dictEntry 链表)
    unsigned long size;       // 哈希表大小(数组长度)
    unsigned long sizemask;   // 大小掩码,用于计算索引,值总是等于size-1
    unsigned long used;       // 已使用的键值对数量
} dictht;
  • table:哈希表数组,每个元素是一个 dictEntry* 指针,指向链表头节点。
  • size:哈希表容量(必须是 2 的幂次)。
  • sizemask:掩码,用于快速计算键的哈希值对应的索引(index = hash & sizemask),sizemask的值总是等于size-1。
  • used:已存储的键值对数量。

2.3 dict(字典)

dict 是字典的主体结构,包含两个哈希表和相关配置。

typedef struct dict {
    dictType *type;           // 类型特定函数(如哈希函数、键值复制/释放函数)
    void *privdata;           // 私有数据(传递给类型特定函数的参数)
    dictht ht[2];             // 两个哈希表(ht[0] 和 ht[1],用于渐进式 rehash)
    long rehashidx;           // rehash 进度(-1 表示未进行 rehash)
    unsigned long iterators;  // 当前运行的迭代器数量
} dict;
  • type:指向 dictType 结构的指针,定义了字典的操作函数(如哈希函数、键值复制/释放函数)。
  • privdata:私有数据,传递给 type 中的函数。
  • ht[2]:两个哈希表,ht[0] 是当前使用的哈希表,ht[1] 用于 rehash。
  • rehashidx:rehash 进度,-1 表示未进行 rehash。
  • iterators:当前运行的迭代器数量(用于 rehash 时的并发控制)。

2.4 Dictionary 的结构图

  • 每个dict包含2个dictht,ht[0]和ht[1]。
  • 每个dictht包含一个dictEntry数组,数组内每个元素为一个dictEntry链表。
  • 每个dictEntry链表由dictEntry连接组成,每个dictEntry为一个具体的键值,以及指向下一个dictEntry的指针。

在这里插入图片描述


3. Dictionary 的哈希算法实现

3.1 MurmurHash2 算法

  • 特点
    • 速度快:MurmurHash2 是一种非加密型哈希算法,计算效率高。
    • 散列均匀:即使输入的键有规律(如连续数字),也能生成分布均匀的哈希值。
    • 32 位或 64 位输出:Redis 通常使用 64 位哈希值(在 64 位系统上)。
  • 适用场景
    • Redis 的字典(用于键值对存储)、集合(Set)、哈希(Hash)等数据类型均依赖此算法。

3.2 索引计算

Redis 通过 按位与操作 将哈希值映射到哈希表数组的索引位置:

// 根据情况不同,ht[x],可以是ht[0],或者ht[1]
index = hash & dict->ht[x].sizemask;
  • hash:键的哈希值(通过 MurmurHash2 计算)。
  • sizemask:哈希表的掩码,等于 size - 1size 必须是 2 的幂次)。
  • x:当前使用的哈希表(ht[0]ht[1],用于渐进式 rehash)。

3.3 哈希与索引计算过程

假设我们有一个键 k0,需要将其插入到 Redis 字典中。

步骤 1:计算哈希值
Redis 使用 MurmurHash2 计算键的哈希值:

hash = dict->type->hashFunction(k0);

假设 k0 的哈希值为 8

步骤 2:计算索引
哈希表的当前大小为 8,因此 sizemask = 7(即 8 - 1)。通过按位与操作计算索引:

// 根据情况不同,ht[x],可以是ht[0],或者ht[1]
index = hash & dict->ht[x].sizemask = 8 & 7 = 0;

此时,键 k0 会被映射到哈希表数组的索引 0 位置。

步骤 3:处理哈希冲突
Redis 使用 链地址法(Separate Chaining) 解决哈希冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来:

  • 当两个键的哈希值映射到同一个索引时,新的键值对会以链表形式追加到该索引对应的链表中。
  • 插入新键值对时,新节点会插入链表头部(O(1) 时间复杂度)。
  • 查找时,需遍历链表比较键是否匹配(最坏情况下 O(N) 时间复杂度)。
// 哈希表数组索引 0 的链表
dictEntry *entry = dict->ht[0].table[0];
while (entry != NULL) {
    if (dictCompareKeys(dict, entry->key, k0) == 0) {
        // 找到匹配的键
        break;
    }
    entry = entry->next;
}

4. Dictionary 渐进式 Rehash

为了避免一次性扩容或缩容导致的性能问题,Redis 采用 渐进式 rehash 策略:

  1. 触发条件
    • 插入键值对时,若负载因子(used / size)超过阈值(默认为 1),触发扩容。
    • 删除键值对时,若负载因子过低(默认为 0.1),触发缩容。
  2. rehash 步骤
    • (1)rehash初始化:计算并为ht[1]分配内存空间,让字典同时持有ht[0]和ht[1]两个哈希表。这个哈希表ht[1]的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht[0].used属性的值)。
      • 如果是扩容操作,那么ht[1]的大小为第一个大于等于ht[0].used + 1的2^n。(+1是因为当前插入一个key);
      • 如果是缩容操作,那么ht[1]的大小为第一个大于等于ht[0].used的2^n (不得小于4)。
    • (2)rehash开始:在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
    • (3)rehash执行:将保存在ht[0]中的所有键值对rehash到ht[1]上面,rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
    • (4)rehash过程中:在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,会将索引计数器变量rehashidx+1,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]。
      • 如果是新增操作,则直接写入ht[1]。
      • 如果是查询、修改和删除操作,则会在ht[0]和ht[1]依次查找并执行,确保ht[0]的数据只减不增,随着rehash最终为空。
    • (5)rehash结束:随着字典操作的不断执行,当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,最后程序将rehashidx属性的值设为-1,表示rehash操作已完成。

渐进式 rehash 的优势

  • 分散 rehash 开销,避免阻塞主线程。
  • 在迁移过程中,读写操作会同时访问 ht[0]ht[1],确保一致性。

5. Dictionary 的核心特性

  1. 高效率查找:平均时间复杂度为 O(1)。
  2. 渐进式 Rehash:减少扩容/缩容对性能的影响。
  3. 链地址法解决冲突:避免哈希冲突导致性能下降。
  4. 支持多种键值类型:键为字符串,值可以是任意 Redis 对象(如字符串、列表等)。

6. Dictionary 在 Redis 中的应用

Redis 的字典在以下场景中广泛应用:

  1. Redis 数据库的核心存储:Redis 的数据库本质上是一个字典,用于存储所有的键值对数据。
  2. 哈希(Hash)数据类型的实现:Redis 的 Hash 类型(如 HSET key field value)底层依赖字典实现。
  3. 集合(Set)和有序集合(ZSET):Redis 使用字典实现集合(Set),键为集合元素,值为 NULL(仅表示存在性);有序集合(ZSET)结合了字典和跳表(Skip List)。
  4. 过期键管理:Redis 使用字典管理键的过期时间(TTL)。每个数据库维护一个字典 expires,键是用户定义的 key,值是过期时间戳(毫秒)。
  5. Lua 脚本缓存:Redis 使用字典缓存已加载的 Lua 脚本。键是脚本的 SHA1 哈希值,值是脚本内容。
  6. 哨兵模式中的主从节点管理:在 Redis 哨兵(Sentinel)模式中,字典用于管理主从节点的状态。键是主节点的名称,值是主节点及其从节点的信息。

四、跳跃表(Skip List)

1. Skip List 的概念

跳跃表(Skip List) 是一种基于 多级链表 的概率型数据结构,通过在有序链表的基础上添加多级索引来加速查找。其核心思想是:

  • 分层索引:每一层是一个有序链表,最底层(第0层)包含所有元素,越往上层的元素越稀疏。
  • 随机化层数:每个节点的层数通过随机算法生成(如抛硬币法),保证索引分布均匀。
  • 快速跳跃:查找时从最高层开始,通过索引快速跳过大量不相关的元素,逐步缩小范围。

2. Skip List 的结构

Redis 的跳跃表通过两个核心结构体实现:zskiplistNode(跳跃表节点)和 zskiplist(跳跃表)。

2.1 节点结构(zskiplistNode

typedef struct zskiplistNode {
    sds ele;              // 成员对象(字符串)
    double score;         // 分值(用于排序)
    struct zskiplistNode *backward;  // 后退指针(指向前一个节点)
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前进指针(指向下一个节点)
        unsigned int span;              // 跨度(到下一个节点的距离)
    } level[];            // 层级数组(柔性数组)
} zskiplistNode;
  • ele:存储有序集合的成员(member)。
  • score:成员的分数(用于排序)。
  • backward:后退指针,指向当前节点的前一个节点(仅用于底层链表的反向遍历)。
  • level[]:层级数组,每个层级包含:
    • forward:指向同一层级的下一个节点。
    • span:记录到下一个节点的跨度(用于计算排名)。

2.2 跳跃表结构(zskiplist

typedef struct zskiplist {
    struct zskiplistNode *header;  // 表头节点
    struct zskiplistNode *tail;    // 表尾节点
    unsigned long length;          // 跳跃表长度(节点数量)
    int level;                     // 当前最大层级(表头节点不计入)
} zskiplist;
  • header:指向跳跃表的表头节点(始终存在,但不存储数据)。
  • tail:指向跳跃表的表尾节点。
  • length:跳跃表中实际存储的节点数量。
  • level:跳跃表当前的最高层级(用于动态调整层级)。

2.3 Skip List 的结构图

如下图,跳跃表在查询score=2.0的SDS2对象时,跳跃表从header指针指向的表头开始,level[1]沿途经过了2个span跨度为1的节点,所以目标节点在跳跃表中的排名为2。同理,level[4]沿途经过了1个跨度为3的节点,所以排名为3。

  • NULL的跨度:指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何节点。
  • 节点的后退指针(backward属性):后退指针用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。
  • 节点的分值(score属性):score是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。
  • 节点、分值与对象的关系:在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。

在这里插入图片描述


3. Skip List 的操作

3.1 插入操作

  1. 确定层级:通过随机算法(如抛硬币法)生成新节点的层级 level
  2. 查找插入位置:从最高层开始,逐层向下查找,直到找到插入位置。
  3. 更新指针:将新节点插入到对应层级的链表中,并更新前后节点的指针。
  4. 调整跨度:根据插入位置更新相邻节点的跨度(span),以支持排名计算。

3.2 查找操作

  1. 从最高层开始:沿着当前层级的 forward 指针向右移动。
  2. 比较值:若当前节点的值小于目标值,则继续向右;若大于目标值,则下降到下一层。
  3. 循环直至底层:直到在最底层找到目标节点或确认不存在。

3.3 删除操作

  1. 查找目标节点:通过查找操作定位到待删除节点。
  2. 更新指针:逐层删除节点的 forward 指针,并更新相邻节点的跨度。
  3. 清理资源:释放节点占用的内存。

4. Skip List 的随机化设计

跳跃表的层级通过 概率化算法 生成,确保索引分布均匀:

  • 随机层级生成规则
    • 最大层级 ZSKIPLIST_MAXLEVEL 默认为 32。
    • 初始层级为 1。
    • 以概率 p=0.25(默认值)决定是否增加层级:若随机数小于 p,则层级加 1,直到达到最大层级。
  • 幂次定律:越大的层级出现的概率越低(如层级 1 的概率为 100%,层级 2 为 25%,层级 3 为 6.25%,依此类推)。

这种设计使得跳跃表在平均情况下具有与平衡树(如红黑树)相当的性能,但实现更简单。

Redis 选择跳跃表而非平衡树的原因

  • 实现简单:跳跃表的代码量比平衡树少 50% 以上,维护成本低。
  • 范围查询友好:跳跃表的底层链表天然支持顺序遍历。
  • 概率化设计:通过随机层级生成,避免复杂的旋转操作。

5. Skip List 的核心特性

  1. 高效性:插入、删除、查找的时间复杂度均为 O(log N)(平均情况下)。
  2. 实现简单:相比平衡树(如红黑树),跳跃表的代码实现更简洁。
  3. 支持范围查询:通过顺序遍历底层链表,可高效处理范围操作(如 ZRANGE)。
  4. 缓存友好:链表的连续内存布局比树结构更利于 CPU 缓存命中。

6. Skip List 在 Redis 中的应用

Skip List是 Redis 有序集合(ZSet)的底层实现之一,适用于以下场景:

  1. 有序集合(ZSet)
  • ZSet 的混合结构:Redis 的有序集合由 跳跃表 + 哈希表 共同实现:
    • 跳跃表:按 score 排序,支持范围查询(如 ZRANGEZREVRANK)。
    • 哈希表:存储 member -> score 的映射,支持快速查找和更新。
  • 内存共享elescore 在跳跃表和哈希表中共享,避免冗余存储。
  1. 集群节点管理
  • Redis 集群使用跳跃表管理主从节点的状态,支持快速查找和排序。

五、整数集合(IntSet)

1. IntSet 的概念

整数集合(IntSet) 是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时(默认不超过 512 个,由 set-max-intset-entries 配置控制),Redis就会使用整数集合作为集合键的底层实现。集合内所有元素按值从小到大有序排列,且无重复


2. IntSet 的结构

2.1 IntSet 的基本结构

typedef struct intset {
    uint32_t encoding;  // 编码类型(INTSET_ENC_INT16/INTSET_ENC_INT32/INTSET_ENC_INT64)
    uint32_t length;    // 元素个数
    int8_t contents[];  // 存储元素的连续数组(实际类型由 encoding 决定)
} intset;
  • encoding:表示元素的存储类型(16位、32位或64位整数)。
  • length:记录了整数集合包含的元素数量,也即是contents数组的长度。
  • contents[]:一个连续的内存块,存储所有元素。元素按值从小到大有序排列,且无重复
    • 虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值。

2.2 IntSet 的结构图

假设现在有一个encoding=INTSET_ENC_INT16编码的整数集合,集合中包含五个int16_t类型的元素,其结构图如下:
在这里插入图片描述


3. IntSet 的核心特性

3.1 紧凑存储

  • 连续内存布局contents 是一个紧凑的数组,避免了指针开销。
  • 变长编码:根据元素的实际大小选择最小的编码方式(如小整数用 int16_t,大整数用 int64_t),节省内存。
  • 有序性:元素按升序排列,支持二分查找(时间复杂度 O(log N))。

3.2 自动升级(Upgrade)

当插入的元素类型超出当前编码范围时,IntSet 会自动升级:

  1. 升级条件
    • 插入的元素值超出了当前编码的最大范围(例如,向 int16_t 集合插入 32768)。
  2. 升级步骤
    • 扩容数组:根据新元素类型扩展 contents 的内存空间。
    • 转换类型:将原有元素转换为新类型,并重新排序以保持有序性。
    • 更新编码:修改 encoding 字段为新的类型。
  3. 示例
    // 原 IntSet 存储 int16_t(范围:-32768 ~ 32767)
    intset *is = intsetNew();
    is = intsetAdd(is, 32767, NULL);  // 正常插入
    is = intsetAdd(is, 32768, NULL);  // 触发升级到 int32_t
    

3.3 不支持降级

整数集合不支持降级操作,一旦对数组进行了升级,即使删除元素后集合满足较小编码的条件,IntSet 也不会降级。这是为了避免频繁的内存分配和数据迁移开销。


4. IntSet 在 Redis 中的应用

IntSet 是 Redis 集合(Set)的底层实现之一,适用于以下场景:

  1. 小规模整数集合
    • 当集合元素数量 ≤ 512 且全为整数时,Redis 优先使用 IntSet。
    • 比如:用户积分、排行榜、用户点赞功能(用户ID为整数)。
  2. 内存敏感场景
    • 需要极低内存占用的集合操作(如缓存高频访问的整数集合)。
  3. 触发 IntSet 转换的条件,自动切换到哈希表
    • 元素数量超过 set-max-intset-entries(默认 512)。
    • 插入非整数元素(如字符串)。

六、压缩列表(Ziplist)

1. Ziplist 的概念

压缩列表(Ziplist) 是 Redis 6.x 之前的列表(List)和哈希(Hash)对象的底层实现之一,通过紧凑的内存布局优化存储效率。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。


2. Ziplist 的结构

压缩列表(Ziplist )由 固定头 + 动态节点 + 结尾标记 组成,具体如下:

2.1 固定头(Header)

字段名 类型 长度 用途说明
zlbytes uint32_t 4字节 整个 Ziplist 占用的字节数。最大值为 2³² - 1。
zltail uint32_t 4字节 表尾节点相对于起始地址的偏移量,用于快速定位尾部节点。
zllen uint16_t 2字节 元素个数。若元素数量超过 65535,则需遍历整个 Ziplist 才能计算真实数量。

2.2 动态节点(Entry)

每个 Entry 存储实际数据,结构如下:

typedef struct zlentry {
    unsigned int prevrawlensize;  // 前驱节点长度字段占用字节数
    unsigned int prevrawlen;      // 前驱节点实际长度
    unsigned int lensize;         // 当前节点长度字段占用字节数
    unsigned int len;             // 当前节点实际长度
    unsigned int headersize;      // 节点头部总大小(prevrawlensize + lensize)
    unsigned char *encoding;      // 编码类型
    unsigned char *p;             // 节点数据起始指针
} zlentry;
  • prevrawlensizeprevrawlen:记录前一个 Entry 的长度,用于反向遍历。
    • 若前一个 Entry 长度 ≤ 254 字节,prevrawlensize 为 1 字节。
    • 若前一个 Entry 长度 ≥ 254 字节,prevrawlensize 为 5 字节(首字节为 0xFE,后 4 字节为真实长度)。
  • encoding:数据类型和长度编码,支持以下类型:
    • TINY:0~12 的无符号整数(直接存储在 encoding 的低 4 位)。
    • STRING:短字符串(长度 ≤ 63 字节)→ 1 字节编码。
    • LONG:长字符串(长度 64~4GB)→ 5 字节编码。
    • INT16/INT32/INT64:整数类型(直接存储数值)。
  • content:实际存储的数据(字符串或整数)。

2.3 结尾标记(zlend)

  • 固定值0xFF(1 字节),十进制255,标识 Ziplist 的结束。

2.4 Ziplist 的结构图

虽然定义了这个结构体,但是根本就没有使用zlentry结构体来作为压缩列表中用来存储数据节点中的结构,因为,这个结构存小整数或短字符串实在是太浪费空间了。这个结构总共在32位机占用了28个字节(32位机),在64位机占用了32个字节。这不符合压缩列表的设计目的:提高内存的利用率。因此,在redis中,并没有定义结构体来进行操作,而是定义了一些宏,压缩列表的节点真正的结构如下图ziplistEntry所示:
在这里插入图片描述

  • previous_entry_length:记录前一个 Entry 的长度,用于反向遍历。

    • 若前一个 Entry 长度 ≤ 254 字节,previous_entry_length 为 1 字节。
    • 若前一个 Entry 长度 ≥ 254 字节,previous_entry_length 为 5 字节(首字节为 0xFE,后 4 字节为真实长度)。
  • encoding:编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节。
    注:以下表格中的下划线“_”表示留空,而a、b、c、x等变量则代表实际的二进制数据,为了方便阅读,多个字节之间用空格隔开。

    • 字节数组编码

      编码 编码长度 content 属性保存的值
      00xxxxxx 1 字节 长度小于等于63字节的字节数组
      01xxxxxx bbbbbbbb 2 字节 长度小于等于16383字节的字节数组
      10______ aaaaaaaa bbbbbbbb cccccccc dddddddd 5 字节 长度小于等于4294967295字节的字节数组
    • 整数编码

      编码 编码长度 content 属性保存的值
      11000000 1 字节 int16_t类型的整数
      11010000 1 字节 int32_t类型的整数
      11100000 1 字节 int64_t类型的整数
      11110000 1 字节 24位有符号整数
      11111110 1 字节 8位有符号整数
      1111xxxx 1 字节 使用这一编码的节点没有相应的content属性,因为编码本身的xxxx四个位已经保存了一个介于0和12之间的值,所以它无需content属性
  • content:负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。


3. Ziplist 连锁更新

3.1 什么是连锁更新?

压缩列表的连锁更新是指在 Ziplist 中插入或删除元素时,由于 每个 Entry 记录前一个节点的长度(previous_entry_length,导致后续多个节点的 previous_entry_length 字段需要连续修改,从而引发多次内存重分配和数据迁移的现象。

触发场景:

  • 插入或删除元素:当某个 Entry 的长度变化后,后续所有 Entry 的 previous_entry_length 字段都需要更新。
  • 长度变化阈值:当某个 Entry 的长度从 小于 254 字节变为大于等于 254 字节(或反之),其 previous_entry_length 字段的存储方式会发生变化(1 字节 → 5 字节)。

3.2 连锁更新的触发示例

假设 Ziplist 中有多个连续的 Entry,每个 Entry 的长度为 250~253 字节previous_entry_length 用 1 字节表示):

Entry1 (250B) → Entry2 (250B) → Entry3 (250B) → ... → EntryN (250B)

插入操作触发连锁更新:

  1. 插入新 Entry:在 Entry1 前插入一个长度为 254 字节 的新 Entry(NewEntry)。
  2. Entry1 的 previous_entry_length 变化
    • 原 Entry1 的 previous_entry_length 为 1 字节(表示前一个 Entry 长度为 250B)。
    • 插入 NewEntry 后,Entry1 的 previous_entry_length 需要变为 5 字节(因为 NewEntry 长度为 254B ≥ 254)。
  3. Entry1 的长度增加previous_entry_length 从 1 字节变为 5 字节,导致 Entry1 的总长度增加 4 字节。
  4. 连锁反应:Entry2 的 previous_entry_length 需要更新以反映 Entry1 的新长度(从 1 字节变为 5 字节),进而导致 Entry2 的长度也增加,依此类推,直到最后一个 EntryN。

最终结果:

  • 所有后续 Entry 的 previous_entry_length 字段都需要修改。
  • 每次修改都需要重新分配内存并迁移数据,而每次空间重分配的最坏复杂度为 O(N) ,所以连锁更新的最坏复杂度为 O(N²)

3.3 连锁更新的影响

  • 性能下降:每次插入/删除操作可能需要多次内存重分配和数据迁移,导致操作时间显著增加。
  • 最坏情况:当 Ziplist 中有 N 个连续的 Entry 需要更新时,时间复杂度为 O(N²)(每次更新都涉及后续所有 Entry)。
  • 内存碎片:频繁的内存重分配可能导致内存碎片,降低内存利用率。

3.4 连锁更新的解决方案

为了解决 Ziplist 的连锁更新问题,Redis 引入了以下优化方案:

3.4.1 QuickList(Redis 3.2+)替代
  • 结构:QuickList 是 Ziplist 和双向链表的结合体,将 Ziplist 分片存储在多个节点中。
  • 优势
    • 避免单个 Ziplist 太大:通过限制每个 Ziplist 的最大长度(list-max-ziplist-size),减少连锁更新的可能性。
    • 链表节点独立:每个链表节点是一个独立的 Ziplist,插入/删除操作仅影响当前节点内的 Entry,不会波及整个链表。
  • 配置项
    list-max-ziplist-size = 512  # 每个 Ziplist 最多存储 512 个 Entry
    
3.4.2 ListPack(Redis 7.0+)替代
  • 改进机制:ListPack 是 Ziplist 的升级版,每个 Entry 仅记录自己的长度,而非前一个 Entry 的长度。
  • 优势
    • 消除连锁更新:修改一个 Entry 的长度不会影响后续 Entry 的存储。
    • 更高效的插入/删除:无需级联更新,时间复杂度降低到 O(1)(尾部操作)或 O(N)(中间操作,但无级联)。
  • 适用场景:Redis 7.0 开始默认使用 ListPack 替代 Ziplist,作为 List、Hash、ZSet 的底层实现。

3.5 如何避免连锁更新?

  • 配置优化:

    1. 限制 Ziplist 大小
      • 通过 list-max-ziplist-size 配置项控制每个 Ziplist 的最大长度(或内存大小)。
      • 示例
        list-max-ziplist-size = 512  # 最多存储 512 个 Entry
        
    2. 避免存储大对象
      • 避免在 Ziplist 中存储长度接近 254 字节的 Entry,减少 prevlen 字段长度变化的概率。
  • 数据结构选择:

    1. 使用 QuickList 或 ListPack:对于 List、Hash、ZSet 等数据类型,优先使用 Redis 3.2+ 的 QuickList 或 Redis 7.0+ 的 ListPack。
    2. 避免直接使用 Ziplist:如果数据量较大或频繁更新,应通过上层数据结构(如 List)间接使用 Ziplist,而非直接操作底层 Ziplist。

4. Ziplist 的核心特性

4.1 内存高效

  • 连续内存块:所有 Entry 存储在一块连续内存中,避免指针开销。
  • 变长编码:根据数据大小动态调整存储长度(如整数直接编码,字符串按长度压缩)。
  • 无冗余字段:不存储前后指针,仅通过 previous_entry_length 实现双向遍历。

4.2 双向遍历

  • 正向遍历:从 zlbytes 开始,逐个解析 Entry。
  • 反向遍历:利用 zltail 定位尾部节点,通过 previous_entry_length 反向查找前一个节点。

4.3 插入与删除的复杂性

  • 插入操作:需要重新分配内存并移动后续数据,时间复杂度为 O(N)
  • 删除操作:直接截断内存,但可能引发 连锁更新(Cascading Update)
    • 当某个 Entry 的 previous_entry_length 从 1 字节变为 5 字节时,后续所有 Entry 的 previous_entry_length 都需更新。
    • 例如:插入一个超长字符串,导致相邻节点的 previous_entry_length 变化,触发级联修改。

4.4 小端字节序

  • 所有字段(如 zlbyteszltailzllen)均采用 小端序 存储(低位字节在前)。
    • 例如:数值 0x1234 实际存储为 0x34 0x12

5. Ziplist 在 Redis 中的应用

Ziplist 是 Redis 的底层数据结构,广泛用于以下场景:

  1. 哈希表(Hash)
    • 当键值对数量 ≤ 512 且所有字段为短字符串时,Redis 使用 Ziplist 存储 Hash。
  2. 列表(List)
    • 在 Redis 3.2 之前,List 直接使用 Ziplist;3.2 之后改为 Quicklist(Ziplist + 双向链表)。
  3. 有序集合(ZSet)
    • 当元素数量 ≤ 128 且所有成员为短字符串时,ZSet 使用 Ziplist 存储。

七、ListPack

1. ListPack 的概念

ListPack 是一种紧凑且高效的序列化数据结构,专为 小规模数据集合 设计,广泛应用于 Redis 的 Hash、ZSet、List 和 Stream 等数据类型中。

它是 Redis 6.x 引入的新数据结构,逐步取代 Ziplist,在Redis 7.x版本完全替代Ziplist。相比 Ziplist,ListPack 支持更灵活的编码方式(如变长整数编码),优化内存效率和操作性能。


2. ListPack 的结构

ListPack 的结构由 头部(Header) + 动态条目(Entries) + 结尾标记(End Marker) 组成,具体如下:

2.1 头部(Header)

字段名 类型 长度 用途说明
tot-bytes uint32_t 4字节 整个 ListPack 占用的字节数。
num-elements uint16_t 2字节 元素个数。若元素数量超过 65535,则需遍历整个 ListPack 才能计算真实数量。

2.2 条目(Entry)

每个 Entry 的结构如下:

struct listpackEntry {
    unsigned int encoding;       // 编码类型(1~5字节)
    unsigned char *data;         // 实际数据(字符串或整数)
    unsigned int element_total_len; // 当前 Entry 的总长度(1~5字节)
};
  • encoding:编码类型,用于描述数据类型和长度。支持以下类型:
    注:以下表格中的下划线“_”表示留空,而a、b、c、x等变量则代表实际的二进制数据,为了方便阅读,多个字节之间用空格隔开。

    • 单字节编码

      编码 编码长度 data 属性保存的值
      0xxxxxxx 1 字节 表示7位无符号整型,后7位为数据
      10xxxxxx 1 字节 表示短字符串,后6位表示长度,最大可以表示63字节的字符串
    • 多字节编码

      编码 编码长度 data 属性保存的值
      110xxxxx bbbbbbbb 2 字节 表示13位有符号整数
      1110xxxx bbbbbbbb 2 字节 后12位表示字符串长度,最大可以表示4095字节的字符串
      11110000 aaaaaaaa bbbbbbbb cccccccc dddddddd 5 字节 后4个字节表示字符串长度,用来表示长字符串
      11110001 aaaaaaaa bbbbbbbb 3 字节 后2个字节用来表示有符号整数
      11110010 aaaaaaaa bbbbbbbb cccccccc 4 字节 后3个字节用来表示有符号整数
      11110011 aaaaaaaa bbbbbbbb cccccccc dddddddd 5 字节 后4个字节用来表示有符号整数
      11110100 aaaaaaaa bbbbbbbb cccccccc dddddddd eeeeeeee ffffffff gggggggg hhhhhhhh 9 字节 后8个字节用来表示有符号整数(最大 64 位)
      11111111 1 字节 结束标记,仅用于 ListPack 结尾
  • data:实际存储的数据(字符串或整数)。

  • element_total_len:当前 Entry 的总长度,用于反向遍历时快速定位下一个 Entry 的起始位置。

2.3 结尾标记(end)

  • 固定值0xFF(1 字节),十进制 255,标识 ListPack 的结束。

2.4 ListPack 的结构图

在这里插入图片描述


3. ListPack 的核心特性

3.1 解决连锁更新问题

  • Ziplist 的缺陷:Ziplist 中每个 Entry 记录前一个 Entry 的长度(previous_entry_length),插入或删除操作可能导致后续 Entry 的 previous_entry_length 字段连续修改(级联更新)。
  • ListPack 的改进:ListPack 直接记录当前 Entry 的长度element_total_len),修改一个 Entry 不会影响其他 Entry,从而彻底消除连锁更新问题。

3.2 内存高效

  • 紧凑编码:通过变长编码(1~9 字节)存储数据类型和长度,减少冗余。
  • 无指针开销:所有 Entry 存储在一块连续内存中,避免指针和链表节点的额外开销。
  • 小端字节序:字段存储采用小端序(低位字节在前),提升解析效率。

3.3 高性能

  • 快速插入/删除:由于无需级联更新,插入/删除操作的时间复杂度为 O(1)(尾部操作)或 O(N)(中间操作,但无级联)。
  • 双向遍历:通过 element_total_len 可快速定位前后 Entry,支持正向和反向遍历。
  • 原地更新:修改数据时,若新数据长度不超过原空间,可直接覆盖原数据区域,避免内存重分配。

3.4 灵活的数据类型支持

  • 字符串:支持长度为 1~4GB 的字符串。
  • 整数:支持 7 位、13 位、21 位、33 位、65 位等有符号整数。
  • 混合类型:同一个 ListPack 可存储多种类型的数据(字符串 + 整数)。

4. ListPack 在 Redis 中的应用

ListPack 是 Redis 的底层数据结构,广泛应用于以下场景:

  1. Hash 类型
    • 当字段数量 ≤ 128 且每个字段值 ≤ 64 字节时,Redis 使用 ListPack 存储 Hash。
  2. ZSet(有序集合)
    • 当元素数量 ≤ 128 且每个成员值 ≤ 64 字节时,Redis 使用 ListPack 存储 ZSet。
  3. List 类型
    • Redis 7.0+ 的 List 默认使用 QuickList(ListPack + 双向链表)。
  4. Stream 类型
    • Stream 的消息列表使用 ListPack 存储。

5. ListPack 与 Ziplist 的对比

特性 ListPack Ziplist
连锁更新 有(修改一个 Entry 可能引发级联更新)
内存效率 更高(无冗余字段,紧凑编码) 较高(连续内存,但存在 previous_entry_length 开销)
插入/删除性能 O(1)(尾部)或 O(N)(中间,无级联) O(N)(需级联更新)
数据类型支持 字符串、整数、混合类型 字符串、整数
适用场景 小规模数据集合(Hash、ZSet、List) 小规模数据集合(旧版 Redis 6.x)
兼容性 Redis 7.0+ 默认使用 Redis 6.x 及以下版本

八、快速链表(QuickList)

1. QuickList 的概念

QuickList 是 Redis 3.2 引入的一种混合数据结构,结合了 双向链表(Linked List)压缩列表(Ziplist) 的优点。它的设计目标是在 内存效率操作性能 之间取得平衡,特别适合实现 Redis 的 List 类型


2. QuickList 的结构

Redis 的 QuickList双向链表(quicklist)链表节点(quicklistNode)压缩列表(ziplist)LZF压缩列表(quicklistLZF) 结构体组成。

2.1 LZF压缩列表(quicklistLZF)

quicklistLZF 结构体是Redis QuickList实现压缩功能的核心组件,通过存储压缩ziplist后的数据和原始大小信息,在内存优化与访问性能之间取得了平衡。

头尾节点保留不压缩,保证高频的头尾操作(如LPUSH/RPUSH)不受压缩影响。仅在访问被压缩的中间节点时触发解压,且解压速度极快(LZF算法设计目标)。

typedef struct quicklistLZF {
    unsigned int sz;  // 原始ziplist未压缩时的总字节数
    char compressed[]; // 压缩后的字节数组(使用LZF算法)
} quicklistLZF;
  • sz:记录原始ziplist未压缩时的总字节数。在解压缩时,用于验证解压后的数据是否完整(解压后的字节数应等于sz)。
  • compressed[]:存储使用LZF算法压缩后的字节数据。

2.2 链表节点(quicklistNode)

typedef struct quicklistNode {
    struct quicklistNode *prev;   // 前驱节点指针
    struct quicklistNode *next;   // 后继节点指针
    unsigned char *zl;            // 指向ziplist或quicklistLZF的指针
    unsigned int sz;              // ziplist的总字节数(压缩前)
    unsigned int count : 16;      // ziplist中的元素数量(最多65535)
    unsigned int encoding : 2;    // 编码类型(1=RAW,2=LZF压缩)
    unsigned int container : 2;   // 容器类型(固定为2=ZIPLIST)
    unsigned int recompress : 1;  // 是否需要重新压缩
    unsigned int attempted_compress : 1; // 测试用标志
    unsigned int extra : 10;     // 扩展字段(未使用)
} quicklistNode;
  • prev:指向链表上一个节点。
  • next:指向链表下一个节点。
  • zl:若未压缩,指向ziplist结构。若压缩,指向quicklistLZF结构(包含压缩后的数据)。
  • sz:记录ziplist的总字节数(压缩前的大小,即使数据被压缩)。
  • count :记录ziplist中的元素数量,最大支持65535个元素。
  • encoding:表示节点数据是否被压缩。
    • 1:未压缩(RAW)。
    • 2:使用LZF算法压缩(LZF)。
  • container:固定为2,表示使用ziplist作为容器。
  • recompress:标记节点是否需要重新压缩。
    • 1:需要重新压缩(如数据被临时解压后需恢复压缩)。
  • attempted_compress:测试用标志,标记节点是否尝试过压缩但失败(如数据过小无法有效压缩)。
  • extra:预留扩展字段,目前未使用。

2.3 双向链表(quicklist)

// QuickList 结构
typedef struct quicklist {
    quicklistNode *head;          // 指向链表头节点的指针
    quicklistNode *tail;          // 指向链表尾节点的指针
    unsigned long count;         // 所有节点中元素的总数
    unsigned long len;           // 链表中节点的数量
    int fill : QL_FILL_BITS;      // 节点填充因子(16位)
    unsigned int compress : QL_COMP_BITS; // 压缩深度(16位)
    unsigned int bookmark_count;  // 快照节点数量
    quicklistBookmark bookmarks[]; // 快照节点数组(可选)
} quicklist;
  • head:指向链表的头节点,支持O(1)时间复杂度的头部插入和删除。

  • tail:指向链表的尾节点,支持O(1)时间复杂度的尾部插入和删除。

  • count:记录所有节点中元素的总数(即所有ziplist中元素数量的总和)。

  • len:记录链表中节点的数量(即quicklistNode的数量)。

  • fill:控制每个ziplist节点的最大容量,通过list-max-ziplist-size参数配置,具体如下:

    fill 值 描述
    -1 每个 quicklistNode 节点的 ziplist 所占字节数不能超过 4kb。
    -2 默认值,每个 quicklistNode 节点的 ziplist 所占字节数不能超过 8kb。
    -3 每个 quicklistNode 节点的 ziplist 所占字节数不能超过 16kb。
    -4 每个 quicklistNode 节点的 ziplist 所占字节数不能超过 32kb。
    -5 每个 quicklistNode 节点的 ziplist 所占字节数不能超过 64kb。
    任意正数 表示:ziplist 结构所最多包含的 entry 个数,最大为 215215。
  • compress:控制节点的压缩深度,通过list-compress-depth参数配置。

    • 0:不压缩任何节点。
    • n:n为正整数(最大65535),链表的前 n 个节点和后 n 个节点不压缩,剩余的中间节点使用 LZF 算法压缩。若 n 超过链表节点总数的一半,则所有节点均不压缩(避免压缩过小数据导致性能损失),等效于 compress=0。
  • bookmark_count:记录bookmarks[]数组的数量。

  • bookmarks[]:记录bookmarks[]数组存储快照节点。bookmarks[]是一个可选字段,quicklist 用来重新分配内存空间时使用,不使用时不占用空间。

2.4 QuickList 的结构图

在这里插入图片描述


3. QuickList 的核心特性

3.1 分段存储(Segmented Storage)

  • 分段设计:QuickList 将大列表分割成多个 ziplist 节点,每个节点存储一定数量的数据项。
  • 优点
    • 内存效率:ziplist 是紧凑的连续内存结构,减少指针和元数据的开销。
    • 操作性能:链表结构允许快速插入/删除操作(O(1) 或 O(N)),而 ziplist 提供了高效的遍历能力。

3.2 配置参数

QuickList 的行为通过两个关键配置参数控制:

  1. list-max-ziplist-size
    • 正数:限制每个 ziplist 最多存储的元素个数(默认 8)。
    • 负数:以内存大小限制 ziplist(例如 -1 表示 4KB,-5 表示 64KB)。
    • 作用:控制单个 ziplist 的大小,避免因数据过多导致操作性能下降。
  2. list-compress-depth
    • 作用:指定从链表两端不压缩的节点数(默认 0,表示不压缩)。
    • 压缩机制:中间的 ziplist 会被压缩存储,减少内存占用,但会增加访问时的解压开销。

3.3 插入与删除操作

  • 头部/尾部插入(LPOP/RPUSH)
    • 时间复杂度为 O(1),直接操作链表的头或尾节点。
  • 中间插入/删除
    • 时间复杂度为 O(N),需要定位到对应的 ziplist 并操作。
  • 性能优化:通过 ziplist 的紧凑存储减少内存碎片,同时通过链表的分段设计降低操作对整个列表的影响。

3.4 内存优化

  • 紧凑存储:ziplist 的连续内存布局减少了指针开销,比传统链表节省 50% 以上的内存。
  • 压缩机制:中间节点的 ziplist 可被压缩(如 LZF 或 Snappy),进一步降低内存占用。
  • 动态调整:当数据量超过配置阈值时,QuickList 会自动将 ziplist 拆分成多个节点。

4. QuickList 在 Redis 中的应用

QuickList 主要用于 Redis 的 List 类型,典型场景包括:

  1. 队列(Queue)
    • 高频的 LPUSH/RPOP 操作(如消息队列、任务调度)。
  2. 日志聚合
    • 在尾部追加日志条目(RPUSH),定期清理旧日志(LTRIM)。
  3. 排行榜(Leaderboard)
    • 虽然 ZSET 通常使用跳表(SkipList),但小规模排行榜可能使用 QuickList。
  4. 缓存标签
    • 存储标签集合(如用户兴趣标签、商品分类标签)。

5. QuickList 与 Ziplist 的对比

特性 QuickList Ziplist
数据结构类型 双向链表 + ziplist 紧凑的连续内存结构
内存效率 高(ziplist 紧凑存储 + 链表分段) 高(无指针开销)
操作性能 插入/删除 O(1)(两端),查找 O(N) 插入/删除 O(N)(需移动元素),查找 O(N)
连锁更新问题 无(QuickList 不依赖前一个节点的长度) 有(修改一个 Entry 会级联更新后续 Entry)
适用场景 Redis List(默认实现) Redis Hash、ZSet(旧版实现)
动态扩展性 支持(自动拆分/合并 ziplist) 不支持(固定大小)

九、Redis 的数据结构优化

  1. Listpack 替代 Ziplist

    • 从 Redis 5.1 引入 listpack,6.x 作为过渡阶段,7.0 完全替代 ziplist
    • listpack 提供更高效的内存布局和操作性能。
  2. 多线程 I/O

    • Redis 6.x 引入多线程处理网络 I/O,但数据操作仍为单线程,确保一致性。
    • 提升高并发场景下的吞吐量。
  3. RESP3 协议

    • 支持更复杂的数据类型(如流、映射),提升客户端与服务端的交互效率。

总结

Redis 的底层数据结构通过灵活的组合和优化,实现了高性能与内存效率的平衡。不同数据结构的选择取决于具体场景:

  • 小数据:使用压缩列表(Ziplist)或 ListPack。
  • 有序集合:使用跳跃表(Skip List)。
  • 键值存储:使用字典(Hash Table)。
  • 字符串操作:使用简单动态字符串(SDS)。
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐