Redis 系列(四):底层实现(二)——List、Set、ZSet 与 Stream 的数据结构
核心目标理解 quicklist、intset、skiplist、listpack、rax 的原理与取舍用MEMORY USAGE实测回答为什么相同元素数ZSet 比 List 贵 10 倍理解集合编码的升级路径与阻塞命令BLPOP的等待机制。前置知识完成 Part 3redisObject、SDS、dict理解编码 当前实现与每键固定开销。验证环境Redis 8.10.0cygwin 移植版127.0.0.1:6379、redis-py 8.1.0、Python 3.11.6、Windows 11源码依据官方 Redis 7.4.2_refsrc/redis-7.4.2。最后复核日期2026-08-07。0. 本篇问题场景相同 1000 个元素内存差 10 倍实测同一实例、同一批 1000 个元素分别装进三种结构List 1000 元素 → 5,921 字节 Set 1000 元素 → 30,162 字节 ZSet 1000 元素 → 67,914 字节同样的数据量ZSet 是 List 的11 倍。为什么答案在三种结构的底层设计里List 用链表 紧凑包省内存Set 用整数压缩或哈希表ZSet 为了支持排序付出了跳表节点 哈希表的双份结构。本篇逐一打开这些实现并回答三个问题List 的快与省是怎么同时成立的Set 的 intset 何时失效ZSet 的 skiplist 为什么贵贵得值不值1. Listquicklist 双向链表 listpack 节点1.1 结构Redis 的 List 既不是简单的双向链表也不是单个 listpack而是quicklist一个双向链表每个节点quicklistNode内部是一个 listpack紧凑列表。源码quicklist.htypedefstructquicklistNode{structquicklistNode*prev;structquicklistNode*next;unsignedchar*entry;// 指向本节点内的 listpacksize_tsz;// listpack 字节数unsignedintcount:16;// 本节点内元素数unsignedintencoding:2;// RAW1 或 LZF2压缩...}quicklistNode;typedefstructquicklist{quicklistNode*head;quicklistNode*tail;unsignedlongcount;// 全部元素总数unsignedlonglen;// 节点个数signedintfill:QL_FILL_BITS;// 每节点填充策略unsignedintcompress:QL_COMP_BITS;// 两端保留不压缩的深度...}quicklist;小 List元素 ≤list-max-listpack-size对应阈值甚至不需要节点链直接以单个 listpack 形式存在——这就是实测10 元素 → encodinglistpack的原因数据变大后拆成多个 listpack 节点组成 quicklist2000 元素 → encodingquicklist。1.2 为什么快与省兼得需求解决结构代价头尾 O(1) 插入删除quicklist 的头/尾节点链表指针每节点 16 字节摊到多元素上省内存每个节点内是紧凑 listpack无每元素指针中间插入/删除需要移动节点内数据O(N)大值省内存list-compress-depth压缩中间节点LZF访问被压缩节点要先解压list-max-listpack-size默认-2每个 listpack 约 8 KBlist-compress-depth 0默认不压缩。设计哲学内存与随机访问性能的平衡——头尾操作永远是 O(1)中间操作按需付出拷贝代价。1.3 阻塞命令的等待机制BLPOP/BRPOP在空队列时不返回而是挂起这是轻量任务队列的核心能力。实测$ redis-cli BLPOP nonexistent:list 1 等待 1 秒后返回 nil本机实测挂起 1.06 秒后返回Nonetimeout1。机制客户端被挂到该 key 的等待队列server.db的阻塞键表当其他客户端LPUSH/RPUSH该 key 时事件循环把数据直接推给等待者并唤醒——这就是生产者-消费者零轮询的实现基础。阻塞期间客户端连接保持服务端不受影响单线程只是等待不是忙等。2. Setintset 的整数快车道2.1 intset 结构当 Set 的所有成员都是整数且数量不多时使用intset整数集合intset.htypedefstructintset{uint32_tencoding;// 元素位宽16/32/64 位uint32_tlength;// 元素个数int8_tcontents[];// 有序、无重复的整数数组}intset;contents是有序数组因此查找用二分查找 O(log N)且天然去重。8 字节一个整数几乎没有每元素开销——这是所有编码里最省内存的集合形态。2.2 升级路径三条路实测的编码决策全部真实输出SADD 100 个整数 → intset 整数、有序、去重 SADD 512 个整数 → intset 未超阈值 SADD 513 个整数 → hashtable 超 set-max-intset-entries512 SADD 整数 abc → listpack 出现非整数转紧凑列表 SADD 100 个字符串 → listpack 非整数小集合升级条件任一触发即离开 intset元素个数 set-max-intset-entries默认 512→hashtable加入非整数元素 → 若元素数仍小则listpack否则hashtable。注意第二条混合后 4 个元素的集合是listpack而不是hashtable——intset 的替代品在小规模时是 listpack更紧凑数据规模大了才上 hashtable。intset 升级 变 hashtable是旧版本的印象7.2 的正确路径是 intset → listpack小或 hashtable大。2.3 对选型的意义纯整数集合如在线用户 ID 集合在 512 以内享受二分查找 极低内存一旦混入字符串或增长编码升级但命令语义不变——这正是type 与 encoding 两层设计的好处业务代码永远不用关心底层。3. ZSetskiplist dict 的组合3.1 结构一份数据两套索引ZSet 的每个成员同时出现在两个结构中server.h:1341typedefstructzskiplistNode{sds ele;// 成员字符串doublescore;// 分数structzskiplistNode*backward;structzskiplistLevel{structzskiplistNode*forward;// 前向指针unsignedlongspan;// 跨越的节点数}level[];// 多层随机高度}zskiplistNode;typedefstructzset{dict*dict;// 成员 → score 的哈希索引O(1) 查分zskiplist*zsl;// 按 score 排序的跳表O(log N) 范围查询}zset;为什么是dict skiplist两份没有哪个单一结构能同时满足两个需求——ZSCORE member按成员查分数要 O(1) → dictZRANGEBYSCORE按分数范围遍历、ZRANK查名次要有序遍历 → skiplist。3.2 skiplist 为什么贵跳表是多级索引的链表每个节点随机一个层高level[]数组里每层存 forward 指针与 span。查找时从最高层往下跳期望 O(log N)。代价是每个节点多层指针平均 ~1.33 层额外指针 × 8 字节 每节点一个 dictEntry24 字节 两个结构的元数据。这就是 §0 实测ZSet 1000 元素 67,914 字节是 List 的 11 倍的构成排序能力是用内存换来的。设计取舍替代方案为什么不用平衡树红黑树实现复杂、范围遍历不如链表结构直观、需要节点旋转数组 二分插入/删除 O(N) 移动仅 dict无法按 score 排序遍历仅 skiplistZSCORE退化为 O(log N) 且无法 O(1) 精确查分skiplist 在期望 O(log N) 下实现简单、范围遍历自然是够用且简单的工程选择。3.3 listpack 编码的小 ZSet小 ZSetzset-max-listpack-entries默认 128、zset-max-listpack-value默认 64先以 listpack 存储成员与 score 交替压缩超限后整体转 skiplistZADD 100 成员 → listpack ZADD 1000 成员 → skiplist与 Hash/Set 同理编码只升不降删除部分成员后不会自动回到 listpack。4. Streamrax 基数树Stream 的消息按 ID毫秒时间戳-序号存储。若用普通哈希表前缀相同的 ID 会浪费大量空间Redis 用rax基数树/压缩前缀树存储消息索引rax.htypedefstructraxNode{uint32_tiskey:1;// 本节点是否是一个 key有值uint32_tisnull:1;uint32_tiscompr:1;// 是否压缩路径uint32_tsize:29;// 子节点数或压缩路径长度...}raxNode;rax 把公共前缀合并成一个节点iscompr1表示这一段路径被压缩成单节点。Stream 的消息 ID 共享时间戳-前缀rax 能把数百万条消息的索引压缩到极小同时保持字典序让从某 ID 之后读XRANGE成为天然的前缀遍历。OBJECT ENCODING对 Stream 返回stream type: stream encoding: streamStream 没有多种编码stream就是它的实现名。5. 内存分析实战为什么我的 Redis 内存涨得比数据大5.1 MEMORY USAGE 对比§0 的三组数据拆解其构成结构1000 元素内存每元素开销主要构成List5,921 B~6 Blistpack 紧凑存储无每元素指针Set30,162 B~30 Bhashtable 的 dictEntry24 B/个 桶ZSet67,914 B~68 BdictEntry skiplist 节点多层指针 span结论需要只按顺序存取用 List——最省需要去重/集合运算用 Set——每个元素一个 dictEntry需要排序范围用 ZSet——最贵但功能最强能用 List/Set 解决的场景别用 ZSet反之亦然。5.2 单键内存观察命令$ redis-cli MEMORY USAGE z:mem # 只算 value 对象 $ redis-cli MEMORY USAGE z:mem 0 # 0 表示把 key 名也算上 $ redis-cli MEMORY DOCTOR # 内存健康体检碎片、峰值等 $ redis-cli --bigkeys # 扫描最大的 keyPart 6 详讲MEMORY USAGE的含 key选项很有用它直接给出如果删掉这个 key 能省多少内存——Part 6 治理大 key 时的第一工具。6. 版本与环境差异差异点官方 7.4本机 8.10.0cygwin 移植版影响hash-max-listpack-entries默认512512CONFIG GET实测升级阈值以CONFIG GET实测为准set-max-intset-entries512512一致zset-max-listpack-entries128128一致intset 升级目标listpack小/hashtable大listpack7.2 行为与旧资料必转 hashtable不同list-max-listpack-size-2~8KB-2一致quicklist / skiplist / rax结构稳定一致源码引用 7.4.2本机与 7.4 在这些结构上行为一致写作时的关键教训是升级阈值要用CONFIG GET实测不要背文档里的数字。7. 测试与验收新增测试建议编码断言intset 512 边界、listpack→skiplist 升级、MEMORY USAGE量级对比List Set ZSet、BLPOP超时语义内存对比实验保留脚本数据规模、测量命令、环境。本篇验收清单能画出 quicklist 的结构双向链表 listpack 节点并解释头尾 O(1)/中间 O(N)能说出 intset 的三个升级触发条件超 512、非整数、规模与 7.2 的升级目标能解释 ZSetdict skiplist双结构的动机与 skiplist 的内存代价能解释 Stream 用 rax 存储消息 ID 的好处公共前缀压缩 字典序遍历能用MEMORY USAGE对比三种结构的实际内存并解释差异来源能说出BLPOP阻塞的机制挂起 写入唤醒并实测超时行为。8. 常见误区“List 是普通双向链表”——是 quicklist节点是 listpack小 List 直接就是一个 listpack§1。“intset 升级就一定变 hashtable”——7.2 小规模转 listpack§2.2旧资料过时。“ZSet 就是排序的 Set”——底层是 dict skiplist 两套结构内存接近 Set 的两倍排序能力有明确价格§3、§5。“skiplist 因为快所以用”——它比平衡树慢一点但简单很多、范围遍历自然Redis 的选择是够用 简单。“BLPOP 超时期间会占用 CPU”——不会客户端挂起等待唤醒不是忙等§1.3。“编码升级后删数据会降回来”——只升不降§3.3紧凑编码需要重建键。9. 本篇小结回到开篇同样 1000 个元素List 5.9KB、Set 30KB、ZSet 68KB——差异不是 bug是三种能力顺序存取 / 集合运算 / 排序范围各自的定价List 用链表 listpack买到了 O(1) 头尾 紧凑存储Set 用 intset 的整数快车道 hashtable 兜底ZSet 用 dict skiplist 双结构换排序能力代价是最高内存。至此五种核心结构 四种扩展结构的内存模型全部建立。下一篇 Part 5持久化回答内存里的一切如何落盘、如何恢复RDB 的 forkCOW、AOF 的 fsync 策略与混合持久化并用隔离实例做真实的断电丢数据实验。10. 官方资料Redis 7.4.2 源码https://github.com/redis/redis/tree/7.4.2/srcOBJECT/MEMORY USAGE/MEMORY DOCTORhttps://redis.io/docs/latest/commands/BLPOP阻塞语义https://redis.io/docs/latest/commands/blpop/Memory optimizationhttps://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/

相关新闻

AI模型选型与集成实战:从官方API到本地部署的工程指南

AI模型选型与集成实战:从官方API到本地部署的工程指南

在实际 AI 模型应用和开发中,我们经常遇到一个核心问题:如何理解、选择并有效使用不断涌现的新模型。无论是 OpenAI 官方发布的迭代版本,还是社区中流传的各种变体,模型名称、能力差异和接入方式常常让人困惑。特别是当看到“GPT …

2026/9/18 17:33:54 阅读更多 →
3步解锁PS4游戏无限可能:GoldHEN Cheats Manager新手快速入门指南

3步解锁PS4游戏无限可能:GoldHEN Cheats Manager新手快速入门指南

3步解锁PS4游戏无限可能:GoldHEN Cheats Manager新手快速入门指南 【免费下载链接】GoldHEN_Cheat_Manager GoldHEN Cheats Manager 项目地址: https://gitcode.com/gh_mirrors/go/GoldHEN_Cheat_Manager 你是否曾经在某个游戏关卡卡住数小时无法前进&#x…

2026/9/18 21:11:17 阅读更多 →
Spark库源码深度剖析:Android渐变动画的实现原理

Spark库源码深度剖析:Android渐变动画的实现原理

Spark库源码深度剖析:Android渐变动画的实现原理 【免费下载链接】Spark 🎨 An Android library to create gradient animation like Instagram&Spotify 项目地址: https://gitcode.com/gh_mirrors/spark20/Spark Spark是一款专为Android平台…

2026/9/14 22:08:44 阅读更多 →

最新新闻

高中数学必修三概率练习题全解析:从古典概型到Python自动化生成

高中数学必修三概率练习题全解析:从古典概型到Python自动化生成

简介:高中数学必修三概率章节的配套练习资料,面向高一学生课后巩固、高三考前回顾以及教师备课选题。内容紧扣教材中的概率基本性质、对立与互斥事件、独立事件与条件概率、组合计数、二项分布、超几何分布和伯努利试验等核心知识点,并以选择…

2026/9/19 18:50:27 阅读更多 →
SpringBoot+Vue中小企业人事管理系统源码解析与毕设实践

SpringBoot+Vue中小企业人事管理系统源码解析与毕设实践

如果你正准备做一套 Java Web 方向的毕业设计,或者刚学完 SpringBoot 和 Vue 但一直没找到机会把前后端完整打通,那么这套“SpringBootVue 中小企业人事管理系统平台”源码是特别值得认真拆一份的。它不是那种只有几个空接口的演示项目,而是把…

2026/9/19 18:50:27 阅读更多 →
用Python自动生成相似三角形练习题并输出Word文档

用Python自动生成相似三角形练习题并输出Word文档

简介:相似三角形练习题精选(华师大版)围绕第18.1课时“相似三角形”设计,是面向初中学生的专项巩固材料。内容按基础填空、选择、综合题分层编排,覆盖相似三角形的对应角相等、对应边成比例、相似比计算、平行线构造相…

2026/9/19 18:50:27 阅读更多 →
CSS border-shape未来边框形状:用clip-path和mask模拟异形边框方案

CSS border-shape未来边框形状:用clip-path和mask模拟异形边框方案

如果你做过带斜切角的卡片,大概都体验过那种凑合的日子:要么用伪元素拼接出三角形,要么塞一张背景图,要么干脆放弃。我印象特别深,去年做活动页的时候,为了一个六边形徽章的边框,前前后后写了四…

2026/9/19 18:50:27 阅读更多 →
SAP资金管理(TRM)概览:核心对象、配置要点与常见问题解析

SAP资金管理(TRM)概览:核心对象、配置要点与常见问题解析

简介:这是一份SAP资金管理概览培训PPT,面向SAP财务顾问、企业资金管理岗及信息化项目组成员,旨在用一份精炼文档讲清SAP TR模块的整体脉络。内容系统梳理了资金管理业务的五大核心板块:现金管理、现金预算管理、金融工具管理、贷款…

2026/9/19 18:50:27 阅读更多 →
ChatDev 2.0 Loop Counter 节点实战指南:用计数抑制机制为多智能体循环装上熔断器

ChatDev 2.0 Loop Counter 节点实战指南:用计数抑制机制为多智能体循环装上熔断器

ChatDev 2.0 Loop Counter 节点实战指南:用计数抑制机制为多智能体循环装上熔断器 【免费下载链接】ChatDev ChatDev 2.0: Dev All through LLM-powered Multi-Agent Collaboration 项目地址: https://gitcode.com/Dennis_Huang/ChatDev 本篇技术指南系统讲解…

2026/9/19 18:49:27 阅读更多 →

日新闻

BP神经网络时序预测:滑窗长度与多窗口平均策略

BP神经网络时序预测:滑窗长度与多窗口平均策略

简介:面向机器学习、深度学习与数据建模学习者的一份完整研究文献,聚焦BP神经网络在农业产量预测中的应用。文档以1980—2018年全国棉花产量为样本,系统讲解数据归一化处理、激活函数原理、多层神经网络结构搭建及训练流程,展示敏…

2026/9/19 0:00:30 阅读更多 →
Transformer训练实时监控实战:基于MindSpore的损失曲线可视化方案

Transformer训练实时监控实战:基于MindSpore的损失曲线可视化方案

上个月调一个Deformable DETR模型,在单卡上要跑将近两天。第二天早上我下意识打开终端翻日志,发现loss从凌晨两点就开始往上爬,一路从0.8涨到1.35,整整六个小时没人发现。那六个小时的训练不仅白跑,还霸占着卡——等于…

2026/9/19 0:00:30 阅读更多 →
OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南

OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南

OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南 【免费下载链接】opencloud 🌤️ OpenCloud is the open source platform for file management, sharing and collaboration. Simple and sovereign. 项目地址: htt…

2026/9/19 0:00:30 阅读更多 →

周新闻

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验 【免费下载链接】ai The AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and ag…

2026/9/19 3:59:36 阅读更多 →
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/GitH…

2026/9/19 3:53:08 阅读更多 →
Flutter应用改名全指南:从Android到iOS的配置与工具实践

Flutter应用改名全指南:从Android到iOS的配置与工具实践

刚接一个外包项目时,甲方要求把工程里临时用的应用名改成正式产品名。我本来觉得“改名”这种小事,打开配置文件改一行不就完了?结果真动手才发现,Flutter项目里“应用名称”根本不是一处配置,而是一整套散落在 Androi…

2026/9/19 4:02:43 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/16 22:31:27 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/19 17:50:38 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/16 22:32:59 阅读更多 →