Redis 五大数据结构:什么场景选哪个,一张表记牢

0 阅读5分钟

Redis 五大数据结构:什么场景选哪个,一张表记牢

作者:鱼宵 | 实战驱动系列 · 第 8 篇

完整课程与可运行源码已开源在 Gitee:gitee.com/j67mk2/redi… (本文对应 lesson-02/)


一、面试题预告:这五个问题几乎必考

  • String / Hash / List / Set / ZSet 底层分别是什么?(一句话版)
  • 为什么 INCR 是原子的?"原子"到底啥意思?
  • 用 List 当消息队列有什么坑?
  • ZSet 为什么用跳表?跳表是什么?
  • 给你一个业务场景,怎么挑数据类型?

上一篇我们"混了个脸熟",这篇一个个拆开,每种类型:底层一句话 + 典型场景 + 高频命令 + 实测。


二、String:贴标签的盒子

  • 底层一句话:SDS(简单动态字符串),就是"会自动扩容的字符串数组",不用记源码。
  • 典型场景:缓存接口结果;点赞数/访问量/库存(INCR/DECR);多台机器一起 INCR 同一个键拿全局唯一递增 ID。

"原子"是啥意思? 人话:INCR 在 Redis 里是一口气做完、中间不会被别的命令插队。两个请求同时给点赞数 +1,如果不是原子,可能都读到 100、都写成 101,少加一次;原子操作保证最终一定是 102。

实测(计数器):

SET lesson:str:counter 0   -> OK
INCR lesson:str:counter    -> 1
INCR lesson:str:counter    -> 2
INCRBY lesson:str:counter 5 -> 7

三、Hash:一格多抽屉

  • 底层一句话:一张哈希表(key → 多个 field-value)。
  • 典型场景:存一个经常只改几个字段的对象(用户信息)。相比把整个对象序列化成 String,Hash 可以只改 age 一个字段,不用重写整条。

实测:

HSET user:1 name "王小明" age 25 city "郑州" -> 3
HGET user:1 name          -> 王小明
HGETALL user:1
    name / 王小明 / age / 25 / city / 郑州

思考题:为什么存"经常只改单个字段"的对象用 Hash 更合适?答:序列化成 String 的话,改一个 age 也得把整个 JSON 重写一遍,Hash 是点改。


四、List:有顺序的队伍

  • 底层一句话:双向链表(新版 Redis 叫 quicklist,"一串链表串起来")。
  • 典型场景:轻量消息队列(生产者 LPUSH 塞左边,消费者 BRPOP 右边取);朋友圈最新 10 条。

List 做队列的坑(面试爱问):

  1. BRPOP 是阻塞读:队列空了它会一直等,连接被占住(实测空队列阻塞了约 2017ms 才返回 null);
  2. 没有消息确认机制:消息一被弹出就没了,消费者拿到后崩了,这条消息就丢了;
  3. 所以只能当"轻量、可容忍丢失"的队列,正经消息队列用 RabbitMQ/Kafka。

实测:

LPUSH list:q "订单1" "订单2" -> 2
LRANGE list:q 0 -1
    订单2
    订单1
BRPOP list:q 2
    list:q
    订单1

注意顺序:LPUSH a b 后从队头看是 b a(最后塞的最靠前),想"先进先出"要配 BRPOP 从队尾取。


五、Set:不会重复的篮子

  • 底层一句话:哈希表(元素唯一);全是小整数时会用更省内存的 intset。
  • 典型场景:去重(点赞用户、签到);交并集——SINTER 求"共同好友"、SUNION 求合集。

实测(共同好友):

SADD set:zhangsan "王五" "赵六" "钱七" -> 3
SADD set:lisi "王五" "孙八" "周九"    -> 3
SINTER set:zhangsan set:lisi
    王五      # 两人的共同好友

六、ZSet:带分数的排行榜

  • 底层一句话:跳表(skiplist)+ 哈希表。
  • 跳表类比 = 多层电梯:普通链表只有一层,找一个人得从一楼挨个走;跳表盖了"快速楼层",先坐高层电梯跳到大概位置,再走楼梯细找,所以按分数排序、按范围查都快。
  • 典型场景:游戏积分榜、热搜榜。
  • 关键命令:ZADD 加成员分数、ZINCRBY 加分、ZRANGE 升序、ZREVRANGE 降序(榜一榜二)。

实测:

ZADD zset:rank 95 "语文" 88 "数学" 100 "英语" -> 3
ZINCRBY zset:rank 5 "数学"   -> 93   (数学从 88 涨到 93)
ZREVRANGE zset:rank 0 2 WITHSCORES
    英语  -> 100
    语文  -> 95
    数学  -> 93

思考题:为什么用跳表不用平衡树?面试答:实现简单、范围查询天然友好、单节点增删只影响局部,Redis 作者选它就是看中"够用且好写"。


七、选型速查表(面试直接背这个)

你想干嘛选哪个例子
缓存文本/数字、做计数器String缓存页面、点赞数
存对象、经常改其中几个字段Hash用户资料
简单排队、可容忍丢消息List站内消息队列
去重、求交集并集Set共同好友、抽奖去重
带分数排序、排行榜ZSet积分榜、热搜

八、挑战题

  1. ⭐ 用 String + INCR 做"网站今日访问量"计数器,连点 3 次看结果。
  2. ⭐⭐ 用 Set 实现"两个用户的共同关注",自己造两组关注列表,SINTER 求交集。
  3. ⭐⭐⭐ 用 List 实现"不能丢消息"的队列,怎么补救?(提示:消费者处理完再删,而不是弹出来就删——分布式锁那篇会再碰到类似问题)

九、面试回答模板

面试官:ZSet 底层是什么?

跳表 + 哈希表。跳表像多层电梯,按分数排序和范围查询都是 O(logN)。

追问:List 做消息队列有什么坑?

① BRPOP 阻塞占用连接;② 没有消息确认,弹出即丢,消费者崩了就丢消息;③ 生产用 MQ。

追问:INCR 为什么安全?

原子操作,一步完成不被插队,多并发不会数错。

追问:场景选型?

缓存/计数 String,对象 Hash,轻队列 List,去重 Set,排行榜 ZSet。


关于这个系列

本文是「Java 后端实战精通营」系列第 8 篇,原则:实战驱动、由浅到深、面试向。

👉 Redis 实战精通营(10 课):gitee.com/j67mk2/redi…

  • 本文对应源码位置:lesson-02/(五种类型一组 Jedis demo 全跑一遍)

下一篇预告:《Redis 持久化:断电了数据怎么不丢?RDB 和 AOF 怎么选》——快照 vs 记账本,混合持久化是啥,亲手触发一次 BGSAVE 看到 dump.rdb 从无到有。

跑完有任何报错,把终端输出发评论区,一起排查。