资讯动态

Redis Hash底层原理:listpack与hashtable编码切换及内存优化指南

发布时间:2026/10/22 5:25:27 来源:尧图企业网站定制
1. 从一个日常面试题说起Redis Hash 到底是怎么存数据的先聊一个最常见的场景。你去看 Redis 的源码或者面试题十有八九会碰到“Redis 的 Hash 底层是什么”这个问题。很多人的第一反应是“不就是哈希表吗”但真正把源码打开一看会发现它根本不是一句话能说清的。Redis 的 Hash 类型在底层其实有两种编码方式一种是 ziplist压缩列表后来改名为 listpack另一种是真正的哈希表 hashtable。为什么会有两套方案核心原因就四个字省内存。这里先直接说结论。当 Hash 中的字段数量比较少、单个字段的值也比较短的时候Redis 会用 listpack 这种紧凑型结构来存储所有的键值对按顺序挤在一块连续内存里几乎没有任何额外指针开销。一旦字段多了或者某个值变大了Redis 就会自动把它升级成真正的哈希表结构也就是 dict用拉链法解决哈希冲突。这个升级是一次性的、不可逆的没有所谓“降级”机制。这套设计在实测中非常有效。我见过很多业务把用户属性、商品详情、会话信息这类小对象塞进 Hash 里字段数量常年维持在几十个以内每个值不到 100 字节这种情况下 listpack 的内存占用差不多只有 hashtable 的几分之一。但你要是没搞懂触发条件就会踩到奇怪的坑明明只是多加了几个字段内存突然涨了一截延迟也变高了。这正是编码切换在起作用。我准备从底层结构讲起再逐步拆解依赖关系、渐进式 rehash、查找流程与陷阱把 Hash 的实现完整梳理一遍。内容更适合已经会用 Redis 基本命令、想深入理解原理的同学。你不用去背源码跟着思路走一遍后面遇见类似问题基本都能自己推断出来。2. 两种底层编码listpack 和 dict 的选型逻辑2.1 先认识 listpack紧凑又省内存的小身板listpack 的前身是 ziplistRedis 7.0 以后全面替换成了 listpack。你可以把它理解成一个用一块连续内存手写的“数组”没有维护 free list也没有节点之间的指针每个元素紧紧挨着前一个元素。对于 Hash 来说listpack 里存储的每一个 entry 要么是一个 key要么是一个 value它们交替排列key1、value1、key2、value2……整个 Hash 对象就是这一串连续字节。每个 entry 由三部分组成encoding len data。encoding 用来标记数据类型比如是整数还是字符串len 表示数据的长度data 就是实际存储的内容。Redis 为了进一步压缩空间对于整数类型干脆把数字直接编码在 encoding 字段里连 data 字段都可以省掉。这种设计思路跟 protobuf 有点像都是尽可能用元信息去描述后面的数据让内存中没有多余的死角。listpack 的遍历逻辑特别简单从头开始根据当前 entry 的 encoding 和 len 计算出下一个 entry 的偏移量一直走到末尾。整个过程不需要像双向链表那样维护 prev 指针因此它天然不具备从后往前遍历的能力也没法快速删除中间节点。Redis 针对 Hash 的很多命令比如 HGETALL、HDEL在 listpack 编码下都是先定位到目标 entry然后做整块内存的 memmove 移动把后面的数据往前搬。这种操作的代价跟数据块长度成正比所以一旦 Hash 变大listpack 的劣势就会立刻暴露出来。举一个直观的数字。一个仅包含 20 个字段的小 Hash每个 key 和 value 平均 30 字节左右在 listpack 编码下可能只占 1.5KB 左右。如果切成 hashtable光 dictEntry 本身就要占 40 字节上下再加 key 对象和 value 对象各自的 redisObject 头轻松破 3KB。差距就是这么大。2.2 再看 dict真正的哈希表是怎么搭建的当 listpack 撑不住的时候Redis 会把数据迁移到 hashtable。这个 hashtable 就是 Redis 整个键空间的底层实现Redis 所有 key 都放在这样一个 dict 里。每个 dictEntry 包含三部分key 指针、value 指针、next 指针。next 指针就是拉链法的链所有哈希冲突的 entry 会串在同一个桶下面。dict 本身有两个哈希表编号为 0 和 1。正常工作时数据只放在 ht[0] 里ht[1] 只在 rehash 过程中扮演临时中转站的角色。每个 dictEntry 通过哈希函数计算出索引位置然后挂到对应的桶上。Redis 采用的哈希函数是 MurmurHash 的一个变种或 SipHash版本不同有差异核心要求就是分布均匀、计算够快。为了控制负载因子load factor也就是 entry 总数除以桶数量的比值Redis 会在条件满足时触发扩容或缩容。默认配置下Redis 的常态负载因子会保持在 1 左右当负载因子超过 1 并且允许扩容时桶数量会翻倍如果负载因子超过 5即便正在做持久化也强制扩容。缩容则发生在负载因子小于 0.1 的时候。桶数量始终以 2 的幂次增长这样计算索引时可以用位运算代替取模hash (size - 1)性能更高。2.3 编码切换的触发条件与实战配置Redis 对 Hash 编码切换有两个核心配置项分别是hash-max-listpack-entries默认 128表示字段数量不超过 128 时允许使用 listpack。hash-max-listpack-value默认 64表示每个字段的 key 或 value 长度不超过 64 字节时允许使用 listpack。只有在字段数不超过 128 且所有 key 和 value 的长度都不超过 64 字节这两个条件同时成立时Hash 才会选择 listpack。任何一个条件被打破Redis 就会触发转换把整个结构升级成 hashtable。也就是说如果你的某个字段值是 100 字节的字符串哪怕字段总数只有 3 个也直接使用 hashtable。这里有一个非常容易被忽略的细节listpack 里存的每个 value 长度是指原始字节长度不是 UTF-8 字符数。如果你的业务用中文存值一个汉字在 UTF-8 下占 3 个字节那么 20 个汉字的字符串就已经 60 字节了逼近临界值。很多同学在测试环境复现不出内存问题就是因为用的是英文短字符串生产环境一上中文数据立刻触发切换。对大多数业务来说64 字节这个限制其实是比较省心的。因为 Redis 官方认为一旦单个 value 超过 64 字节listpack 省下的内存已经不足以弥补查找和更新时的拷贝开销不如直接用哈希表。但如果你明确知道自己的 Hash 只用来存少量短字段且想进一步压缩内存可以适当调大这两个参数。反过来如果你的 Hash 字段大小不确定建议保持默认值不要盲目把 hash-max-listpack-value 调得很大否则一次 HGET 触发大块内存读写的延迟会让你很难受。注意编码切换是不可逆的。一旦从 listpack 升级到 hashtable即使之后删除字段、缩短 value也不会再退回 listpack。原因很简单Redis 不维护“降级”路径避免频繁切换带来额外的 CPU 和内存抖动。3. 重建依赖树从配置参数到内存布局的完整链路3.1 Hash 对象的创建流程当我们执行 HSET myhash field value 时服务端并不是一开始就创建好最终结构。整个流程大致是根据 key 在全局 dict 中查找 myhash 对应的 redisObject。如果对象不存在创建一个新的 Hash 对象此时默认按 listpack 编码初始化。如果对象已存在直接进入后续写入流程。每当写入一个新字段都会检查当前字段数量以及新字段 key/value 的长度。如果任何一项超出阈值先执行编码升级把 listpack 转换成 hashtable再执行真正的写入。这里有个隐藏的先后顺序升级发生在字段写入之前而不是之后。Redis 的做法是先判断是否满足切换编码的条件如果满足先把已有的 listpack 数据全部重新插入到新建的 hashtable 中然后才把新的字段添加到 hashtable。这么设计是为了避免先写入、再升级时要做一次多余的插入操作。最新版本的 Redis7.x已经没有 ziplist 相关配置了统一叫 listpack。如果你在网上搜索资料看到“hash-max-ziplist-entries”这样的名词说明那篇文章针对的是 Redis 5.0 或更早版本。两个配置项的作用完全一样只是内部实现从 ziplist 换成了 listpack接口和语义保持兼容。3.2 依赖关系全景图从源码依赖上看Hash 的实现可以整理成下面这张逻辑依赖链Redis 对象系统redisObject负责保存编码类型 OBJ_ENCODING_LISTPACK 或 OBJ_ENCODING_HT。编码类型决定后续调用命令时的具体实现函数。OBJ_ENCODING_LISTPACK 分支会走 listpack 的通用迭代器依次解析 key/value。OBJ_ENCODING_HT 分支会走 dict 的相关接口操作真正的哈希表。dict 依赖 ht[0]、ht[1] 两张表、rehashidx 字段、iterators 字段来支撑渐进式扩容。listpack 依赖自身的 encoding、len、data 三段式布局完成顺序读写。这套依赖关系可以用一个简单的实际场景来串联。假设你想知道某个 Hash 到底用了什么编码直接执行 OBJECT ENCODING myhash 就能看到结果。如果返回 listpack说明它还是紧凑模式如果返回 hashtable说明已经升级。这个命令在排查内存问题时非常有用我建议你把它加进日常定位流程第一时间确认编码状态再决定要不要优化数据模型。3.3 内存布局对比我用一个具体的 Hash 来对比两种编码的内存布局。假设字段如下name - tomage - 28city - beijing在 listpack 编码下内存布局大约是这样[entry: name][entry: tom][entry: age][entry: 28][entry: city][entry: beijing]每个 entry 先写 encoding 和 len再写实际数据。整数 28 可以直接以内嵌整数形式编码所以它的 entry 只有 encoding 和 data甚至没有独立的 len 字段。整体是一段连续内存Redis 用一个头部结构记录总字节数、元素个数等元信息。在 hashtable 编码下结构变成这样一个 dict 主要结构包含两个哈希表数组指针。ht[0] 有若干桶buckets每个桶是一个 dictEntry 指针。每个 dictEntry 内部有 key 指针、value 指针、next 指针。key 和 value 分别是指向 redisObject 的指针。这个差异带来的内存差距非常直观。hashtable 里每个字段都多出一个独立的 key redisObject、value redisObject 以及一个 dictEntry光对象头就是三份额外开销。对于几千个这样的小 Hash 来说内存差距能到一半以上。这也是为什么 Redis 官方坚持保留 listpack 这个看起来“不高级”的结构的根本原因。4. 核心操作流程拆解写入、读取、删除、遍历4.1 写入流程从 listpack 追加到 hashtable 插入对 listpack 编码的 Hash 执行 HSETRedis 会沿着 listpack 逐个比较字段名。这里要注意listpack 的查找是 O(n) 线性的比较完一个 entry 之后根据长度跳到下一个 entry。如果字段不存在就在 listpack 末尾追加两个 entry一个 key 一个 value。如果字段已存在则直接替换对应的 value entry。由于 listpack 是连续内存替换 value 时如果新旧长度不一致需要先删除旧 entry再把新数据插入这就会引发一次内存移动。对 hashtable 编码的 Hash 执行 HSET流程就变成对 field 做哈希计算得到索引。在对应桶的链表里查找 field。如果找到了替换 value 指针并释放旧 value。如果没找到创建一个新的 dictEntry插入链表头部。整个过程的时间复杂度是 O(1)。你可能会好奇为什么插入是头插法而不是尾插法因为头插法不需要遍历链表到底直接把新节点挂到桶的第一个位置代码更简单性能也更好。不过头插法对同一字段的重复写入并不友好因为链表顺序跟写入顺序相反但这不影响正确性。4.2 读取流程HGET 时 Redis 做了什么当我们执行 HGET myhash field 时Redis 先通过 key 找到 Hash 对象然后根据编码分派到不同的查找逻辑。listpack 编码下Redis 逐个 entry 比较字段名直到找到匹配项然后读取紧随其后的 value entry。如果字段不存在需要把整个 listpack 全部扫描一遍才能得出“不存在”的结论。这里有性能陷阱一个有着 100 个字段的 listpack Hash读取某个尾部字段可能需要比较 100 次。虽然单个比较很快在高 QPS 下还是会造成额外的 CPU 损耗。hashtable 编码下Redis 先对 field 做哈希直接定位到某个桶再沿着链表查找。理想情况下一次命中最差情况要看链表长度。除非你的业务出现严重哈希冲突否则冲突链长度会保持在个位数性能非常稳定。实际生产中如果遇到 QPS 特别高但仍然在用默认 listpack 的小 Hash可能会出现一种很有意思的现象内存占用低但 CPU 占用偏高。因为 listpack 的线性查找在字段数多时并不划算。这时候你需要权衡是保持低内存还是换更高的吞吐。我个人倾向于如果单个 Hash 的字段数会稳定超过 100与其依赖 Redis 自动升级不如一开始就设计成更小的 Hash 分片或者直接用 String JSON提前绕开性能拐点。4.3 删除流程listpack 的痛点与 hashtable 的优势HDEL 删除字段时listpack 的劣势特别明显。因为要在连续内存中删除一个 entry后续所有数据都要往前移动。假设你有一个 128 字段的 listpack Hash删除第一个字段理论上要搬动剩余 127 个字段的全部字节。虽然 Redis 内部用了 memmove速度很快但数据量大时还是会产生明显延迟。hashtable 删除则简单很多找到对应桶把目标 dictEntry 从链表中断开释放 key、value 对象更新计数。整个过程是 O(1) 的代价非常低。这也是为什么一旦 Hash 变大Redis 宁可放弃 listpack 也绝不回头。4.4 遍历流程顺序读与无序读HGETALL、HKEYS、HVALS 这类全量遍历命令在不同编码下的行为也完全不同。listpack 编码下顺序扫描所有 entry读出来的字段顺序跟写入顺序一致。这一点很实用如果你存的是一个有顺序要求的小配置对象可以用 Hash 的 listpack 编码白嫖一个“有序字典”效果。hashtable 编码下遍历顺序由哈希结果决定不是写入顺序也不是字典序而是一个接近随机的桶遍历顺序。如果你对返回顺序有要求必须在应用层自己排序。千万不要依赖 HGETALL 的顺序做业务判断因为哈希函数一旦变化顺序就变了。同一个 Redis 版本下顺序可能是稳定的但升级版本后可能完全变样。5. 渐进式 rehash大 Hash 扩容时为什么不卡5.1 rehash 触发的边界条件hashtable 编码的 Hash 在运行中如果负载因子超过阈值就会触发扩容。Redis 选择把 ht[1] 初始化成 ht[0] 的两倍大小然后把字典的 rehashidx 从 0 开始标记为 rehash 进行中。触发扩容的具体条件有两个服务器没有执行 BGSAVE 或 BGREWRITEAOF 时负载因子超过 1。服务器正在执行 BGSAVE 或 BGREWRITEAOF 时负载因子超过 5。为什么持久化期间阈值要调高因为执行 BGSAVE 时会 fork 子进程子进程需要复制父进程的页表如果此时父进程大量写入新内存就会触发 COW 复制造成额外内存和 CPU 消耗。为了降低 fork 期间的内存页变化Redis 故意推迟扩容尽量复用已有内存页。这是一个典型的内存与性能的权衡设计。5.2 渐进式 rehash 的步进机制很多人以为 rehash 是一次性把所有数据从 ht[0] 搬到 ht[1]其实不是。Redis 采用每次处理一个桶的渐进式策略。rehash 过程中每次对 dict 的增删改查操作都会顺带执行一步 _dictRehashStep把 ht[0] 上当前 rehashidx 指向的那个桶里的所有 entry重新哈希后迁移到 ht[1]然后把 rehashidx 加一。这样就把一次大搬家分摊到了后续每一次命令执行中。rehash 期间所有新插入的字段只会被放到 ht[1]ht[0] 只减不增直到全部迁移完成Redis 再释放 ht[0]把 ht[1] 变成新的主表。这个设计的核心价值在于扩容操作不会阻塞 Redis 主线程太长时间。无论你的 Hash 有多大最多只会在某一次命令执行时搬迁一个桶延迟始终可控。相比一次性搬迁渐进式 rehash 避免了“扩容瞬间卡顿数百毫秒”的极端情况。不过要注意rehash 期间查找一个字段需要同时查两张表。Redis 的做法是先查 ht[0]没命中再查 ht[1]。因为大部分数据应该在 ht[0] 中这样的顺序能尽量提升命中率。为了保证迁移不遗漏新字段一律进 ht[1]。5.3 遍历时遇到 rehash 怎么办如果有人在 rehash 期间执行 HGETALL 或者 SCANRedis 会同时遍历两张表。它先把 ht[0] 上从 rehashidx 到末尾的桶扫完再扫 ht[1] 的全部桶。这种顺序设计保证了已经迁移到 ht[1] 的 entry 不会被漏掉也不会被遍历两次。但对于 SCAN 这种增量式遍历rehash 期间可能出现某个 key 被返回多次或漏掉的情况这是所有动态哈希表遍历的固有难题。Redis 通过反向二进制位迭代器有效降低了这种概率但无法完全避免。如果你在生产环境做大 Key 排查恰好又遇到 rehash会发现 SCAN 的结果可能有一点点抖动。这是正常现象不影响正确性。真正的业务热点数据建议在低峰期做迁移操作把影响降到最低。6. 哈希冲突与扩容策略的底层原理6.1 为什么链表法在这里够用hashtable 解决哈希冲突的方式是链表法也就是每个桶下面挂一个链表。当多个字段算出来同一个索引时它们会依次挂到同一个桶上。跟开放寻址法相比链表法的最明显优点是删除简单。开放寻址法删除一个元素往往需要特殊标记否则会破坏后续查找的路径链表法则直接断开节点即可。另一个优点是负载因子可以容忍更高允许桶数量略小于元素数量因为链表可以无限增长下去。缺点则是需要额外存储 next 指针内存占用偏高。Redis 选链表法还有一个现实原因它的主线程是单线程。链表法在插入和删除时不需要考虑锁竞争代码路径更直接。在 C 语言层面维护一个单向链表也比实现一个复杂的探测序列更简洁、更不容易出错。6.2 哈希函数的碰撞概率与安全视角Redis 的 dict 哈希函数不只追求速度快还考虑了一个安全问题防止恶意构造 key 造成哈希碰撞攻击。假如哈希函数非常弱攻击者可以精心构造大量哈希值相同的字段让所有 entry 都挂到同一个桶下面把 O(1) 的操作变成 O(n)最终拖垮 Redis。因此 Redis 在部分版本中引入了 SipHash它带有一个随机种子每次进程启动时种子不同。这就导致同一个字段在不同启动实例中算出的哈希值不一样攻击者无法提前预测哈希结果也就很难构造针对性的碰撞攻击。这个细节常被忽略但它是 Redis 作为基础组件在安全性上很重要的设计。6.3 容量增长为什么是 2 的幂次Redis 的 dict 扩容时新桶数量必须是 2 的幂次。原因很纯粹用位运算替代取模。当你需要计算一个哈希值 h 对应的桶索引时正常数学逻辑是 h % size但取模运算在 CPU 层面比较贵。如果 size 是 2 的幂次那么 h % size 等价于 h (size - 1)一步位运算搞定。举个例子size 为 8 时size - 1 就是 7二进制是 111任何哈希值跟 111 做按位与只保留低 3 位正好是 0 到 7 的索引。这个技巧在哈希表实现里非常常见不只是 Redis 在用。6.4 遇到大量冲突时的连锁反应极端情况下如果某个 Hash 字段数极多且哈希函数对这批字段分布特别不均匀会导致某个桶链表特别长。这会让所有针对该字段的查询都退化成链表扫描性能一落千丈。但别慌Redis 哈希函数本身分布很好正常业务几乎不可能碰到这种问题。真正需要担心的是你有没有把一个 Hash 当成无限大的集合来用塞了几百万个字段。这种情况下即便哈希分布均匀内存、遍历、删除都会是大问题。我见过一个大 Hash 存了几百万条用户行为记录平时读写只用了其中一小部分但一次 HGETALL 直接把服务打挂。这种问题跟编码无关纯粹是数据模型设计失误。7. 常见问题与排查技巧实录7.1 为什么我的小 Hash 变成了 hashtable排查这个问题先执行 OBJECT ENCODING yourkey 确认编码。如果是 hashtable再看具体原因单个 value 是否超过了 hash-max-listpack-value 配置值默认是 64。中文、Emoji、Base64 这类字符会让实际字节数远超你肉眼看到的内容。比如一个 20 字的 UUID 是 36 字节一个 20 个中文的词组就是 60 字节离 64 很近了。字段数是否超过了 hash-max-listpack-entries 配置值。这里给一个快速验证方法直接暴力写入一个超长 value 触发升级然后用 OBJECT ENCODING 确认再用 DEBUG OBJECT 查看对象信息把编码状态、内存大小都打出来。这套流程能帮你形成第一直觉遇到 Hash 相关内存问题先看编码。7.2 内存一下子涨了很多怎么定位某一天你的 Redis 内存突然多了好几 GB点开监控发现一堆 Hash 键从 listpack 变成了 hashtable。最直接的原因是字段数或单个 value 长度超过了阈值。想确认具体是哪些键可以用 SCAN 遍历然后逐个执行 OBJECT ENCODING但千万别用 KEYS线上会卡。更实用的是提前预防。如果你知道某个业务的 Hash 必然要存长文本就不要让它走 listpack 这条省钱路线了直接把它设计成 hashtable然后在业务逻辑上避免全量读写。反过来若是短字段业务可以适当调大 listpack 阈值尽量让数据留在连续内存中。7.3 HGETALL 大 Key 卡顿怎么办Hash 变成大 Key 之后HGETALL、HKEYS、HVALS 这类命令会一次性读出所有字段造成网络包过大、内存分配过高、主线程阻塞。解决方案不是优化 Redis 编码而是从业务侧改造把一个大 Hash 拆成多个小 Hash按业务维度分片。使用 HSCAN 分批读取每一批只取一定数量的字段再在应用层合并。如果是纯缓存数据干脆换成 String 存储序列化后的整体对象读一次搞定。这三种方案各有优劣。拆 Hash 能保持 Redis 的字段级操作能力HSCAN 不改变数据结构但增加应用层复杂度String 则牺牲字段级更新能力换取极简读逻辑。我个人倾向于优先拆 Hash因为字段级操作是 Hash 的核心优势拆完以后还可以顺手做数据过期操作更灵活。7.4 如何判断一个 Hash 是否需要拆分提供一个简单的判断标准如果单个 Hash 字段数接近或超过 10 万几乎必然要拆。如果单个字段 value 经常超过几百字节也要考虑换成 JSON。如果业务总是需要操作大部分字段避免让 Hash 承担“大对象”的职责。如果内存非常敏感且字段短延迟暂时可接受小 Hash 用默认 listpack 就好。7.5 渐进式 rehash 会不会影响访问延迟正常情况不会。每次命令执行只搬运一个桶耗时通常在微秒级。但要小心一种特殊情况你的 Hash 里很多字段在哈希计算后恰好落在同一个桶也就是说桶负载极不均匀一次迁移就可能要移动几百万个 entry。这种极端场景会把单次命令卡到几十毫秒甚至更久。解决办法是提前监控大 Hash避免出现超大规模字段的集合。真的遇到了选择在业务低峰期主动触发扩容或者直接重启实例让 dict 重建也能绕开 rehash 高峰期。7.6 使用 DEBUG OBJECT 查看 Hash 内部信息排查时可以用DEBUG OBJECT myhash输出里包含 serializedlength、encoding 等信息。serializedlength 代表序列化后的近似内存大小对估算该键所占内存有参考意义。注意这是调试命令线上谨慎使用频繁执行会带来额外开销。7.7 日常监控指标推荐建议在两个层面监控 Hash对象层面定期抽样执行 OBJECT ENCODING统计 listpack 与 hashtable 的比例变化。内存层面关注 used_memory 的突增结合 RDB 分析工具找出大 Key。如果比例在短时间内出现大量从 listpack 变成 hashtable 的键说明业务数据模型发生了变化要么是字段数变大要么是 value 长度变大需要及时跟进。8. 个人实操经验与最终建议先分享一个我在实际项目中踩过的坑。某个内部系统把用户的基础信息存成 Hash每个用户一个键字段大概 30 个左右value 多是中文字段。一开始内存表现很好listpack 编码单实例稳定跑了大半年。后来产品加了一个“备注”字段随手存了一段 100 多字的文本结果一夜之间所有用户键全部升级成 hashtable。内存占用直接翻了一倍多扩容了两台机器才缓过来。教训很直接不要忽略编码切换的临界点。你永远不知道业务什么时候会加一个超长字段所以对 Hash 这种数据结构上线前就要明确约定 value 长度上限最好在代码层做强制校验。如果你无法保证长度就把这个键设计成真正的大表结构接受 hashtable 的内存成本而不是让它半路升级。还有一个细节想提醒Redis 源码里的 listpack 和 dict 都经过了很多版本的迭代不同版本的行为可能有所不同。比如 Redis 6.2 之前还叫 ziplist7.0 换成 listpack 后内存布局更紧凑连续内存碎片也更少。但命令层的表现几乎一致所以你只需要关注 OBJECT ENCODING 返回的结果即可。最后如果你只是想用 Redis 的 Hash 存一些少量字段的配置直接默认配置就好不用刻意调参。如果你确认自己的字段数永远不会超过 128 且 value 很短可以把 hash-max-listpack-entries 和 hash-max-listpack-value 稍微调大让更多键维持 listpack 编码。这种调优带来的收益在大量小对象场景下非常明显能省下不少内存成本。但请记住一个原则任何编码层面的优化都是锦上添花真正决定 Redis 性能的是你对业务数据模式的理解。Hash 字段多、值长、频繁全量读写这些问题不是调配置能解决的需要从模型设计上做取舍。懂得什么时候该用 Hash什么时候该拆成多个键什么时候该换成别的数据结构比记住源码里的每一行代码都重要。希望你读完这篇之后再看 Redis 的 Hash不再把它当成一个黑盒。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑