Redis 数据类型有哪几种?

#中间件#面试八股

Redis 数据类型有哪几种?

结论:Redis 对外暴露的是「数据结构 + 命令语义」,不是单纯的 KV。常用的是 String、List、Hash、Set、ZSet 五种基础类型,另有 Bitmap、HyperLogLog、GEO、Stream 四种扩展类型。选型的判断标准只有一条:你的读写模式与底层编码能否对上,而不是哪个类型听起来高级。

一、九种类型的能力对照

类型语义底层编码(小→大)典型场景复杂度锚点
String字节串 / 整数 / 位图载体int、embstr、raw计数器、分布式锁、Session、序列化对象GET/SET O(1)
List有序可重复的双端队列quicklist(listpack 节点)简单队列、最新 N 条、栈头尾 O(1),按下标 O(N)
Hash字段 → 值的映射listpack → hashtable对象属性、购物车、配置项单字段 O(1)
Set无序去重集合intset → listpack → hashtable标签、共同好友、抽奖去重增删查 O(1),交并差 O(N)
ZSet带分数的有序去重集合listpack → skiplist + dict排行榜、延时队列、滑动窗口增删 O(log N),ZRANK O(log N)
BitmapString 上的位操作同 String(raw)签到、布尔标记、UV 粗略统计SETBIT/GETBIT O(1)
HyperLogLog基数估算稀疏/稠密 HLL千万级 UV 去重计数PFADD O(1),误差 0.81%
GEO经纬度 + 成员ZSet(GeoHash 编码为 score)附近的人、门店距离排序GEOSEARCH O(log N)
Stream追加日志 + 消费组rax + listpack轻量消息队列、事件溯源XADD O(1),XRANGE O(N)

二、为什么"看起来像同一种类型"却要分编码

写入 → 判断规模与元素大小 → 选择紧凑编码 → 触及阈值 → 单向升级为散列编码
       │                      │                        │
       │                      ├─ listpack:连续内存,缓存友好,适合小集合
       │                      └─ hashtable/skiplist:指针结构,适合大集合
       └─ 阈值来自 redis.conf:hash-max-listpack-entries 等

关键点在于:类型是逻辑契约,编码是物理实现。同一个 Hash,字段少时是一块连续内存,字段多时变成哈希表;命令不变,但内存占用与单次访问的局部性完全不同。所以面试里回答"有哪几种"之后,通常会立刻追问"底层用什么实现",两层都要答。

三、命令速查与选型样例

# String:计数器 + 过期,一条命令完成"不存在则初始化"
SET page:view:20261103 0 EX 86400 NX
INCR page:view:20261103

# Hash:只更新部分字段,避免整体读改写
HSET user:1001 name "lin" level 3
HINCRBY user:1001 level 1

# ZSet:排行榜(分数相同按成员字典序)
ZADD rank:week 980 u1001 960 u1002
ZREVRANGE rank:week 0 9 WITHSCORES

# HyperLogLog:按天 UV,误差换内存
PFADD uv:20261103 u1001 u1002
PFCOUNT uv:20261103
PFMERGE uv:week uv:20261101 uv:20261102
// 选型示例:用 Hash 存对象属性,只为变更字段写一次,而不是整对象覆盖
Boolean ok = redis.opsForHash().putIfAbsent("user:1001", "level", "3");
if (Boolean.FALSE.equals(ok)) {
    redis.opsForHash().increment("user:1001", "level", 1);
}

四、一次需求里怎么选类型

读到的数据要不要保持顺序?
├─ 否 → 按"整体 / 局部"再分
│        ├─ 整体覆盖、可当黑盒 → String
│        └─ 只改某几个字段     → Hash
└─ 是 → 是否需要去重 / 带分值排序?
         ├─ 只是先进先出、允许重复 → List(LPUSH + BRPOP)
         ├─ 只要成员、不排序       → Set(交集/并集/差集)
         └─ 要按分数取范围         → ZSet(排行榜、延时队列、滑动窗口)

统计类需求再补一层判断:要精确成员就用 Set 或 Bitmap,只要基数就用 HyperLogLog;带经纬度的"附近"直接用 GEO,不必自己算距离再排序。

五、常见追问

  • 问:String 存 JSON 和 Hash 存字段,怎么选?答:整体读写、字段固定用 String;需要单字段更新、字段数量不定用 Hash。Hash 字段过多会退化成 hashtable,收益下降。
  • 问:Set 和 ZSet 都能去重,差别在哪?答:ZSet 多一个 score,能排序、能按分数区间取,代价是每个元素多存一个分值并维护跳表。
  • 问:Bitmap 能替代 Set 做签到吗?答:能,前提是用户 ID 连续且可映射为偏移量;ID 稀疏(如雪花 ID)会浪费大量内存。
  • 问:HyperLogLog 为什么不能取具体成员?答:它只保存概率性寄存器,不保存原始元素,因此只能计数、不能判断"某用户是否在里面",需要精确判断时用 Set 或 Bitmap。
  • 问:Stream 与 List 做队列的区别?答:Stream 有消费组、ack、pending 列表,能应对消费者崩溃;List 的 BLPOP 取出即丢,没有确认语义。
  • 问:为什么没有单独的"队列类型"?答:List 的头尾操作已是 O(1),配合阻塞命令即可当队列;需要可靠性时升级到 Stream,Redis 不再引入新的重结构。
  • 问:GEO 底层是什么?答:成员作为 ZSet 的 member,GeoHash 编码后的 52 位整数作为 score,所以 GEO 天然支持按距离排序与范围查询。

小结

  • 五种基础类型解决"值是什么形状",四种扩展类型解决"特定统计/排序/消息"问题。
  • 每种类型都有紧凑编码与散列编码两档,阈值由 redis.conf 控制,且只升不降。
  • 选型先看访问模式(整体/局部、有序/无序、精确/估算),再看内存与复杂度,最后才看命令是否顺手。
笔记加载中…