Redis Ziplist压缩列表的实现

 更新时间:2026年08月28日 09:08:43   作者:只爱喝胡辣汤  
Ziplist压缩列表是Redis中一种极致紧凑的序列化数据结构,用于在内存中存储少量元素的列表,它是 Redis 为节省内存而设计的,下面就来介绍一下如何使用,感兴趣的可以了解一下

一、概述

Ziplist(压缩列表)是 Redis 中一种极致紧凑的序列化数据结构,用于在内存中存储少量元素的列表。它是 Redis 为节省内存而设计的核心编码之一。

1.1 为什么需要 Ziplist?

问题:Redis 的 List、Hash、ZSet 等数据结构如果用标准链表或哈希表实现,内存开销很大。

解决方案:Ziplist 将多个元素紧凑地编码到连续内存中,大幅减少内存占用。

1.2 使用场景

数据类型使用条件说明
List元素少且短<list-max-ziplist-entries 且 <list-max-ziplist-value
Hash字段少且短<hash-max-ziplist-entries 且 <hash-max-ziplist-value
ZSet元素少且短<zset-max-ziplist-entries 且 <zset-max-ziplist-value

注意:Redis 7.0+ 中,Ziplist 已逐渐被 Listpack 替代,但理解 Ziplist 对理解 Redis 的设计思想至关重要。

二、整体布局

2.1 内存结构

+-----------+-----------+-----------+--------+--------+-----+--------+
|  zlbytes  |  zltail   |   zllen   | entry1 | entry2 | ... |  zlend |
| (4 bytes) | (4 bytes) | (2 bytes) |        |        |     | 1 byte |
+-----------+-----------+-----------+--------+--------+-----+--------+
       ^
       所有字段都是小端序(Little Endian)
字段类型大小说明
zlbytesuint32_t4 bytes整个 ziplist 占用的字节数
zltailuint32_t4 bytes到最后一个 entry 的偏移量
zllenuint16_t2 bytesentry 数量(最大 65535,超过需遍历统计)
entry变长不定实际数据条目
zlenduint8_t1 byte结束标记,固定为 0xFF(255)

2.2 空 Ziplist

[11 00 00 00] [0B 00 00 00] [00 00] [FF]
    |              |            |      |
 zlbytes=11     zltail=11    zllen=0  zlend

空 ziplist 大小:11 bytes(Header 10 bytes + End 1 byte)

三、Entry 编码详解

每个 Entry 的格式:

<prevlen> <encoding> <entry-data>

3.1 Prevlen(前一条目长度)

用于反向遍历,记录前一条目的总字节数。

长度范围编码方式大小
< 254直接存储1 byte
>= 2540xFE + 4 bytes5 bytes
// 编码前一条目长度
if (len < 254) {
    p[0] = len;              // 1 byte
} else {
    p[0] = 0xFE;             // 标记
    memcpy(p+1, &len, 4);    // 4 bytes
}

3.2 Encoding(编码方式)

Encoding 字节决定了数据的类型和长度。

字符串编码

编码模式格式最大长度示例
`00pppppp`1 byte63 bytes0x0B = 长度 11 的字符串
`01ppppppqqqqqqqq`2 bytes16383 bytes0x40 0x01 = 长度 64 的字符串
`10000000...`5 bytes2^32-1 bytes0x80 00 00 10 00 = 长度 4096

整数编码

编码模式编码字节数据大小范围
`11000000`0xC02 bytesint16_t
`11010000`0xD04 bytesint32_t
`11100000`0xE08 bytesint64_t
`11110000`0xF03 bytes24-bit signed
`11111110`0xFE1 byteint8_t
`1111xxxx`0xF1-0xFD0 byte4-bit immediate (0-12)

3.3 编码示例

示例 1:存储整数 2 和 5

[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
      |              |            |       |       |      |
   zlbytes=15     zltail=12    zllen=2   "2"    "5"    end

解析:

  • 第一个 entry:00 f3
    • 00:prevlen = 0(第一个 entry)
    • f3:1111 0011,immediate integer = 3 - 1 = 2
  • 第二个 entry:02 f6
    • 02:prevlen = 2(第一个 entry 的长度)
    • f6:1111 0110,immediate integer = 6 - 1 = 5

示例 2:存储字符串 "Hello World"

[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]
 |    |    |
prevlen=2  len=11  "Hello World" (11 bytes)

解析:

  • 02:前一条目长度为 2 bytes
  • 0b:0000 1011,string 编码,长度 = 11
  • 48 65 ... 64:"Hello World" 的 ASCII

四、核心 API

4.1 创建与销毁

// 创建空 ziplist
unsigned char *ziplistNew(void);
// 释放 ziplist
void zfree(unsigned char *zl);  // 直接用 zfree

4.2 插入操作

// 在头部或尾部插入
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, 
                           unsigned int slen, int where);
// where: ZIPLIST_HEAD=0, ZIPLIST_TAIL=1

// 在指定位置插入
unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p,
                             unsigned char *s, unsigned int slen);

4.3 删除操作

// 删除指定位置的 entry
unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p);

// 删除范围内的 entry
unsigned char *ziplistDeleteRange(unsigned char *zl, int index, unsigned int num);

// 替换指定位置的 entry
unsigned char *ziplistReplace(unsigned char *zl, unsigned char *p,
                              unsigned char *s, unsigned int slen);

4.4 查询操作

// 按索引获取 entry(支持负数索引)
unsigned char *ziplistIndex(unsigned char *zl, int index);

// 获取下一个 entry
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p);

// 获取上一个 entry
unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p);

// 获取 entry 的值
unsigned int ziplistGet(unsigned char *p, unsigned char **sval, 
                        unsigned int *slen, long long *lval);

// 获取 ziplist 长度
unsigned int ziplistLen(unsigned char *zl);

// 获取总字节数
size_t ziplistBlobLen(unsigned char *zl);

4.5 查找操作

// 查找匹配的 entry
unsigned char *ziplistFind(unsigned char *zl, unsigned char *p,
                           unsigned char *vstr, unsigned int vlen, 
                           unsigned int skip);

// 比较 entry 和字符串
unsigned int ziplistCompare(unsigned char *p, unsigned char *s, 
                            unsigned int slen);

五、级联更新(Cascade Update)

5.1 问题描述

当插入或删除一个 entry 时,可能导致后续 entry 的 prevlen 字段长度发生变化:

Before:
[entry A: 2 bytes] [entry B: 253 bytes] [entry C: ...]
       |                   |
    prevlen=1          prevlen=1

After: Insert a new entry before B with 300 bytes
[entry A: 2 bytes] [new entry: 300 bytes] [entry B: 253 bytes] [entry C: ...]
       |                                      |
    prevlen=1                             prevlen=5 (!!!)

问题:entry B 的 prevlen 从 1 byte 变成了 5 bytes,导致 B 自身变大了!

5.2 级联效应

B 变大了 -> C 的 prevlen 可能也要变 -> C 也变大 -> ...

这种连锁反应就是级联更新。

5.3 解决方案

unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
    // 1. 计算当前 entry 的长度
    // 2. 检查下一个 entry 的 prevlen 是否需要更新
    // 3. 如果需要,更新并继续检查下一个
    // 4. 直到不需要更新为止
}

优化:

  • 只增不减(避免"抖动")
  • 实际发生级联的概率很低(需要连续多个 entry 刚好在边界)
  • 最坏情况下是 O(n),但平均情况是 O(1)

六、编码自动转换

6.1 整数编码

Ziplist 会自动尝试将字符串转换为整数编码:

// 尝试将字符串编码为整数
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, 
                   long long *v, unsigned char *encoding) {
    if (string2ll((char*)entry, entrylen, &value)) {
        // 可以编码为整数
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN + value;  // 4-bit immediate
        } else if (value >= INT8_MIN && value <= INT8_MAX) {
            *encoding = ZIP_INT_8B;
        } else if (value >= INT16_MIN && value <= INT16_MAX) {
            *encoding = ZIP_INT_16B;
        } // ... 以此类推
        return 1;
    }
    return 0;  // 无法编码为整数,保持字符串
}

6.2 编码选择策略

数据类型编码方式大小
字符串`00pppppp`1 + len
整数 0-12`1111xxxx`1 byte(无数据)
整数 -128~127`11111110`1 + 1 = 2 bytes
整数 -32768~32767`11000000`1 + 2 = 3 bytes
大整数`11100000`1 + 8 = 9 bytes

七、性能分析

7.1 时间复杂度

操作复杂度说明
头部插入O(n)需要 memmove + 级联更新
尾部插入O(1)直接追加(无级联时)
按索引查找O(n)线性遍历
按值查找O(n)线性遍历
删除O(n)memmove + 级联更新
获取长度O(1)直接读取 zllen
获取总字节数O(1)直接读取 zlbytes

7.2 空间复杂度

场景内存占用说明
空 ziplist11 bytesHeader + End
存储小整数1-3 bytes/个极致紧凑
存储短字符串1-64 bytes/个取决于字符串长度
存储长字符串较大不适合用 ziplist

八、与 Listpack 的对比

特性ZiplistListpack
设计目标紧凑编码紧凑编码 + 改进
prevlen存储前一条长度存储当前条长度
级联更新可能发生避免
遍历方向双向单向(从头遍历)
安全性较低(级联 bug)更高
状态逐步淘汰Redis 7.0+ 默认

九、设计亮点总结

9.1 紧凑编码

思想:"每一个 bit 都要有价值"

  • 整数自动编码(最小 1 byte)
  • 字符串长度按需编码(1/2/5 bytes)
  • prevlen 按需编码(1/5 bytes)

9.2 双向遍历

思想:"用空间换时间,但空间要花在刀刃上"

  • 通过 prevlen 实现反向遍历
  • O(1) 获取尾部(通过 zltail)

9.3 自动类型转换

思想:"对用户透明,自动选择最优编码"

  • 字符串自动转整数
  • 小整数用 immediate encoding

附录:完整编码表

|00pppppp| - 1 byte
     String value with length <= 63 bytes

|01pppppp|qqqqqqqq| - 2 bytes
     String value with length <= 16383 bytes

|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
     String value with length >= 16384 bytes

|11000000| - 3 bytes total
     Integer encoded as int16_t (2 bytes)

|11010000| - 5 bytes total
     Integer encoded as int32_t (4 bytes)

|11100000| - 9 bytes total
     Integer encoded as int64_t (8 bytes)

|11110000| - 4 bytes total
     Integer encoded as 24 bit signed (3 bytes)

|11111110| - 2 bytes total
     Integer encoded as int8_t (1 byte)

|1111xxxx| - 1 byte (xxxx between 0001 and 1101)
     Immediate 4 bit integer (0-12)

|11111111| - End of ziplist

参考源码:Redis 8.8.2 (src/ziplist.h, src/ziplist.c)

到此这篇关于Redis Ziplist压缩列表的实现的文章就介绍到这了,更多相关Redis Ziplist压缩列表内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Redis分布式锁防止缓存击穿的实现

    Redis分布式锁防止缓存击穿的实现

    本文主要介绍了Redis分布式锁防止缓存击穿的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • redis适合场景八点总结

    redis适合场景八点总结

    在本篇文章中我们给大家整理了关于redis适合什么场景的8点知识点内容,需要的朋友们参考下。
    2019-06-06
  • 搭建Redis服务器步骤详细介绍

    搭建Redis服务器步骤详细介绍

    大家好,本篇文章主要讲的是搭建Redis服务器步骤详细介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览
    2021-12-12
  • Redis分布式锁存在的问题(推荐)

    Redis分布式锁存在的问题(推荐)

    有很多基于Redis实现的分布式锁方案或者库,但是有些库并没有解决分布式环境下的一些问题陷阱,这篇文章主要介绍了Redis分布式锁存在的问题,需要的朋友可以参考下
    2022-12-12
  • redis stream 实现消息队列的实践

    redis stream 实现消息队列的实践

    本文主要介绍了redis stream 实现消息队列的实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • 详解redis分布式锁(优化redis分布式锁的过程及Redisson使用)

    详解redis分布式锁(优化redis分布式锁的过程及Redisson使用)

    在分布式的开发中,以电商库存的更新功能进行讲解,在实际的应用中相同功能的消费者是有多个的,这篇文章主要介绍了redis分布式锁详解(优化redis分布式锁的过程及Redisson使用),需要的朋友可以参考下
    2021-11-11
  • 控制Redis的hash的field中的过期时间

    控制Redis的hash的field中的过期时间

    这篇文章主要介绍了控制Redis的hash的field中的过期时间问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-01-01
  • Redis实现分布式锁和等待序列的方法示例

    Redis实现分布式锁和等待序列的方法示例

    这篇文章主要介绍了Redis实现分布式锁和等待序列的方法示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-06-06
  • Redis实现排名功能的示例代码

    Redis实现排名功能的示例代码

    本文主要介绍了Redis实现排名功能的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02
  • Redis中Scan命令的基本使用教程

    Redis中Scan命令的基本使用教程

    这篇文章主要给大家介绍了关于Redis中Scan命令的基本使用教程,文中通过示例代码介绍的非常详细,对大家学习或者使用Redis具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-06-06

最新评论