Redis 数据结构总览:String、Hash、List、Set、ZSet、Stream 怎么选(三)
系列导航:
- 上一篇:Redis 核心模型:键、内存、单线程与性能边界(二)
- 当前篇:Redis 数据结构总览:String、Hash、List、Set、ZSet、Stream 怎么选(三)(本文)
- 下一篇:Redis 缓存设计:Cache Aside、穿透、击穿与雪崩(四)
前两篇 overview 解决的是“为什么需要 Redis”以及“Redis 作为系统是什么”。到了第三篇,问题会落到真正影响建模的一步:同样是一个 Key,里面到底该放成什么结构。
很多 Redis 用法之所以越用越重,不是因为命令不会写,而是因为一开始结构就选错了。结构一旦选错,后面你看到的往往是这些现象:
- 本来只想存一个对象,后来更新越来越别扭
- 本来只是有序列表,后来又开始需要去重和范围读取
- 本来只是简单消息,后来又想确认、回溯和重试
- 本来只是成员集合,后来却要求按分数排序
这些都说明:问题不再是“能不能存进去”,而是“现在这份数据的语义,是否还和当前结构匹配”。
本文只做三件事:
- 给出一张 Redis 常见数据结构的认知地图。
- 说明每种结构最核心的判断维度。
- 说明什么时候应该从一种结构切换到另一种结构。
本文刻意不展开缓存模式、排行榜方案、消息系统设计等业务模式。这里只建立结构判断,不讨论完整业务落地。
先用四个问题判断结构
在选结构之前,可以先问四个最基础的问题:
- 这个值是一个整体,还是由多个字段组成?
- 我更关心顺序、唯一性,还是分数排序?
- 我需要按范围读取,还是按成员判断存在?
- 这份数据是静态值,还是会不断追加形成序列?
只要这四个问题答清楚,结构选择通常不会偏得太远。
一张够用的结构认知地图
先把最常用的几种结构放在一张表里:
| 结构 | 先看什么 | 更像什么 | 什么时候要警惕切换 |
|---|---|---|---|
| String | 值是否应整体读写 | 一个完整值 | 当你频繁只改局部字段 |
| Hash | 值是否由字段组成 | 一个字段对象 | 当你多数时候还是整对象全取 |
| List | 是否只关心顺序 | 一个线性序列 | 当你开始需要按成员检索或确认机制 |
| Set | 是否只关心唯一成员 | 一个无序集合 | 当你开始需要顺序或分数 |
| Sorted Set | 是否需要按 score 排序 | 一个可排序集合 | 当排序维度变复杂或成员无限增长 |
| Stream | 是否需要不断追加的消息序列 | 一条事件流 | 当你其实只需要简单列表或广播 |
这张表的重点不是背结论,而是理解:结构的本质差异,在于它优先表达什么行为。
String 和 Hash:先分清“整体值”还是“字段对象”
这通常是第一组最容易混淆的选择。
什么时候优先 String
如果一份值更像一个整体,读的时候通常整块取出,写的时候也通常整块覆盖,那么 String 往往最自然。
它适合表达的是:
- 一个完整文本
- 一个序列化后的对象
- 一个二进制值
- 一个整体替换成本低的结果
你可以把 String 理解成“我更关心这个值整体是什么”,而不是“我想在 Redis 内部频繁改它的某个字段”。
什么时候优先 Hash
如果一份值天然由多个字段组成,而且你经常只改其中一部分字段,那么 Hash 更贴近这种语义。
它更像:
- 一个由字段组成的对象
- 一个可以按字段单独读取或单独更新的容器
什么时候该从 String 切到 Hash
出现下面这些信号时,往往说明 String 已经开始不顺手:
- 你经常只想改一个字段,却总要整对象反序列化再写回
- 对象越来越大,但修改总集中在少数字段
- 业务经常按字段读取,而不是每次都拿整块值
什么时候别为了“更细粒度”盲目切到 Hash
如果你多数时候仍然是整对象读取,而且字段更新并不频繁,那么从 String 切到 Hash 不一定更好。结构切换不是为了显得更专业,而是为了让读写方式更匹配。
List、Set、Sorted Set:顺序、唯一性和排序不要混着看
这三种结构的区别,最适合从“你到底最在意什么关系”开始看。
List:只强调顺序
List 最核心的语义,是元素按先后进入一个线性序列。
如果你更关心的是:
- 谁在前,谁在后
- 从左边还是右边进出
- 取一段连续区间
那么它的心智模型通常最接近 List。
Set:只强调唯一成员
Set 不关心顺序,也不关心分数,它最在意的是“一个成员在不在集合里,而且不要重复”。
如果你的第一诉求是:
- 某个元素是否存在
- 两组成员有没有交集
- 成员要天然去重
那它更像 Set 问题。
Sorted Set:既要唯一成员,又要有序排名
Sorted Set 可以理解成“带分数的 Set”。成员依旧唯一,但每个成员同时带一个 score,于是结构可以按 score 排序。
当你的问题变成下面这样时,通常是在向 Sorted Set 靠近:
- 不只是要知道成员在不在,还要知道它排第几
- 不只是保留顺序,还要按可计算分值做范围读取
- 不只是线性先后,而是明确存在排序依据
List、Set、Sorted Set 之间什么时候切换
这一组切换最常见,因为很多需求会从“差不多能放”演变成“原结构已经不对味”。
从 List 切到 Set
当你原本只需要保留一串元素,后来开始更频繁地关心“某个成员是否已经存在”,并且不再真正依赖顺序时,说明问题已经更像集合而不是列表。
从 Set 切到 Sorted Set
当你一开始只要求唯一性,后来又开始要求:
- 按某个权重排序
- 取前 N 个成员
- 按 score 做范围扫描
这说明仅靠 Set 已经不够,问题开始需要 Sorted Set 的 score 维度。
从 List 直接切到 Sorted Set
如果你最初只想保留先后关系,但后来发现真正需要的其实不是“插入顺序”,而是“按某个数值或时间戳排序”,那么更适合直接切到 Sorted Set。
一个简单判断是:
- 顺序来自进入先后,用
List - 顺序来自外部数值,用
Sorted Set
Stream 为什么不是“高级版 List”
很多人第一次看到 Stream,会自然把它理解成“更复杂一点的 List”。这会带来很多判断错误。
Stream 和 List 的共同点,只是它们都能形成序列;真正的差异在于,Stream 表达的是不断追加、带消息身份、可被消费的记录流。
所以,Stream 不是为了替代所有顺序结构,而是为了表达这类问题:
- 数据会持续追加
- 每条记录有自己的 ID
- 读取不是单纯拿一段区间,而是围绕消费位置前进
- 序列本身更像一条持续增长的事件流
什么时候从 List 切到 Stream
出现下面这些信号时,通常说明问题已经不再是简单顺序容器:
- 你开始关心每条记录的独立身份
- 你希望记录不是取完就算,而是能按消费位置前进
- 你需要围绕“还没处理到哪里”来理解这份数据
什么时候别过早用 Stream
如果你只是需要一个简单的有序序列,或者只是在一端追加、另一端取出,List 往往更直接。Stream 的价值在于它表达的是流,而不是“更复杂所以更高级”。
Bitmap、HyperLogLog、Geo 和 JSON:什么时候再往外扩
当基础结构已经无法准确表达问题时,才值得考虑这些更专门的能力。判断方式仍然一样:先看语义是否匹配,而不是先看命令新不新。
Bitmap
当一批状态天然可以压成位,并且你更关心的是“某个位置是否为 1”,这是位图问题,而不是普通字符串问题。
HyperLogLog
当你需要的是近似去重计数,而不是精确成员集合,这已经不是 Set 问题,而是近似统计问题。
Geo
当排序或查询依赖的是地理位置,而不是普通数值分数,这就不再是普通 Sorted Set 心智模型能完整表达的范围。
JSON
当对象天然是嵌套文档,而且你真正需要的是路径级读写时,它才值得和 String、Hash 分开看。
这些结构的共同点是:只有当问题本身已经进入对应语义,切换才有意义。
结构切换时,先看“主操作”有没有变
很多人判断是否要换结构,容易盯着“数据长什么样”;但更实用的标准其实是:你最常执行的主操作有没有变。
可以按这个顺序判断:
- 最常做的是整体覆盖,还是局部字段更新。
- 最常关心的是顺序、成员唯一,还是分数排序。
- 最常读取的是一段值,还是按成员判断、按范围检索。
- 数据更像静态容器,还是不断追加的流。
只要主操作变了,结构往往也该跟着变。
结构切换,不等于频繁迁移
需要强调一点:理解切换判断,不是鼓励你在同一条链路里反复迁移结构。真正更重要的是在建模初期就把主要语义想清楚。
切换判断的意义在于:
- 帮你识别当前结构是不是已经开始别扭
- 帮你知道问题为什么变别扭
- 帮你在重构时有一条更清晰的方向
它不是让你每看到一个新命令就重写模型。
这一篇只留下一个结构判断框架
如果把今天的内容压缩成一个最小框架,可以记成:
- 整体值还是字段对象:
String或Hash - 顺序、唯一性还是分数排序:
List、Set或Sorted Set - 静态容器还是持续追加的流:普通结构或
Stream - 只有当问题进入更专门语义时,才考虑 Bitmap、HyperLogLog、Geo、JSON
理解 Redis 数据结构,关键不是记住每个命令,而是看见一份数据时,能先判断它究竟更像哪一种关系模型。
本篇解决了什么:
- 给出了 Redis 常见结构的一张认知地图,区分整体值、字段对象、顺序、唯一性、排序和流
- 说明了
String、Hash、List、Set、Sorted Set、Stream各自最核心的判断维度 - 总结了几类常见切换信号,帮助判断什么时候当前结构已经不再匹配
- 明确了本文只建立结构判断,不展开缓存、消息、排行榜等业务模式