Skip to content

Redis 数据结构与常用命令 ​

S4-007必学进阶约 90 分钟JDK 21企业开发进阶 · Redis

1. 本节定位 ​

从本节起进入缓存与中间件。第一个问题是:为什么不用 HashMap 缓存,而要用 Redis?

text
HashMap:单机、进程内、重启就没了、多实例不共享   ✗
Redis:  独立进程、多实例共享、可持久化、有数据结构 ✓✓

★ 本节先讲"Redis 能装什么",后面四节讲"怎么用它解决真问题" ✓

2. 学习目标 ​

  • 能说出五种基本数据结构的用途与选型口诀 ★★★;
  • 知道 Redis 为什么快(内存 + 单线程 + IO 多路复用)★★★;
  • 知道两种过期删除策略,以及"设了 TTL 内存也不会立刻释放" ★★★;
  • 知道 HyperLogLog / Bitmap / Stream 三个"特殊类型"的用途 ★★;
  • 知道减少 RTT 比减少命令数更重要(pipeline / Lua)★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个"用错数据结构"的例子 ✗:

text
需求:实时排行榜(按销量取前 10)
✗ 用 String 存 JSON 数组 → 每次都要全量读出、自己排序 → O(n log n) ✗
✓ 用 ZSet → ZREVRANGE rank 0 9 → O(log n + 10) ✓✓

★ 本节的核心是"按需求选结构" ✓✓:

需求结构
缓存对象 / 计数String ✓
队列 / 最新 N 条List ✓
去重 / 共同好友Set ✓
排行榜 / 延时队列ZSet ✓✓

5. 核心概念 ​

5.1 五种基本数据结构 ★★★ ​

类型结构核心命令典型用途
String ★★★字符串/数字/二进制SET GET INCR SETEX缓存、计数器、分布式锁
Hash ★★★字段-值映射HSET HGET HGETALL对象缓存、购物车
List ★★双向链表LPUSH RPUSH LPOP LRANGE队列、栈、最新 N 条
Set ★★无序去重集合SADD SMEMBERS SINTER去重、共同好友、抽奖
ZSet ★★★带分数的有序集合ZADD ZRANGE ZRANK排行榜、延时队列、限流

★ 本节的实测(MiniRedisLesson 的输出,标注了对应的真实命令)✓:

text
  SET user:1:name alice  →  GET 返回 alice
  SET session:abc user-1 EX 60 →  TTL 剩余 60 秒

  HSET product:1 name 机械键盘 / price 399 / stock 50
  HGETALL product:1     → {name=机械键盘, price=399, stock=50}

  RPUSH queue:tasks 任务1 任务2 任务3 → [任务1, 任务2, 任务3]
  LPOP queue:tasks → 取出 任务1(先进先出 = 队列)✓

  SADD user:1:tags 技术 读书 技术 → [技术, 读书](重复的只存一份 ✓)

  ZADD rank:sales 9800 商品A / 15600 商品B / 7300 商品C / 21000 商品D
  ZRANGE rank:sales 0 -1 WITHSCORES → 按分数升序输出 ✓

5.2 Hash vs String(JSON):怎么选 ★★★ ​

这是缓存对象时最常见的抉择 ✓:

维度HashString(存 JSON)
改单个字段 ★★★只更新那个字段 ✓✓要读出整个 JSON、改完写回 ✗
更新粒度字段级 ✓整体 ✗
序列化开销无 ✓每次都要序列化/反序列化 ✗
内存效率小 Hash 有压缩(ziplist)✓整体存,可能更省 ✓
适合只改某几个字段的对象 ✓✓整体读写、不关心中间状态 ✓

★ 判断口诀 ✓✓:

"会不会只更新其中一部分字段?" ——会 → Hash;不会 → String ✓

5.3 为什么 Redis 快 ★★★ ​

三个原因 ✓:

#原因说明
① 纯内存操作 ★★★没有磁盘 IO ✓(持久化是异步的)
② 单线程模型 ★★★没有锁竞争、没有上下文切换 ✓✓
③ IO 多路复用 ★★一个线程管理大量连接(epoll)✓

★★ "单线程"常被误解 ✓✓:

它指的是"命令执行是单线程串行" ✓

不是"Redis 只有一个线程" ——持久化、集群同步、异步删除都有独立线程 ✓

而命令串行带来的最大好处是:所有命令天然是"原子"的 ✓✓ 这就是 INCR、SETNX 能直接用来做计数器和锁的原因 ✓✓

★ 本节的性能实测(节选,10 万次写入) ✓:

text
  String(SET):  38 ms
  ZSet(ZADD):    19 ms  ← ZADD 本身是哈希表写入,排序成本在 ZRANGE 时才产生(本实现惰性排序)

★★ 但这不是Redis 的真实性能 ✗ ——必须说清三点 ✓:

差异说明
没有网络往返 ✗真实 Redis 一次命令要 1 次 RTT(本机约 0.1ms,跨机房约 1ms)
不是单线程 ✗这里用并发容器,真实 Redis 串行执行
没有协议解析 ✗RESP 的编解码本身也有成本

★ 所以真实项目里的优化重点是"减少 RTT" ✓✓:

手段效果
pipeline(批量发送) ★★★N 次命令只花 1 次 RTT ✓✓
Lua 脚本 ★★多条命令在服务端一次执行 ✓
批量命令(MGET / MSET)★一次取多个 key ✓

★★ 一条算账 ✓✓:10 万次单条命令 × 1ms RTT = 100 秒 ✗ 而用 pipeline 分 100 批:100 × 1ms + 服务端执行时间 ≈ 几秒 ✓✓

5.4 过期策略:设了 TTL 内存也不会立刻释放 ★★★ ​

Redis 用两种策略配合 ✓:

策略做法优点缺点
惰性删除 ★★访问时才发现过期,顺手删掉省 CPU ✓不被访问的 key 会一直占内存 ✗
定期删除 ★★★每秒 10 次,每次随机抽查 20 个 key主动清理 ✓不能扫全量(会阻塞)→ 抽样 ✗

★ 本节的实测 ✓:

text
  SETEX code:... 123456 1  → 立即 GET:123456
  等 1.1 秒后 GET:null(返回 null)✓        ← 惰性删除生效

  另外 1000 个 key 也过期了,但没人访问它们
  执行定期删除(抽样扫描):清理了 1000 个 ✓  ← 定期删除生效

★★ 由此推出一条重要结论 ✓✓:

即使设了 TTL,内存也不会立刻释放 ✗

所以必须配置"内存淘汰策略"(maxmemory-policy) ✓✓

★ 八种淘汰策略里,最常用的三种 ✓:

策略淘汰谁
allkeys-lru ★★所有 key 里最久没用的(纯缓存场景推荐)✓
volatile-lru ★只从"设了过期时间的 key"里淘汰
noeviction ⚠️不淘汰,写入直接报错(当数据库用时选它)✓

5.5 三个"特殊类型" ★★ ​

类型一句话量级感受
HyperLogLog ★★估算基数(UV),误差 0.81%12 KB 估算上亿 UV ✓✓
Bitmap ★★位图,一 bit 一个状态1 亿用户签到一年 = 4.5 GB ✓
Stream ★带消费组的消息队列可替代轻量 MQ ✓

★ HyperLogLog 的取舍 ✓:

它不存原始数据,只存"估算结构" ✓

所以不能"列出具体有哪些用户" ✗ ——只能回答"大概有多少个" ✓✓

用于 UV 统计这种"只想看数字"的场景 ✓

★ Bitmap 的典型用法 ✓✓:

text
签到:SETBIT sign:20260922 <userId> 1        ← 一个用户一天只占 1 bit
统计7天连续签到:对 7 个 bitmap 做 AND        ← 一次位运算搞定 ✓✓

5.6 命令速查(按用途)★★ ​

想做什么命令
缓存对象SET / GET(JSON)或 HSET / HGETALL
计数INCR / DECR / INCRBY
带过期的缓存SET key value EX 60
只有不存在才设置(锁)SET key value NX EX 30(《Redis 分布式锁》)
批量取MGET k1 k2 k3
排行榜ZADD / ZREVRANGE / ZRANK
队列LPUSH + BRPOP(阻塞取)
延时任务ZSet(score 存执行时间戳) ✓✓
限流INCR + EXPIRE 或 ZSet 滑动窗口 ✓
模糊查 keySCAN(不是 KEYS)✗✗

★★ 最后一条是生产禁忌 ✓✓:

KEYS * 会遍历所有 key 并阻塞整个 Redis ✗✗

生产环境一律用 SCAN(游标分批) ✓✓

FLUSHALL / FLUSHDB 同理 —— 要么禁用,要么改名 ⚠️

6. 递进示例 ​

6.1 最小示例:缓存一个对象 ​

text
# 方式一:Hash(可以只改某个字段)
HSET product:1 name 机械键盘 price 399
HINCRBY product:1 stock -1          ← 只改库存 ✓

# 方式二:String + JSON(整体读写)
SET product:1 '{"name":"机械键盘","price":399}'
GET product:1

6.2 业务示例:延时任务用 ZSet ​

text
# 下单 30 分钟后自动取消
ZADD delay:cancel <当前时间戳+1800> ORD-123       ← score 是"该执行的时间"

# 后台轮询:取出所有到点的
ZRANGEBYSCORE delay:cancel 0 <当前时间戳> LIMIT 0 100

★ 这就是"延时队列"的最小实现 ✓✓

6.3 反例:用 List 当正经消息队列 ​

text
LPUSH + BRPOP 看起来很香,但它:
  ✗ 没有消费确认(取出来就没了,处理失败就丢了)
  ✗ 没有重试
  ✗ 没有多消费组
→ 要这些能力就上 RabbitMQ / Kafka(S4-012)✓

7. 完整可运行代码与实测 ​

代码:labs/redis-lab/src/main/java/cn/kb/learn/redis/ —— MiniRedis(最小实现)+ MiniRedisLesson(演示与实测)

运行:

powershell
cd labs
.\mvnw.cmd -pl redis-lab compile
java "-Dstdout.encoding=UTF-8" -cp redis-lab/target/classes cn.kb.learn.redis.MiniRedisLesson

★ 为什么要用"自己实现的 MiniRedis" ✓✓:

本机没有安装 Redis ——与其伪造数据,不如实现一个"行为一致的模型" ✓

每条演示都标注了对应的真实 Redis 命令,语义一致,可直接迁移 ✓✓

它的局限也在代码注释里写明了(无网络协议、无内存淘汰、无持久化)✗

实测结论 ✓:

#结论
①五种结构各司其职:缓存用 String/Hash、排队用 List、去重用 Set、排序用 ZSet ✓
②过期是"惰性 + 定期"双策略,所以设了 TTL 内存也不会立刻释放 ✓✓
③真实场景的瓶颈在 RTT,不在命令本身 → 用 pipeline / Lua ✓✓

8. Debug 观察点 ​

  1. 对同一个 key 反复 SETEX 并观察 TTL:理解"过期时间会被重置" ✓;
  2. 用 KEYS * 和 SCAN 对比(在有真实 Redis 的环境):体会阻塞风险 ⚠️;
  3. 对一个大 Hash 做 HGETALL:观察网络传输量(改 HMGET 只取需要的字段)★;
  4. 对 List 用 LRANGE key 0 -1:观察大 List 的返回量(应该用 LRANGE key 0 9)★。

9. 常见错误 ​

#错误说明
1生产用 KEYS * ✗✗阻塞整个 Redis ✓
2以为"设了 TTL 内存就立刻释放" ✗✗惰性 + 定期(5.4)✓
3不配淘汰策略 ✗✗内存满了写入直接失败 ✓
4把大对象(整张表)塞进一个 key ✗✗大 key 会阻塞、难删除 ✓
5把 List 当正经 MQ ✗没有确认/重试/消费组 ✓
6一条条发命令,不做 pipeline ✗RTT 成为瓶颈 ✓
7用 HyperLogLog 后想"列出具体用户" ✗它只存估算结构 ✓

10. 动手实验 ​

目标:把"按需求选结构"变成本能。

步骤:

  1. 跑 MiniRedisLesson,确认五种结构的输出与真实命令的对应 ✓;
  2. 给"用户购物车"选结构:说明为什么用 Hash 而不是 JSON ✓;
  3. 给"最近 100 条登录记录"选结构:写出命令序列(提示:LPUSH + LTRIM)★★;
  4. 用 ZSet 实现一个"延时 10 秒执行"的任务,并写轮询逻辑 ★★;
  5. 算一笔账:10 万次单条命令 vs 100 次 pipeline,差多少 RTT ✓。

验收方式:能对任意一个需求说出"该用哪种结构、为什么"。

扩展任务:设计一个"附近的人"的最小方案:

  • 用 GEO 系列命令(底层也是 ZSet)✓;
  • GEOADD 写入位置、GEOSEARCH 查附近 ✓;
  • 说明:为什么"按经纬度分桶"不如用现成的 GEO 命令 ★

11. 练习题 ​

  1. 判断:Redis 是单线程的,所以它很慢。(对 / 错)
  2. 填空:排行榜用 ______;队列用 ______;去重用 ______;计数器用 ______。
  3. 简答:为什么"设了 TTL 的 key"不会立刻释放内存?
  4. (设计)
text
需求:商品详情页缓存
  - 商品有 20 个字段,其中"库存"会被高频更新
  - 其他字段几乎不变

用 String(JSON) 还是 Hash?为什么? 5. (编程)用 ZSet 设计一个"滑动窗口限流":限制每用户每分钟最多 100 次请求(提示:score 存时间戳,ZREMRANGEBYSCORE 清窗口外的)。

参考答案(要点)
  1. 错。单线程指"命令串行执行",反而避免了锁竞争与上下文切换 ✓ —— 而它是内存操作 + IO 多路复用,所以极快 ✓★
  2. ZSet;List;Set;String(INCR) ✓
  3. 因为它靠"惰性删除 + 定期抽样删除" ✗ ——没被访问到的过期 key 仍然占内存 ✓ ——所以必须配淘汰策略 ✓★
  4. Hash ✓ ——因为"库存"是高频更新的单个字段,Hash 可以 HINCRBY 只改它, 而 String 要读出整个 JSON、改完写回(还会增加并发覆盖的风险)✓✓
  5. 要点:
    ZREMRANGEBYSCORE limit:<user> 0 <now-60000>      ← 清掉 1 分钟前的记录
    ZCARD limit:<user>                                ← 看还剩多少
    ZADD limit:<user> <now> <请求ID>                  ← 记录本次
    三步要么用 Lua 保证原子,要么接受微小误差 ✓

12. 掌握标准 ​

  • [ ] 能说出五种结构的用途与选型口诀 ★★★;
  • [ ] 知道 Hash 与 String(JSON) 的取舍判断 ★★★;
  • [ ] 知道 Redis 快的三个原因,以及"单线程"的确切含义 ★★★;
  • [ ] 知道两种过期策略,以及为什么会配淘汰策略 ★★★;
  • [ ] 知道 HyperLogLog / Bitmap / Stream 的用途与边界 ★★;
  • [ ] 知道减少 RTT 比减少命令数更重要(pipeline / Lua)★★★;
  • [ ] 知道生产禁用 KEYS / FLUSHALL ★★。

13. 扩展资源 ​

14. 下一步学习 ​

《缓存穿透、击穿与雪崩》:缓存失效时会发生什么,以及怎么接住。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引