跳到主内容

一文搞懂 Redis 数据结构选型:从 String 到 Stream 的场景映射决策表 ​

把 Redis 当成「大号 HashMap」是很多团队踩过的第一个坑:对象一律序列化成 JSON 塞进 String,想排个序就把整个列表读出来在应用层排,想做个队列就 LPUSH 一堆序列化串。功能上确实都能跑,但改一个字段要读-改-写全量数据,内存涨得比业务快,队列失败即丢,等数据量上来后这些代价会集中兑现。选对数据结构不是炫技,它直接决定了同一份数据占多少内存、一次更新碰多少字节、以及哪些并发问题可以交给 Redis 原子命令去解决。这篇文章先盘清九大结构的能力边界,再给一张从 String 到 Stream 的场景映射决策表,最后用两段实操把最容易选错的两个场景讲透。

先摸清九大结构的能力边界 ​

选型前得知道每个结构擅长什么、不能做什么。String 是最通用的字节串,适合缓存 JSON、计数器和分布式锁,底层对短字符串有 embstr 编码优化。Hash 是字段级的映射表,最大价值在于可以只读写其中一个字段,适合对象缓存和购物车。List 是双端链表(quicklist 实现,7.x 起每个节点内部用 listpack 存储),支持阻塞弹出,适合简单任务队列和最新列表。

Set 是无序去重集合,核心能力是 SINTER、SDIFF、SUNION 这类集合运算,适合标签、共同关注、抽奖去重。ZSet 每个成员带一个 double 类型的 score,底层是跳表加哈希表的双结构,天然按分数排序,排行榜和延迟任务的标配。Bitmap 是位数组,一个亿级用户的签到场景每天只占十几 MB,配合 BITOP 还能做交集并集。

剩下三个容易忽略:HyperLogLog 用固定约 12KB 内存估算任意规模的基数,标准误差约 0.81%,只适合 UV 这类允许误差的计数;GEO 底层就是 ZSet,把经纬度编码成 52 位整数存进 score,适合附近的人、附近门店;Stream 是 5.0 引入的日志型结构,带消息 ID、消费者组和 Pending 列表,是目前 Redis 内做可靠队列的正确答案。

还有一条底层规则必须记住:集合类结构(Hash、Set、ZSet)在元素少且值短时会使用 listpack 或 intset 这类紧凑编码,超过阈值后自动转成哈希表或跳表。以 Hash 为例,默认 hash-max-listpack-entries 128、hash-max-listpack-value 64,一旦超过,内存占用会出现一次明显的台阶式跳升。选型时预估的元素数量级,直接决定了你实际拿到的内存账单。

场景映射决策表:业务问题直接对号入座 ​

下面这张表是本文的核心,按「业务问题 → 推荐结构 → 关键命令 → 选型理由与注意点」组织,评审或写方案时可以直接引用:

业务场景推荐结构关键命令选型理由与注意点
对象缓存、购物车HashHSET / HMGET / HINCRBY字段级更新省网络与序列化;大 Hash 谨慎 HGETALL
全局计数器、接口限流StringINCR / INCRBY + EXPIRE自增原子,配合过期做固定窗口限流
简单任务队列ListLPUSH / BRPOP阻塞弹出省轮询;但无确认机制,失败即丢
事件流、可靠队列StreamXADD / XREADGROUP / XACK消费者组 + Pending 重试;配 MAXLEN 控制内存
标签、共同关注SetSADD / SINTER / SDIFF服务端集合运算,避免拉回应用层
排行榜、延迟任务ZSetZADD / ZREVRANGE / ZRANGEBYSCOREscore 排序天然有序;注意大榜分页
签到、在线状态位BitmapSETBIT / BITCOUNT / BITOP按位存储极致省内存;offset 稀疏有坑
UV、去重估算HyperLogLogPFADD / PFCOUNT / PFMERGE固定约 12KB、误差 0.81%,不可做精确计数
附近的人、门店GEOGEOADD / GEOSEARCH底层 ZSet,geohash 编码进 score

用表的方法:先在第一列找到自己的业务问题,再看第三列的关键命令能不能覆盖读写模式。如果发现关键命令覆盖不了(比如需要「消费确认」和「失败重试」),说明该换行里的结构——这正是 List 队列升级 Stream 的典型信号。反过来,如果只是存个开关值,就不要上 Hash 甚至 Stream,结构越重维护成本越高。

实操对比:同一个用户资料,String 与 Hash 差在哪 ​

以「用户资料页要展示昵称、等级、积分,且积分高频变动」为例,对比两种写法。反模式是把整个对象序列化后全量覆盖:

bash
# 反模式:String 存 JSON,改昵称也要读-改-写全量
SET user:1001 "{\"nickname\":\"dev\",\"level\":6,\"points\":3200}"
GET user:1001

这样写的三个代价:改任何一个字段都要先 GET 再改再 SET 回去,并发下存在丢更新竞态;积分高频变动意味着每次都传整个对象,带宽和序列化 CPU 都是浪费;应用层还要维护「读到一半被别人改了」的加锁逻辑。换成 Hash 之后:

bash
# 推荐:Hash 字段级读写,HINCRBY 原子自增积分
HSET user:1001 nickname dev level 6
HINCRBY user:1001 points 50
HMGET user:1001 nickname level

HINCRBY 一条命令完成「读出积分、加 50、写回」三步,且是原子的,竞态问题直接消失。字段级更新意味着改昵称只碰昵称那一个字段,与对象大小无关。这里有个工程细节:如果对象字段特别多且单个值很长(比如用户简介几千字),Hash 转成哈希表编码后内存可能反而更高,此时要么把大字段拆出独立 key,要么调整 hash-max-listpack-value 阈值后压测验证——选型不是教条,最终以自己数据形态的实测为准。

Stream 最小可用实现:把队列做对 ​

List 当队列最大的问题是「取出来就没了」,消费者处理失败消息直接丢失,也无法知道哪些消息没处理完。Stream 的消费者组机制补上了这两块。最小可用的命令序列如下:

bash
# 1. 创建消费者组,$ 表示从最新消息开始消费,MKSTREAM 自动建流
XGROUP CREATE order:events order-worker $ MKSTREAM

# 2. 生产侧:投递事件,MAXLEN ~ 1000000 近似修剪防止内存无限增长
XADD order:events MAXLEN ~ 1000000 * type "PAID" orderId "SO202610120001"

# 3. 消费侧:阻塞读 5 秒,> 表示只取从未投递过的新消息
XREADGROUP GROUP order-worker consumer-1 COUNT 10 BLOCK 5000 STREAMS order:events >

# 4. 处理成功后显式确认,消息从 Pending 列表移除
XACK order:events order-worker 1697080000000-0

# 5. 兜底:把空闲超过 60 秒仍未 ACK 的消息转移给当前消费者重试
XAUTOCLAIM order:events order-worker consumer-1 60000 0

五个步骤对应可靠队列的完整闭环:投递有 ID、消费有记录(Pending 列表)、成功有确认(XACK)、宕机有交接(XAUTOCLAIM 把超时未确认的消息重新分配)。XAUTOCLAIM 是 6.2 引入的命令,替代了早期「XPENDING 手动查询 + XCLAIM 手动转移」的组合,兜底逻辑一个命令就能写完。实践中建议在应用里起一个定时任务周期性执行第 5 步,重试次数超限的消息转投死信流人工处理,避免毒消息反复重试。

避坑总结 ​

  1. HGETALL 与 KEYS 不能当例行操作。大 Hash 的 HGETALL 一次拉全量字段,大 keyspace 的 KEYS 会阻塞主线程,线上一律用 HMGET 按需取、SCAN 代替 KEYS。
  2. 小心紧凑编码的台阶跳变。Hash、Set、ZSet 超过 listpack/intset 阈值后内存跳升明显,预估好元素量级,必要时用 MEMORY USAGE 对比两种形态的真实占用。
  3. List 队列在要求不丢消息的场景直接不合格。没有 ACK 和 Pending 机制,消费者崩溃即丢消息;只要业务提了「可靠」二字,就用 Stream。
  4. HyperLogLog 只能估算。0.81% 的标准误差对 UV 无感,但对「精确去重计数」是致命的,这类需求用 Set 或 Bitmap。
  5. Stream 必须配 MAXLEN。消息流不会自动收缩,不设修剪策略的 Stream 最终会把内存吃满;按条数或按时长修剪,写进团队规范。

写在最后 ​

数据结构选型的本质,是把业务读写模式翻译成 Redis 已经优化好的原语:字段级更新找 Hash,排序找 ZSet,位运算找 Bitmap,可靠队列找 Stream。九大结构覆盖了缓存场景九成以上的需求,真正需要的自定义逻辑比想象中少。下次评审方案时,先问一句「这个需求在决策表的第几行」,很多后来的性能问题在选型那一刻就已经埋下了。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。