恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Redis压缩列表底层原理剖析:内存布局、编码规则与连锁更新

  • 首页
  • 资讯中心
  • /
  • Redis压缩列表底层原理剖析:内存布局、编码规则与连锁更新

相关资讯

Hydra Windows 版部署与密码字典配置避坑指南 2026/10/21 3:02:58
Tomcat架构原理、性能调优与生产故障排查实战 2026/10/21 3:02:58
深度强化学习入门基石:从Q-learning到DQN及其进化路线解析 2026/10/21 3:02:58

最新资讯

基于YOLO的无人机占道经营识别:数据集构建与边缘部署实践
.NET Core微服务接入Consul集群:高可用注册中心搭建与容灾实践
技能不是名词:从盘点、训练到迁移,打造个人技能增长系统
QLScriptPublic 批量自动签到:青龙面板部署 + 100 个以上平台的完整配置教程
EasyWAM-Hidden-Wan22-LoRA-128 检查点细节全览:7 维动作、8 维状态与 224px 双相机训练配置解析
Linux文件查找实战:find与locate等工具详解及避坑指南

今日推荐

SAP 屏幕上的金额未必存在数据库表里,从 WB2R_SV 的 Net Amount 看懂运行时计算
具身智能中‘指哪打哪’的视觉语言动作联合建模
AI时代人工翻译技术博客的价值与实践指南

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Redis压缩列表底层原理剖析:内存布局、编码规则与连锁更新

发布时间:2026/10/21 3:07:58
Redis压缩列表底层原理剖析:内存布局、编码规则与连锁更新 1. 压缩列表到底在解决什么问题我当年第一次翻开Redis源码看到ziplist这个词的时候第一感觉是它跟平时理解的“列表”完全是两回事。压缩列表Compressed List字面意思很直白把数据压缩到一块连续内存里。Redis愿意为此多花一点CPU去解析编码规则换来的则是内存占用的大幅下降。在纯内存数据库这个场景里这笔交换非常划算。先说清楚它解决的根本问题Redis的数据常驻内存内存就是最珍贵的资源。如果你用普通双向链表来存一个小列表在64位系统上光prev和next两个指针就占16字节再加上每个节点独立malloc、内存碎片、CPU缓存命中率下降一套组合拳下来开销很惊人。压缩列表恰恰相反它把所有元素按顺序拼接在一块连续内存上每个节点之间不留空洞节点边界信息用尽量短的编码来表示能省就省。小对象的内存消耗可以压得很低同时也大幅减少了内存分配次数加载到CPU缓存时也更友好。压缩列表具体出现在哪里最常见的三个地方hash的小数据量编码、zset的小数据量编码以及list在演进过程中经历的ziplist阶段。你往一个hash里塞几十个字段往zset里加几十个成员数据量还没超过阈值时Redis默认就用压缩列表来存。数据量继续增长才会转换成hashtable、skiplist这些更“常规”的结构。什么人需要搞懂这块内容一类是做Redis运维和调优的人至少得明白OBJECT ENCODING命令查出来的ziplist到底意味着哪些性能特征另一类是读源码、设计中间件、对内存布局敏感的工程师。这篇文章我会把字节层的设计决策和背后取舍尽量讲透也把读源码时容易踩的坑一起说出来帮你少走弯路。2. 压缩列表的五个区段逐字节拆开看2.1 整体布局三个头字段、若干节点、一个结束符压缩列表在内存里就是一段连续字节。整块结构可以分成五段zlbytes、zltail、zllen、entry列表、zlend。每个部分都承担明确职责我一一展开。字段字节数作用zlbytes4字节记录整块压缩列表占用的总字节数需要重新分配内存时直接取这个值zltail4字节记录了尾节点相对于压缩列表起始地址的偏移量用来实现尾部O(1)访问zllen2字节记录节点数量但上限只能到65535entry1..entryN不定长实际存储的元素每个节点内部还有自己的三字段结构zlend1字节固定值0xFF标记列表结束zlbytes和zltail这两个字段是最容易被忽略却又很关键的设计。Redis的list命令经常要做LPUSH、RPUSH、LPOP、RPOP这类两端操作如果在尾部也要从头遍历一次性能直接崩掉。zltail就是为此存在的它把尾节点地址偏移量提前存好任何一个遍历操作都能迅速定位末尾。zlbytes则服务于内存管理扩容或缩容时系统只需要读取这个值就知道当前块占多大不需要自己计算。zllen这个2字节字段有个经典坑它最大只能表示65535。当节点数超过65535时zllen会被固定设置为65535真正的节点数量只能通过遍历才能得到。这意味着用zllen判断“列表是否为空”是安全的判断“列表是否只有几百个节点”也是安全的但如果你拿它当精确计数用遇到大列表就会出问题。读源码的时候不了解这个限制很容易产生误解。2.2 一个空列表到底占多少内存很多人以为空列表就是0字节压缩列表可不是。即使一个元素都没有一块ziplist也有固定的11字节开销zlbytes占4字节zltail占4字节zllen占2字节zlend占1字节加起来正好11字节。这11字节看起来不多但会直接影响Redis的配置阈值设计。正因为它有固定开销Redis在使用ziplist作为底层编码时配套了一系列阈值参数用来判断“到了多少数据量就不再划算了”。一旦元素数量超过阈值立刻转成hashtable、skiplist等结构避免让一个变小了的空间优势被固定开销抵消掉。举个直观的例子你创建一个空hash对象其实底层的ziplist已经分配好11字节了。这时候你再往里写两个字段每个字段的key和value各占一点空间整体内存会继续上涨。正是因为起点固定成本不算低Redis才坚持在小数据量下才用ziplist超出阈值就切换避免为“省几个指针”付出不必要的解析代价。2.3 每个entry节点的内部分工真正存数据的部分由一组entry节点连续排列组成。每个entry节点内部又分成三段prevlen、encoding、data。prevlen记录前一个节点的字节长度作用是在反向遍历时能直接定位前一个节点的起点。因为压缩列表是连续内存没有链表式的指针反向遍历所有节点就只能靠这个基准反推。encoding用来标记当前节点的数据类型和长度data就是实际内容。注意压缩列表没有链表那种“头节点指针结构”它对内存的利用做到了极致但代价是节点定位只能顺序扫描。也就是说压缩列表的读写复杂度本质上是O(N)。这也是它只适合小数据量的根因一旦数据量大起来顺序扫描的代价会超过内存节省带来的收益。3. 节点编码方案那些拼命省空间的细节3.1 prevlen的两种形态每个entry节点的开头通过prevlen记录前一个节点的长度。它有两种编码形态如果前一个节点长度小于254字节prevlen只占1个字节直接存长度值。如果前一个节点长度大于等于254字节prevlen占5个字节第一个字节固定是0xFE后面4个字节才是真实长度。这里有几个值得琢磨的设计细节。第一个是0xFE这个哨兵值的来源zlend的结束符是0xFF所以编码时把254以上的情况交给多字节表示避免和结束符冲突。第二个是prevlen占用的字节数并不固定一个节点本身大小变化会直接影响它后面那个节点的prevlen占用空间。这为后面要讲的“连锁更新”埋下了伏笔。在64位系统下如果列表里每个节点都只有几十字节prevlen几乎都能用1字节覆盖整体开销非常小。但如果你在列表中间放了一个1KB的大节点它后面那个节点就得用5字节的prevlen来记录它的存在。这个决策极其节能同时也把节点间的耦合关系变复杂了。3.2 encoding字段的编码规则encoding字段是压缩列表能省内存的重头戏。它根据开头两个bit分成几个大类源码里的判断逻辑基本都围绕这两个bit展开开头两位含义可表示范围00短字符串后6位记录长度长度不超过63字节01中字符串后14位记录长度长度不超过16383字节10长字符串后32位记录长度大于16383字节11整数编码具体类型由后续位决定见下文从encoding的第一个字节的最高位开始读就能判断当前节点属于哪种类型这是解析节点最关键的一步。我读源码时养成的习惯是先明确“现在读的是什么编码大类的节点”再去解析对应字段顺序一旦搞反后面所有字节都会错位。3.3 整数编码的节省诀窍整数编码是压缩列表里非常有创造力的地方。数字100如果直接转成字符串100要占3个字节但如果用整数编码通常只需要encoding加上2字节或者更少的data空间。如果数字落在0到12之间还有更极端的处理方式值本身可以直接塞进encoding字段连data都不用单独分配。这种特别小的数字Redis节省到连一个data字节都不愿意浪费。具体到编码类型整数编码包含几种子类型int16、int32、int64、24位有符号整数等。Redis在写入时会先尝试把输入字符串解析成整数如果成功且在支持范围内就走整数编码。这个决策对日常调优的启示是写hash的value时如果值本身能用整数表示就尽量别加无意义的格式化字符串比如100和100后者在压缩列表下能省出好几个字节。4. 连锁更新最容易被误判为性能灾难的问题4.1 什么是连锁更新连锁更新的根源在prevlen的两种形态上。当一个节点的长度小于254字节时它后面节点用1字节的prevlen就够了一旦这个节点长度跨过254字节后面节点的prevlen就得变宽到5字节。连锁更新发生的场景非常极端假设一个压缩列表里连续存在多个长度在250到253字节之间的节点此时你在它们前面插入了一个长度超过254字节的大节点。第一个小节点原来的prevlen只有1字节现在要记录大节点的长度prevlen需要扩容到5字节于是它自身的长度多了4字节。如果它原来的长度加上这4字节后超过了254字节就会导致它后面的节点又要跟着扩容……这样一个接一个地传递直到所有受影响的节点都完成调整。最坏情况下这是一个O(N^2)的操作因为每个节点可能被反复调整多次。4.2 它真的那么可怕吗说实话我在实际项目中碰到连锁更新的概率很低。要触发它需要一整套节点长度整齐地卡在250到253字节这个狭窄区间然后你还恰好在大节点前面做插入。Redis源码注释里也直接说了即便触发连锁更新分摊后的时间开销仍然是可接受的并不会让Redis卡死。不过有一个场景值得注意删除大节点时也可能触发连锁更新。当你删除一个长度较大的节点后它后面的节点prevlen可能从5字节缩回1字节这种缩容同样会引发后续节点的字节数变化。所以不只是插入删除也可能让压缩列表批量调整布局。我踩过的坑是在初始化设计阶段没有关注value长度的分布导致某个业务场景里的hash字段长度很均匀普遍在250字节附近。虽然最后没有真的出现灾难性延迟但分析问题时要排查“是否触发连锁更新”就多了一层复杂度。所以如果你在设计一个数据模式能让大量节点长度不要整齐地卡在临界值附近那会省去后面很多麻烦。5. 压缩列表在Redis里的应用与调优经验5.1 三个主要使用场景压缩列表作为Redis的底层编码出现在三类对象中hash对象当字段数量少且字段名、字段值的长度都较小时底层使用ziplist。zset对象当成员数量少且member的长度较小时底层使用ziplist。list对象在Redis 3.2之前小列表直接由ziplist实现3.2之后list底层变成了quicklist而quicklist的每个节点内部又用ziplist存数据。这些事情用一条命令就能验证。对某个key执行OBJECT ENCODING返回ziplist就说明当前数据正用压缩列表编码。我在排查线上内存问题时几乎都会先批量跑一遍这条命令把返回ziplist的key挑出来再决定要不要调整参数。5.2 一堆默认阈值到底该怎么调与压缩列表相关的核心配置包括下面这些结构配置项默认值hashhash-max-ziplist-entries128hashhash-max-ziplist-value64zsetzset-max-ziplist-entries128zsetzset-max-ziplist-value64list3.2前list-max-ziplist-entries512list3.2前list-max-ziplist-value64这些参数的含义很清晰entries是指最多容纳多少元素value是指单个key或value的长度上限。只要其中一个超了Redis就立即做结构转换。hash会从ziplist转成hashtablezset会从ziplist转成skiplist加dict的混合结构。在3.2版本之后list对象原来的这两个配置被list-max-listpack-size和list-max-listpack-value取代了因为list底层的quicklist内部节点已经换成了ziplist的变体参数语义也有调整。如果你还在用旧版配置去调新版Redis的list会发现参数根本没有生效这个坑我见过不止一次。5.3 真正值得依靠的收益点压缩列表最核心的收益在于它能让连续内存块在CPU缓存里更高效地被加载。少了指针跳转少了碎小的分配块小数据量场景下速度不但不慢反而比普通链表更快。我调整过一个字段少、value短的hash类型业务从hashtable切到ziplist编码之后内存占用直接下降了约三成读取延迟几乎没变。这种收益在数据量还小于转码阈值时非常明显。调整阈值时要记住一个原则不建议无脑调大hash-max-ziplist-entries。ziplist毕竟是O(N)遍历数据量大了以后查找成本会线性上涨。折中方案是先用默认参数观察内存和延迟曲线再用业务实际读写比例逐步试探。只要线上读多写少、字段长度稳定适当调大阈值通常能换来可观的省内存收益但如果是高并发随机访问还是把阈值压低更稳妥。6. 从ziplist到listpack演进背后的取舍6.1 quicklist一个过渡方案Redis 3.2引入quicklist的逻辑是在ziplist和双向链表之间找一个折中。quicklist本质上是一个双向链表但每个链表节点内部又嵌入一个ziplist。这样一来两端的插入删除依然高效中间区域的表示也保留紧凑内存的优势。quicklist的节点数量、每个节点内部zip数量都可以通过配置调整。它吸收了双向链表对两端操作的支持也吸收了ziplist节省内存的特点。旧的list对象在3.2版本之前要么是ziplist、要么是linkedlistquicklist用混合结构解决了“既要又要”的问题。6.2 listpack为什么能根治连锁更新listpack和ziplist最本质的区别是listpack不再把前一个节点的长度放在当前节点开头而是把每个节点的长度信息放在节点末尾从后往前读取时先读长度再往前定位。这样在插入或删除节点时影响范围被严格限制在当前节点附近不会出现因为前节点长度变化而引发的连锁反应。正是因为去掉了连锁更新这一隐患Redis 7.0在hash、zset、list的小数据量场景都逐步切到了listpack实现。现在你关心的对象编码名可能已经不在乎叫不叫ziplist新代码只要避开那个设计顽疾就能获得同等甚至更好的内存效率。这也是我在实际调优中建议关注新版本的原因之一即使没有特殊参数改动底层结构的改进也能带来额外收益。6.3 回头看压缩列表给我的启发虽然压缩列表正在被listpack替代但我依然认为它是理解Redis演进脉络的最佳入口之一。它真正教会我的不是某一个具体实现而是一套思路如何在内存开销、解析复杂度和操作复杂度三者之间做折中。下次设计任何需要极致节省内存的小型容器我都会自然地问自己能不能把元素压进连续内存用变长编码标记边界并且在读写时尽量降低解析成本。这套手艺是ziplist留给后来者最有价值的东西。你在自己的项目里哪怕只复刻其中一两个思想比如把短字符串用紧凑编码表示、用偏移量替代指针就能在特定场景中获得非常可观的内存收益。我当时把类似的连续内存编码思路用在一个自研的小型缓存组件上虽然功能远不及Redis庞大但内存效率的提升确实立竿见影。这也是我从压缩列表里收获最深的经验。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号