当前位置: 代码网 > it编程>数据库>Redis > Redis Ziplist压缩列表的实现

Redis Ziplist压缩列表的实现

2026年08月28日 Redis 我要评论
一、概述ziplist(压缩列表)是 redis 中一种极致紧凑的序列化数据结构,用于在内存中存储少量元素的列表。它是 redis 为节省内存而设计的核心编码之一。1.1 为什么需要 ziplist?

一、概述

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压缩列表内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com