当前位置: 首页 > news >正文

Redis 的压缩列表:像快递驿站 “紧凑货架“ 一样的内存优化结构

目录

一、先懂核心:为什么需要压缩列表?(对比之前的结构)

二、压缩列表的结构:像 "贴标签的紧凑货架"

2.1、关键:每个节点(entry)的三个 "标签"

1. previous_entry_length:前一个包裹的 "长度标签"(核心定位字段)

2. encoding:包裹的 "内容类型 + 大小标签"

3. content:包裹里的 "实际货物"

三、核心操作:从表尾往表头遍历(zltail+previous_entry_length)

四、重点难点:连锁更新

场景铺垫:一排 "小个子货架"

触发连锁更新:加一个 "大个子货架"

为什么不用怕连锁更新?(Redis 的权衡)

五、压缩列表的适用场景:什么时候用它?(对比其他结构)

六、总结:压缩列表的设计逻辑(衔接 Redis 整体思路)


如果说 SDS 是 "智能价签"、链表是 "普通货架"、跳跃表是 "多层导航货架",那压缩列表(ziplist) 就是 Redis 为 "小批量货物" 设计的紧凑折叠货架—— 专门用来存少量、小个子的商品(短字符串、小整数),目的是最大限度节省内存。它既是列表键的底层实现(比如少量元素的LPUSH列表),也是哈希键的底层实现(比如少量键值对的HSET哈希),核心优势就是 "挤一挤,省空间"。

一、先懂核心:为什么需要压缩列表?(对比之前的结构)

之前学的链表(list) 虽然灵活,但有个 "内存浪费" 的问题:每个链表节点(listNode)要存 prev、next 两个指针(至少 16 字节,64 位系统),哪怕节点只存一个 1 字节的字符,指针开销也比数据本身大。比如存 5 个 "a"、"b" 这样的短字符串,链表的指针总开销会是数据的好几倍。

而压缩列表的思路是:把所有节点挤在一块连续内存里,不存指针,改用 "前节点长度" 来定位—— 就像快递驿站的折叠货架,所有货物紧挨着放,不用每个货架单独装轮子(指针),而是靠 "前一个货架多长" 来找到下一个,省出大量空间。

二、压缩列表的结构:像 "贴标签的紧凑货架"

压缩列表是连续内存块组成的顺序结构,每个 "货物"(节点 entry)都带三个 "标签",整体结构和快递驿站的紧凑货架对应如下:

压缩列表整体结构快递驿站对应场景作用
zlbytes(4 字节)货架总长度标签记录整个压缩列表的内存长度,方便快速计算总空间
zltail(4 字节)最后一个货架的位置标签记录最后一个节点的起始地址,支持从表尾往表头遍历(不用从头找)
zllen(2 字节)货架总数标签记录节点数量,快速知道有多少个元素(元素少的时候准确,多了会标记为 0xFF)
entry1(节点 1)第一个货物 + 标签实际存储的数据(短字符串 / 小整数)
entry2(节点 2)第二个货物 + 标签同上,紧挨着第一个节点
.........
zlend(1 字节)货架结束标记标记压缩列表的末尾(固定为 0xFF)

2.1、关键:每个节点(entry)的三个 "标签"

每个节点就像 "贴了三张标签的小包裹",结构是 previous_entry_length + encoding + content,我们逐个对应到 "包裹" 上:

1. previous_entry_length:前一个包裹的 "长度标签"(核心定位字段)
  • 作用:记录前一个节点的总长度(字节数),用来实现 "从后往前找"(表尾→表头遍历)。比如想从最后一个节点找倒数第二个,就用 "当前节点起始地址 - 前节点长度",就能准确定位。
  • 空间优化:长度不是固定的,分两种情况(为了省内存):
    • 前节点长度<254 字节:这个标签只占 1 字节(比如前节点长 10 字节,标签就存 0x0A)。
    • 前节点长度≥254 字节:这个标签占 5 字节(第一字节固定为 0xFE,后面 4 字节存实际长度,比如前节点长 300 字节,标签就是 0xFE + 0x0000012C)。

类比:快递包裹的 "前包裹长度" 标签,小包裹用 1 格纸写长度,大包裹用 5 格纸,不浪费标签纸。

2. encoding:包裹的 "内容类型 + 大小标签"
  • 作用:告诉 Redis 这个节点存的是 "短字符串" 还是 "小整数",以及内容的长度。
  • 编码规则(简单记):
    • 最高位是000110:存的是字节数组(字符串,比如 "abc"),后面的位记录字符串长度。
    • 最高位是11:存的是整数(比如 123),后面的位记录整数类型(16 位、32 位等)。

类比:包裹的 "内容说明" 标签,写着 "字符串・3 字节" 或 "整数・16 位",方便快递员快速识别内容类型。

3. content:包裹里的 "实际货物"
  • 作用:存储真实数据,内容由 encoding 决定:
    • 如果是字符串:存的是 SDS 的底层字节数组(比如 "abc" 就存0x61 0x62 0x63,和 SDS 的 buf 字段一致,衔接之前的知识)。
    • 如果是整数:存的是二进制整数(比如 123 就存0x7B,直接用二进制节省空间)。

类比:包裹里的实际物品,可能是小文件(字符串)或小零件(整数)。

三、核心操作:从表尾往表头遍历(zltail+previous_entry_length)

压缩列表支持双向遍历,其中 "从后往前找" 的逻辑特别依赖zltailprevious_entry_length,我们用快递驿站的场景拆解:

假设驿站有 3 个紧凑货架(节点 e1、e2、e3),要从最后一个 e3 找到第一个 e1:

  1. 定位最后一个货架:看总标签zltail,它记录了 e3 的起始地址(比如内存地址 0x100),直接找到 e3。
  2. 找倒数第二个 e2:看 e3 的previous_entry_length标签(假设是 5 字节,说明 e2 长 5 字节),用 e3 的起始地址(0x100)减去 5,得到 e2 的起始地址(0x0FB)。
  3. 找第一个 e1:看 e2 的previous_entry_length标签(假设是 3 字节),用 e2 的起始地址(0x0FB)减去 3,得到 e1 的起始地址(0x0F8)。
  4. 停止:e1 的previous_entry_length是 0(因为是第一个节点),遍历结束。

这个过程不用从头遍历所有节点,效率比链表的反向遍历(靠 prev 指针,其实效率差不多,但压缩列表省内存)更高,核心是zltailprevious_entry_length的配合。

四、重点难点:连锁更新

压缩列表为了省内存,把previous_entry_length设计成 "1 字节或 5 字节",但这会带来一个极端情况 ——连锁更新,我们用 "驿站加新货架" 的场景讲明白:

场景铺垫:一排 "小个子货架"

假设驿站有 5 个连续的小货架 e1~e5,每个货架的长度都是 252 字节(<254),所以每个货架的previous_entry_length标签都只占 1 字节(因为前货架长度<254)。此时每个货架的总长度 = 1(prev 标签)+ encoding(假设 1 字节)+ content(250 字节)=252 字节,完美符合 "小个子" 条件。

触发连锁更新:加一个 "大个子货架"

现在来了个新货架new,长度 255 字节(≥254),要放在最前面(成为 e1 的前节点)。这时候问题来了:

  1. 第一步:e1 的标签不够用了
    原本 e1 的previous_entry_length是 1 字节(记录前节点长度<254),但现在前节点是new(255 字节,≥254),1 字节存不下了,必须改成 5 字节的标签。
    这会导致 e1 的总长度从 252 字节变成:5(新 prev 标签)+1(encoding)+250(content)=256 字节(≥254)。

  2. 第二步:e2 的标签也不够用了
    e2 的previous_entry_length原本是 1 字节(记录 e1 的 252 字节),现在 e1 变成 256 字节(≥254),1 字节也存不下了,必须改成 5 字节标签。
    e2 的总长度也从 252 变成 256 字节(≥254)。

  3. 第三步:e3、e4、e5 连锁反应
    同理,e3 的前节点 e2 变成 256 字节,e3 的标签也要改;e4 的前节点 e3 变,e4 改;直到 e5 改完后,下一个节点(如果有的话)的前节点长度还<254,连锁才会停止。

这个 "一个改→个个改" 的过程,就是连锁更新—— 像推倒多米诺骨牌,因为前一个节点的长度变化,导致后面所有节点的 "前长度标签" 都要调整。

为什么不用怕连锁更新?(Redis 的权衡)

虽然连锁更新听起来吓人,但实际中几乎不用担心里程碑,原因有二:

  1. 触发条件极端:必须满足 "连续多个节点长度在 250~253 字节(改完标签后超 254)+ 加一个≥254 字节的头节点",这种情况在实际业务中很少见(大部分场景下,压缩列表存的是更小的元素,比如几字节的字符串、两位数的整数)。
  2. 影响范围有限:就算触发,压缩列表本身存的是 "少量元素"(Redis 只用它存小数据量),最多改十几个节点,不会像字典 rehash 那样动则几万、几十万数据,对性能影响微乎其微。

类比:驿站的多米诺骨牌只有 5 张,就算倒了,捡起来也快;如果有 1000 张,才会麻烦,但驿站根本不会摆 1000 张紧凑货架(会换成普通货架,对应 Redis 会把压缩列表转成链表)。

五、压缩列表的适用场景:什么时候用它?(对比其他结构)

Redis 选择用压缩列表,遵循 "小数据量用紧凑结构,大数据量用灵活结构" 的原则,具体场景:

  • 列表键:当列表元素数量少(默认<512 个),且每个元素是短字符串(默认<64 字节)或小整数时,用压缩列表;否则转成链表。
  • 哈希键:当哈希键值对数量少(默认<512 个),且每个键和值都是短字符串(默认<64 字节)或小整数时,用压缩列表;否则转成字典。

类比:驿站的紧凑货架只放少量、小个子的快递;如果快递多了、大了,就换成普通货架(链表)或多层导航货架(跳跃表)。

六、总结:压缩列表的设计逻辑(衔接 Redis 整体思路)

压缩列表的所有设计,都是 Redis"根据场景做取舍" 的体现,和之前学的结构一脉相承:

  • SDS:用 "len+free" 平衡字符串扩展和内存(取舍:预分配少量空间换扩展效率);
  • 字典:用 "双哈希表 + 渐进式 rehash" 平衡查找效率和扩容性能(取舍:分多次迁移换服务不中断);
  • 跳跃表:用 "多层索引" 平衡有序查找和实现复杂度(取舍:多存几层索引换比平衡树简单);
  • 压缩列表:用 "连续内存 + 可变长度标签" 平衡内存占用和极端情况(取舍:接受罕见的连锁更新换极致省内存)。

简单说,压缩列表就是 Redis 为 "小数据量、省内存" 场景定制的 "紧凑货架"—— 它不追求万能,但在自己的专属场景里,内存效率远超链表和字典,是 Redis"因地制宜" 设计哲学的又一个典型例子。

http://www.dtcms.com/a/363696.html

相关文章:

  • Redis-底层数据结构篇
  • 8.30美团技术岗算法第二题
  • 【C++】15. ⼆叉搜索树
  • WordPress.com 和 WordPress.org 之间的区别说明
  • 系统架构——过度设计
  • IO_HW_9_2
  • 教你 Centos 如何离线安装 rlwrap 插件(内网环境)
  • MATLAB矩阵及其运算(三)矩阵的创建
  • 一文搞懂:0-1整数规划与蒙特卡罗模拟(附MATLAB代码详解)
  • 命令行文本处理小工具:cut、sort、uniq、tr 详解与应用
  • 从零开始的python学习——函数(2)
  • shell复习(2)
  • Flutter环境搭建全攻略之-windows环境搭建
  • 毫米波雷达信号处理步骤顺序
  • 树莓派网页监控
  • [嵌入式embed][Qt]Qt5.12+Opencv4.x+Cmake4.x_用Qt编译Windows-Opencv库
  • LangGraph 重要注意事项和常见问题
  • MTK Linux DRM分析(二十六)- MTK mtk_drm_ddp_xxx.c
  • 如何创建逻辑卷
  • Shell脚本入门:从零到精通
  • 容器设备映射配置在海外云服务器GPU加速环境的实施规范
  • QML的focus与activeFocus
  • C++ 左值引用与右值引用介绍
  • MySQL数据库精研之旅第十五期:索引的 “潜规则”(下)
  • OpenCV Python
  • 0825-0829 | 大模型方向周报:多模态模型研究、训练与优化策略、安全与对齐等方向
  • SQL Server--提取性能最差的查询
  • 阿里云国际代理商:如何重置阿里云服务器密码?
  • 阿里云日志服务之WebTracking 小程序端 JavaScript SDK (阿里SDK埋点和原生uni.request请求冲突问题)
  • 现代CPU设计哲学——加载/存储(Load-Store)架构