跳到主要内容
约 8 分钟8 张卡

Redis 数据结构

Redis 数据类型定义命令语义,内部编码定义存储方式。本文以 Redis 6、7 为基线,讲解类型选择、编码与复杂度。

1. 面试核心​

先记住这几条,再读正文
  • 键与值对象。 String 保存字节串,键空间字典则负责从键定位值对象。卡 · 字符串
  • 数据类型选择。 Hash 按字段读写,Set 去重,ZSet 按分数与成员顺序排列。卡 · 数据类型与场景
  • 紧凑编码。 Redis 6 的 QuickList 用双向链表连接 ZipList 块,以减少小元素的内存开销。Redis 7.0 起,块内结构改为 Listpack。卡 · 列表
  • 有序集合索引。 ZSet 的 skiplist 编码以字典查成员,用跳表维护分数顺序。卡 · 有序集合
  • 原子递增。 INCR 原子完成整数递增,客户端 GET 后 SET 存在丢失更新。卡 · 字符串

2. 数据类型与场景​

类型常用操作面试中的选择依据
StringGET、SET、INCR独立值、计数器、序列化缓存
HashHGET、HSET、HINCRBY同一对象的多个字段,可局部读写
ListLPUSH、RPOP、LMOVE有序序列、双端队列;弹出后不会自动确认
SetSADD、SISMEMBER、SINTER去重、成员判断、交并差
ZSetZADD、ZSCORE、ZRANGE排名、分数区间、延迟任务的时间索引
StreamXADD、XREADGROUP、XACK日志式消息、消费组、待确认消息

对 List 执行 GET 会得到 WRONGTYPE。TYPE key 查看逻辑类型。OBJECT ENCODING key 查看内部编码。应用按类型使用命令,编码用于诊断实现与内存成本。

3. 字符串​

String 保存文本、序列化对象和二进制字节。Redis 6、7 支持 int、embstr、raw 编码。符合整数编码条件的值可用 int 保存。短字符串可将对象头与 SDS 连续分配为 embstr。raw 将对象头与 SDS 分开分配。

SDS 显式记录长度,因此能保存零字节。它还保存分配容量,以减少连续增长时的重复分配。扩容仍需分配或复制内存。

SET interview:counter 41
INCR interview:counter
GET interview:counter

结果为 42。INCR 原子完成取值、加一和写回。它要求值能表示为有符号 64 位整数。非整数或溢出会报错。

两个客户端都 GET 到 41,再各自 SET 42,只会增加一次。需要计数时应直接使用 INCR。

4. 列表​

List 保持插入顺序,允许重复元素。LPUSH 配合 RPOP 构成先进先出队列。同端入出则是后进先出。单元素头尾操作为 O(1)。索引访问需沿列表定位,成本随位置增长。

Redis 6 的 List 使用 QuickList。QuickList 用双向链表连接多个 ZipList 存储块。ZipList 连续存储元素,减少逐元素指针开销。单块插入仍需移动数据,并可能触发连锁更新。

Redis 7.0 起,QuickList 的紧凑块由 Listpack 替代 ZipList。Listpack 将反向遍历信息保存在当前条目尾部。它避免 ZipList 的前一条目长度变化引发连锁更新。

LPUSH interview:queue first second
RPOP interview:queue

弹出结果是 first,列表剩下 second。普通弹出会删除元素。消费者随后崩溃,就失去待处理记录。

Redis 6.2 起,BLMOVE 可将任务原子移到处理中列表。成功后确认删除,超时任务再交给其他消费者。重复处理由业务幂等控制。

5. 有序集合​

ZSet 的 member 唯一,score 可重复。同分成员按字节字典序排列。更新已有 member 的 score 会调整排序位置。

score 使用双精度浮点数。整数在 [-2^53,2^53] 范围内可精确表示。金额可先转为最小货币单位,再检查是否超出精确范围。

Redis 6 的小 ZSet 可用 ZipList。Redis 7.0 起由 Listpack 替代 ZipList。常规 skiplist 编码组合字典与跳表。字典用于按 member 查分数,跳表支持有序导航。节点各层记录前向指针和跨度,节点另有后退指针。

高层: [2] ----------> [6]
低层:[1]->[2]->[3]->[4]->[5]->[6]

查找 5 时,先在高层跳过较小节点。下一跳超过目标时,下降一层继续前进。跨度记录跨过的成员数,用于计算排名。

跳表导航的期望成本为 O(log N)。返回 M 个成员的区间查询还需 O(M)。

ZADD interview:rank 100 alice 100 bob 80 carol
ZRANGE interview:rank 0 -1 WITHSCORES
ZADD interview:rank 120 alice

首次查询顺序为 carol/80、alice/100、bob/100。更新 alice 后仍有三个成员,alice 移到最后。

6. Hash 与 Set​

Hash 保存字段到字节串值的映射。HSET 可更新单个字段,减少整体对象的传输。Redis 6 使用 ZipList 或 hashtable 编码。Redis 7.0 起由 Listpack 替代 ZipList。Redis 6 和 7.0 的 TTL 作用于整键。

Set 保存无序、唯一的成员。Redis 6 和 7.0 使用 intset 或 hashtable 编码。元素内容、大小和数量共同影响编码选择。Redis 7.2 增加了 listpack 编码。

SISMEMBER 判断成员是否存在。集合求交和全量读取需要处理更多元素。因此,类型相同也需按具体命令估算复杂度。

7. 字典与渐进式 rehash​

键空间与 hashtable 编码都使用字典。Redis 6、7 字典用桶和链式结构处理哈希冲突。

扩缩容时,字典准备第二张哈希表。rehash 按桶逐步把旧表元素迁移到新表。迁移期间,查找按需检查两张表,新元素写入新表。迁移完成后释放旧表。

普通字典操作会推动部分迁移。启用 activerehashing 时,定时任务也会推进键空间迁移。这样可分摊整表迁移的耗时。分配新桶数组和处理长冲突链仍会消耗时间。

这套机制属于字典实现。String 的值编码、ZipList、Listpack 和跳表各有自己的结构。

8. 紧凑编码与可观察性​

紧凑编码将多个元素连续存放,减少指针与分配开销。代价是查找需要扫描,插入需要移动部分数据。容量条件不再满足时,Redis 转换为适合较大对象的编码。

类型和命令语义保持不变。编码转换规则则依赖版本、配置和数据。诊断时可对练习键执行以下命令。

TYPE interview:rank
OBJECT ENCODING interview:rank
MEMORY USAGE interview:rank

三者分别检查类型、编码和内存估计。删除元素后是否转换回紧凑编码,需按具体实现判断。

9. SCAN 游标遍历​

SCAN 从游标 0 开始,按返回游标继续迭代。返回游标再次为 0 才表示本轮完成。

SCAN 0 MATCH interview:* COUNT 100

COUNT 是工作量提示,返回条数可以多于或少于该值。MATCH 过滤后可返回空批次,游标却仍不为 0。

完整迭代能返回全程存在的元素,但同一元素可重复返回。遍历期间新增或删除的元素,是否出现没有保证。批处理应去重或保证动作幂等。SCAN 适合渐进遍历,数据快照需另行实现。

10. Stream 与专用操作​

Stream 按消息 ID 保存追加记录。消费组通过 PEL 跟踪已投递但未确认的消息。XACK 移除待确认记录,消息本体仍由删除或裁剪管理。消费者故障后,可用 XCLAIM 认领待确认消息。Redis 6.2 起还可用 XAUTOCLAIM 自动扫描并认领。

任务执行成功但确认丢失时,会再次投递。业务应通过幂等标识处理重复。裁剪策略也需保留待恢复的消息内容。

Bitmap 是 String 上的位操作。它适合稠密编号,稀疏的大偏移会占用更多空间。HyperLogLog 估计去重数量,标准误差约为 0.81%。它只维护估计状态,精确成员判断应使用 Set。

Geo 用于地理位置与距离范围检索。它支持候选筛选,路线规划还需专门的道路模型。

单命令原子性和多命令协调的区别见 原子命令与事务。

11 · 面试题卡片​

本页题目与参考回答

11.1 Redis 的 String、Hash 与键空间字典分别是什么?​

考察重点

  • 逻辑类型与内部编码。
  • SDS 与二进制安全。

相关内容:字符串、Hash 与 Set。

参考回答

String 保存字节串,Hash 保存字段映射。

键空间字典负责按键查找值对象。String 的 int、embstr、raw 是其内部编码。字典定位与值的存储分属两个层次。

11.2 缓存对象、去重、排行榜和可靠任务分别怎样选类型?​

考察重点

  • 操作需求与数据结构。
  • 队列确认和幂等。

相关内容:数据类型与场景、Stream 与专用操作。

参考回答

整体对象可用 String,字段级读写可用 Hash。

精确去重用 Set,按分数排名用 ZSet。可靠任务可用处理中 List 或 Stream 消费组。两者都需超时恢复和业务幂等。

11.3 Redis 为什么给同一数据类型提供多种编码?​

考察重点

  • 空间与操作成本。
  • 版本和转换条件。

相关内容:列表、紧凑编码与可观察性。

参考回答

紧凑编码节省小对象的指针和分配成本。

Redis 6 的 QuickList 用链表连接多个 ZipList 块。Redis 7.0 起将这些紧凑块替换为 Listpack。对象变大后,分块或哈希结构可减少操作成本。应结合目标版本、配置和 OBJECT ENCODING 判断实际结构。

11.4 ZSet 为什么同时使用字典与跳表,范围查询的成本是多少?​

考察重点

  • 按成员查找与有序访问。
  • 分数、排名与结果数量。

相关内容:有序集合。

参考回答

字典支持按 member 查分数,跳表维护分数与成员顺序。

跨度还支持计算排名。常规跳表导航期望为 O(log N),输出 M 条再加 O(M)。Redis 6 的小集合可用 ZipList,内部访问方式不同。Redis 7.0 起改用 Listpack。

11.5 INCR 与 GET 后 SET 有什么并发差异?​

考察重点

  • 单命令原子性。
  • 多命令与跨键边界。

相关内容:字符串、Stream 与专用操作。

参考回答

INCR 在一个原子命令内完成递增。

GET 后由客户端计算再 SET,存在并发覆盖窗口。两个客户端都读到 1,再各写 2,会丢掉一次增加。复杂条件需使用相应原子命令、脚本或 WATCH 重试。

11.6 List 消费者取出任务后崩溃,怎样减少任务丢失?​

考察重点

  • 弹出与确认。
  • 重投递和业务幂等。

相关内容:列表、Stream 与专用操作。

参考回答

用 BLMOVE 将任务原子移入处理中列表,可保留恢复依据。

处理成功后删除,超时未完成则重投。Stream 消费组可用 PEL 与认领机制管理该过程。任务完成但确认丢失时仍会重复,业务需保持幂等。

11.7 SCAN 返回空数组时能结束吗,它会重复或漏掉元素吗?​

考察重点:游标终止、COUNT 与并发变更。

相关内容:SCAN 游标遍历。

参考回答

应以返回游标为 0 判断完成,空数组只代表本批没有匹配项。

COUNT 是提示值,返回数没有固定保证。完整迭代会返回全程存在的元素,但可重复。期间变化的元素是否出现没有保证。

11.8 Redis 字典怎样扩容,渐进式 rehash 期间如何查找和插入?​

考察重点:双表迁移、操作分摊与延迟边界。

相关内容:字典与渐进式 rehash。

参考回答

字典建立新表,按桶逐步迁移旧表元素。

迁移期间查询按需检查两张表,新插入写入新表。普通操作和定时任务可推进迁移。它分摊迁移耗时,但新表分配和长链处理仍有成本。

12. 参考资料​