Redis 数据类型与底层结构

举报
yd_216253085 发表于 2026/11/03 14:40:46 2026/11/03
【摘要】 从 9 种数据类型到 7 种底层编码,讲清转换阈值与选型依据

Redis 常被当作"大号 HashMap"使用,但它的内存开销、命令复杂度和性能拐点都由底层编码决定。本文以 Redis 7.x 为基准,从 redisObject 出发,逐一拆解 String、List、Hash、Set、ZSet 的编码方式与转换阈值,并给出可以直接落地的选型建议。适合已经会用的读者补齐原理。

一、对象模型:类型与编码是两回事

每条 Redis 命令最终都落到一个 redisObject 上:

typedef struct redisObject {
    unsigned type:4;       // 对外类型:string/list/hash/set/zset/stream
    unsigned encoding:4;   // 内部编码:int/embstr/raw/listpack/...
    unsigned lru:24;       // LRU 时钟,或 LFU 的 ldt + counter
    int refcount;          // 引用计数,用于共享整数对象
    void *ptr;             // 指向真正的数据结构
} robj;

type 决定命令能不能执行,encoding 决定内存布局和单条命令的复杂度。同一类型在数据量变化时会切换编码,阈值由 redis.conf 中的 *-max-listpack-* 系列参数控制。用 OBJECT ENCODING key 可以直接查看当前编码。

| 类型 | 小数据编码 | 大数据编码 | 主要阈值 |

| --- | --- | --- | --- |

| String | int / embstr | raw | 44 字节 |

| List | quicklist + listpack | quicklist + listpack | 单节点 8 KB |

| Hash | listpack | hashtable | 128 个 / 64 字节 |

| Set | intset / listpack | hashtable | 512 / 128 个 |

| ZSet | listpack | skiplist + dict | 128 个 / 64 字节 |

二、String:int、embstr 与 raw

String 是唯一可以直接存二进制、数字和 JSON 的类型。三种编码的判定顺序是:

  1. 值是整数且能用 long 表示,编码为 int,ptr 直接存数值,不再额外分配。
  2. 长度小于等于 44 字节,编码为 embstr,redisObject 与 SDS 在一次 malloc 中连续分配。
  3. 其余情况编码为 raw,对象头与 SDS 分开分配。

44 这个数字来自内存对齐:redisObject 占 16 字节,sdshdr8 头占 3 字节,再加 1 字节结束符,64 字节的 jemalloc 小对象块刚好容纳 44 字节内容。

SDS 相比 C 字符串多了 len 和 alloc 字段,因此 STRLEN 是 O(1);追加时按需预分配,小于 1 MB 时翻倍,大于 1 MB 时每次加 1 MB;同时二进制安全,可以存 \0。APPEND 或 SETRANGE 会让 embstr 退化为 raw,因为 embstr 被设计为只读。

典型用法:计数器(INCR)、分布式锁(SET NX PX)、Session、Bitmap。

三、List:quicklist 套 listpack

Redis 3.2 起 List 统一为 quicklist:一个双向链表,每个节点挂一个压缩块。Redis 7.0 把节点内的 ziplist 换成了 listpack,解决了 ziplist 在中间插入时级联更新 prevlen 导致的 O(n²) 问题。

listpack 的结构是:4 字节总长度、2 字节元素个数、若干条目(编码 + 数据 + 变长 backlen)、1 字节 0xFF 结束符。backlen 记录当前条目自身的长度,而不是前一条目的偏移,因此修改一个条目不会影响其他条目。

每个 quicklist 节点的大小由 list-max-listpack-size 控制,默认 -2 表示单节点不超过 8 KB;正数表示按元素个数限制。list-compress-depth 可以对首尾之外的节点做 LZF 压缩,默认 0 不压缩。

提示:LRANGE key 0 -1 在大 List 上是 O(n),且会一次性分配大块内存,线上应避免;分页请用固定窗口。

四、Hash:listpack 与 hashtable

字段少时 Hash 用 listpack 顺序存储,查找是遍历,但数据量小,常数极低。满足以下任一条件即转为 hashtable:

  • 字段数超过 hash-max-listpack-entries,默认 128;
  • 任一字段名或值的长度超过 hash-max-listpack-value,默认 64 字节。

hashtable 就是标准的链地址法字典,负载因子为 1 时扩容、为 0.1 时缩容,采用渐进式 rehash:同时保留 ht[0] 和 ht[1],用 rehashidx 记录进度,每次增删改查顺带迁移一个桶,定时任务再补 1 ms。因此 rehash 期间内存会短暂翻倍。

编码转换是单向的:一旦变成 hashtable,即使删到只剩一个字段也不会退回 listpack。这也是"删完大 Hash 内存不降"的常见原因。

五、Set:intset、listpack 与 hashtable

Set 有三段式编码:

  • 所有元素都是整数且数量不超过 set-max-intset-entries(默认 512),使用 intset;
  • Redis 7.2 起,非整数但元素数不超过 set-max-listpack-entries(默认 128)且单个元素不超过 set-max-listpack-value(默认 64 字节),使用 listpack;
  • 其余使用 hashtable。

intset 是一段连续内存:encoding(2/4/8 字节)、length、有序数组。查找用二分,复杂度 O(log n)。插入超出当前位宽的值会触发整体升级,例如插入 2^40 会把所有元素从 2 字节扩到 8 字节,且不可逆。

Set 适合去重、SINTER 求共同好友、SRANDMEMBER 抽奖。元素较多时 SMEMBERS 会阻塞主线程,应改用 SSCAN。

六、ZSet:listpack 与 skiplist

ZSet 在元素数不超过 zset-max-listpack-entries(128)且成员长度不超过 zset-max-listpack-value(64 字节)时用 listpack,按 score 有序排列,插入需要 O(n) 搬移。

超过阈值后转为 skiplist + dict 的组合:

  • dict 保存 member → score,ZSCORE 为 O(1);
  • skiplist 按 score 排序,节点带 span 字段,ZRANK 可以在 O(log n) 内算出排名。

跳表最大层数 32,每层晋升概率 0.25,期望查询复杂度 O(log n)。Redis 选跳表而不是红黑树,原因有三:范围查询只需在底层链表顺序走、实现和调试简单、并发场景下局部修改更容易加锁。

七、扩展类型

Bitmap 本质是 String,最大偏移 2^32 - 1,即 512 MB。适合签到、活跃用户统计,BITCOUNT 按字节查表,BITOP 可做与或非。

HyperLogLog 也存成 String,固定 12 KB:16384 个 6 位寄存器,标准误差 0.81%。只支持 PFADD、PFCOUNT、PFMERGE,不能取回元素,适合 UV 去重计数。

GEO 底层是 ZSet,把经纬度编码成 52 位 geohash 存进 score,支持 GEOADD、GEOSEARCH。

Stream 是唯一为消息队列设计的类型:rax 索引加 listpack 存储,支持消费组、XACK 与 PEL(Pending Entries List),但堆积时内存增长快,重吞吐场景仍建议用 Kafka 或 RocketMQ。

八、怎么查、怎么调

redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> HSET user:1 name tom age 18
(integer) 2
127.0.0.1:6379> OBJECT ENCODING user:1
"listpack"
127.0.0.1:6379> MEMORY USAGE user:1
(integer) 104

MEMORY USAGE 返回 key 及其值占用的字节数,定位大 key 时比 STRLEN 更准确。调整阈值只能在写入前生效,已存在的 key 不会因为改配置而回退编码。

常见坑 / 最佳实践

  1. 不要为了"省内存"盲目调大 listpack 阈值。 128 个元素、64 字节是压测后的平衡点,调到 1024 会让单次 HGET 的遍历成本上升,主线程更容易被拖慢。
  2. Hash 字段值超过 64 字节会直接退化。 缓存 JSON 对象时如果某个字段是长文本,整个 Hash 都会变成 hashtable,内存可能翻 2~3 倍,应把长文本拆到独立 String。
  3. 禁止在生产环境执行 KEYS *、大 Hash 的 HGETALL、大 Set 的 SMEMBERS。 用 SCAN、HSCAN、SSCAN 的游标方式分批取。
  4. ZSet 用时间戳做 score 时注意精度。 score 是 double,毫秒时间戳在 2^53 内仍可精确表示,但微秒级会丢精度,建议用「秒 + 序号」拼接或改用 Stream。
  5. List 当队列时用 LPUSH + BRPOP,并关注 list-max-listpack-size。 队列很长时节点数增多,内存碎片和遍历成本都会上升。

小结

  • Redis 的 type 是对外语义,encoding 才是内存与复杂度的决定因素,用 OBJECT ENCODING 验证。
  • String 三态由 44 字节决定;List 是 quicklist 套 listpack;Hash、Set、ZSet 在数据量小时都用 listpack,超过阈值转为 hashtable 或 skiplist。
  • 编码转换基本单向,删数据不会退回紧凑编码,容量规划要按峰值估算。
  • 选型先看访问模式:整取用 String,部分更新用 Hash,排序用 ZSet,去重用 Set,计数用 HLL 或 Bitmap。
  • 所有阈值改动都要在写入前完成,并通过压测确认主线程延迟没有恶化。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。