Hash / List / Set / ZSet
四种结构场景选型。
五种数据结构:选对结构,代码少一半
这一课你将能做到
- 用 Hash、List、Set、ZSet 各完成一组真实操作(对象字段读写、队列、收藏去重、排行榜),并说出每种结构「像什么」
- 用
TYPE 判断 key 类型,解释 WRONGTYPE 错误的成因和处理方式
- 论证本站 tracks 列表为什么用「String 存 JSON」而不是 Hash/List,建立「结构选型跟着读写模式走」的判断力
为什么要学
只会 String 的开发者,遇到业务会硬造:
- 要「用户收藏的课程」→ 用 String 存逗号分隔的 id?判断收藏过没有还得取出来 split——Set 的
SISMEMBER 一条命令 O(1) 搞定
- 要「课程学习人数排行」→ 自己查全表排序?ZSet 的
ZINCRBY + ZREVRANGE 两条命令搞定,天然实时有序
- 要「异步任务队列」→ 数据库轮询?List 的
LPUSH/RPOP 就是现成队列
Redis 官方给自己的定位是数据结构服务器,每种结构都对应一类高频业务场景。选对结构,你的 Java 代码能少写一半,性能还好几个量级;选错了不是「跑不起来」,而是改一个字段要重写整包、去重靠应用层循环、排行每次全量排序——又慢又脆。
先建立图像
String → 一张便签(整块内容) coderoadmap:tracks:list
Hash → 一张名片(多个字段) user:1001:profile {name:"lin", level:3}
List → 一根吸管(两端进出) queue:notify [task3,task2,task1]
Set → 一袋无序不重的球 user:1001:bookmarks {lesson:5, lesson:12}
ZSet → 一张自动排序的成绩单 rank:lessons {lesson-7 → 98 分}
| 结构 | 人话比喻 | 典型场景 |
|---|
| String | 整封信 | 缓存 JSON、计数器、锁 |
| Hash | 档案袋里多张卡片 | 对象的字段级读写 |
| List | 排队叫号条 | 队列、最新 N 条 |
| Set | 去重抽签箱 | 收藏、标签、共同关注 |
| ZSet | 带分数的成绩榜 | 排行榜、按分数/时间取范围 |
跟着做
步骤 1:Hash——对象的名片
HSET user:1001 name "xiaolin" level 3
HGET user:1001 name # "xiaolin"
HGETALL user:1001 # 全部字段和值
HINCRBY user:1001 level 1 # level 变 4 —— 字段级原子自增
HDEL user:1001 level
适合「一个实体的多个字段、字段需要单独读写」的场景。
步骤 2:List——队列与栈
LPUSH queue:notify "task-1" "task-2"
RPUSH queue:notify "task-3"
LRANGE queue:notify 0 -1 # 全量查看(0 -1 = 从头到尾)
LLEN queue:notify
RPOP queue:notify # 取走队尾一个(消费)
概念了解 BRPOP:阻塞版 RPOP,队列没数据时等待而不是空轮询,是任务队列的标配。本站目前没有用到它,看懂即可——要可靠投递、重试、死信,请用专业 MQ,不要用 List 硬撑。
步骤 3:Set——收藏与去重
SADD user:1001:bookmarks lesson:5 lesson:12
SADD user:1001:bookmarks lesson:5 # (integer) 0 —— 重复元素,集合不变
SISMEMBER user:1001:bookmarks lesson:5 # (integer) 1 —— 已收藏
SCARD user:1001:bookmarks # (integer) 2 —— 数量
SADD user:1002:bookmarks lesson:12 lesson:9
SINTER user:1001:bookmarks user:1002:bookmarks # 共同收藏 → lesson:12
步骤 4:ZSet——排行榜
ZADD rank:lessons 98 "lesson-7" 88 "lesson-3"
ZINCRBY rank:lessons 1 "lesson-3" # 分数 +1,排名自动更新
ZREVRANGE rank:lessons 0 -1 WITHSCORES # 按分数从高到低
ZRANGEBYSCORE rank:lessons 90 +inf # 90 分以上
边界提醒:课程在站内导航的排序权威在 MySQL 的 sort_order 字段;ZSet 适合「实时积分榜」这类补充场景,不是取代后台排序字段。
步骤 5:TYPE 与 WRONGTYPE
TYPE user:1001 # hash
TYPE rank:lessons # zset
LPUSH user:1001 "x" # (error) WRONGTYPE
每个 key 在第一次写入时确定类型,之后只能执行该类型家族的命令。WRONGTYPE 是类型系统的保护,不是 bug。
步骤 6:论证本站 tracks 列表为什么用 String JSON
看 TrackService(backend/src/main/java/com/codeforge/service/)的 listAll():它把整份 track 列表返回给前端渲染,访问特征是「整体读、整体换、无需按字段改」。
- 整体读 → 一次
GET 全拿到,不需要 Hash 按字段读
- 没有按字段改的需求 → Hash 的字段级操作用不上
- 反序列化一次得到 DTO 列表,和 Java 对象天然对齐
结论写进笔记:**结构选型跟着读写模式走,不是越高级越好。**需要「单条更新 + 单条读」才考虑 Hash,需要服务端排序才上 ZSet;整包只读缓存,String JSON 就是最优解。
讲清楚要点
- Hash 字段少时底层是 listpack(紧凑列表),省内存;字段多了自动转哈希表。你不用管转换,但要知道
HGETALL 在大对象上依然危险——单命令返回全部字段,既阻塞又大响应,大 Hash 用 HSCAN。
- List 是双端结构(quicklist):头尾操作 O(1),中间
LRANGE 是 O(N)——大列表别全量取。
- Set 无序去重,交集并集是杀手锏(共同关注、标签筛选);
SISMEMBER O(1)。
- ZSet 每个成员带 score,服务端按 score 排序:底层跳表 + 哈希双结构,范围查询和排名都是 O(log N),
ZINCRBY 原子加分、排名实时更新。
- WRONGTYPE 是保护不是 bug:类型固定才能让底层结构稳定优化。换类型前先
DEL 重建。
- 本站缓存里 JSON String 是默认选择;其他结构用于「需要 Redis 服务端能力」的场景——计数、排序、去重、阻塞队列。
你可能会懵的地方
-
什么时候用 Hash,什么时候用 String JSON?
需要按字段读/改(HGET/HINCRBY)或局部更新时用 Hash;对象整进整出、有嵌套结构时用 String JSON。本站公开 API 缓存全是后者。
-
ZSet 的 score 相同怎么排序?
相同 score 按成员的字典序排。需要严格时间序就把 score 存毫秒时间戳,保证唯一递增。
-
List 做消息队列靠谱吗?
轻量场景(BRPOP + 消费失败 LPUSH 回去)够用;要 ACK、重试、广播就上 Redis Stream 或专业 MQ。第 10 课讲 Pub/Sub 时会系统对比三者。
-
SINTER 性能怎样?
最坏 O(N*M)。小集合(几十几百个成员)没问题,百万级成员慎用,可以改在客户端归并。
-
ZREVRANGE 0 -1 会把排行榜全吐出来吗?
会。线上要限长:ZREVRANGE key 0 9 只取前十名,避免全量响应。
错法对照
| 错法 | 问题 | 正解 |
|---|
| String 存 "1,5,12" 当收藏列表 | 判断收藏要全量解析,去重靠应用层 | Set + SISMEMBER |
| 用 Hash 存排行榜再客户端排序 | 排序在应用层做,并发更新丢分 | ZSet + ZINCRBY |
| 对超大 Hash 执行 HGETALL | 单命令阻塞 + 大响应 | HSCAN 游标遍历 |
| 对 zset key 执行 LPUSH | WRONGTYPE | 先 TYPE 再选命令,换类型先 DEL |
| List 队列用 RPOP 空轮询 | 空转打爆 Redis | BRPOP key 5 阻塞等待 |
卡点急救
- WRONGTYPE:
TYPE key 看类型;确认要用其他类型先 DEL(缓存场景重建成本低,注意先看第 6 课再动手删生产 key)。
- HGETALL 卡顿:Hash 太大,换
HSCAN key 0 MATCH * COUNT 100。
- ZINCRBY 后分数显示成浮点:ZSet score 是双精度浮点,读出来做数值比较,别当字符串判等。
- BRPOP 把终端阻塞住了:设超时
BRPOP key 5——5 秒没数据返回 (nil)。
- SADD 了重复元素却没报错:Set 天然去重,返回值是「实际新增个数」,返回 0 表示元素已存在。
面试常问
-
Redis 五种基本结构及各自场景?
String(缓存 JSON、计数器、锁)、Hash(对象字段级读写)、List(队列、最新列表)、Set(去重、收藏、共同关注)、ZSet(排行榜、延迟任务)。要能结合自己项目各举一例。
-
ZSet 底层怎么实现的?
跳表 + 哈希表双结构:跳表支持 O(log N) 的范围查询和排序(ZREVRANGE/ZRANGEBYSCORE),哈希表支持 O(1) 查成员分数(ZSCORE)。
-
排行榜怎么设计?
ZADD/ZINCRBY 写分数,ZREVRANGE 取 TopN(记得限长),ZRANK 查个人排名。多维度排行(日榜/周榜/总榜)多建几个 ZSet key,各自 TTL 或定期清理。
自测三问
- 「判断用户是否收藏过某门课」用什么结构和命令?为什么不用 List 存收藏?
- 为什么本站 tracks 列表用 String JSON 而不是 Hash?说出三条理由。
- ZSet 和 Set 的核心区别是什么?ZSet 底层靠什么数据结构做到 O(log N)?
小结一句话
五种结构各管一类场景:String 缓存计数、Hash 对象字段、List 队列、Set 去重关系、ZSet 排行——选型跟着读写模式走,本站整包只读缓存坚持 String JSON。
延伸