Redis 经典用例全解:从数据结构到系统设计
Redis 不只是缓存。它是一把瑞士军刀——凭借五种基础数据结构和若干扩展模块,Redis 能解决从分布式锁到社交网络、从排行榜到消息队列的几乎所有高频系统设计问题。
本文的目标是:建立一套从业务问题到 Redis 数据结构的映射思维。对于每个用例,我们都会回答三个问题:
- 业务问题是什么? 需求的本质是什么操作?
- 为什么选 Redis? 相比 MySQL、MQ 等方案,Redis 的优势和代价是什么?
- 怎么设计? 用哪种数据结构,Key 怎么命名,核心命令是什么?
mindmap
root((Redis 用例全景))
基础存储
缓存策略
Cache-Aside
穿透/击穿/雪崩
分布式会话
Session 共享
验证码/短链接
分布式 ID
INCR 自增
日期分段
并发控制
分布式锁
SET NX
Redlock
看门狗续期
计数器与限流
固定窗口
滑动窗口
令牌桶
有序数据
排行榜
实时排名
多维度榜单
延时队列
Score=到期时间
Lua 原子消费
关系与集合
社交关系
关注/粉丝
共同好友
集合运算
评论系统
实体+索引分离
二级评论树
消息与事件
消息队列
List 简单队列
Pub/Sub 广播
Stream 可靠队列
地理与统计
地理位置
GEO 附近的人
GeoHash 编码
UV 统计
HyperLogLog
12KB 统计 2^64
布隆过滤器
穿透防护
概率换空间
二值状态
签到系统
Bitmap
BITCOUNT/BITOP
Redis 数据结构速查与设计思维框架
数据结构总览
在开始任何设计之前,先建立对 Redis 数据结构能力的直觉:
| 数据结构 | 本质 | 核心能力 | 典型映射场景 |
|---|---|---|---|
| String | 二进制安全的字节串 | GET/SET/INCR/DECR,原子操作,支持 TTL | 缓存、计数器、分布式锁、会话 |
| Hash | 字段-值映射表 | HSET/HGET/HGETALL,单个 Key 下的结构化存储 | 对象存储、用户 Profile、购物车 |
| List | 双向链表 | LPUSH/RPOP/BRPOP,阻塞弹出 | 消息队列、最新动态、操作日志 |
| Set | 无序唯一集合 | SADD/SISMEMBER/SINTER/SUNION/SDIFF | 标签、共同好友、去重、抽奖 |
| Sorted Set (ZSet) | 带分数的有序唯一集合 | ZADD/ZRANGE/ZRANK/ZRANGEBYSCORE | 排行榜、延时队列、时间线、滑动窗口 |
| HyperLogLog | 概率基数统计 | PFADD/PFCOUNT/PFMERGE,误差 ≤ 0.81% | UV 统计、独立访客计数 |
| Bitmap | 位数组 | SETBIT/GETBIT/BITCOUNT/BITOP | 签到、在线状态、布隆过滤器 |
| GEO | 地理坐标索引(底层是 ZSet) | GEOADD/GEODIST/GEORADIUS | 附近的人、门店搜索 |
| Stream | 持久化消息日志 | XADD/XREAD/XREADGROUP/XACK | 消息队列、事件溯源 |
设计思维框架:三步映射法
面对任何业务问题,可以按以下三步将其转化为 Redis 方案:
flowchart TD
A["业务问题"] --> B["Step 1: 抽象操作语义"]
B --> C{"需要什么操作?"}
C -->|"读写单值"| D["String"]
C -->|"结构化对象"| E["Hash"]
C -->|"有序队列/栈"| F["List"]
C -->|"集合运算"| G["Set"]
C -->|"排序 + 范围查询"| H["Sorted Set"]
C -->|"基数统计"| I["HyperLogLog"]
C -->|"位运算"| J["Bitmap"]
C -->|"地理距离"| K["GEO"]
C -->|"消息流"| L["Stream"]
D --> M["Step 2: 设计 Key Schema"]
E --> M
F --> M
G --> M
H --> M
I --> M
J --> M
K --> M
L --> M
M --> N["Step 3: 评估取舍"]
N --> O{"数据丢失可接受?"}
O -->|"是"| P["纯 Redis 方案"]
O -->|"否"| Q["Redis + 持久化存储"]
Step 1 — 抽象操作语义:把业务需求翻译成数据操作。"用户签到"本质是"标记某天某用户为已签到"→ 位操作 → Bitmap。"排行榜"本质是"按分数排序并查询排名"→ 排序 + 排名查询 → Sorted Set。
Step 2 — 设计 Key Schema:Key 的命名决定了数据的组织方式。通用模式是 {业务域}:{实体类型}:{实体ID}:{子资源},例如 post:123:comments、user:456:followers。
Step 3 — 评估取舍:Redis 是内存数据库,天然面临容量和持久性的约束。每个方案都需要回答:数据丢了怎么办?内存不够怎么办?需要事务一致性吗?
七大可迁移模式(先记住模式,再看用例)
在深入 14 个用例之前,先建立模式意识。下面这七个模式覆盖了 Redis 解决问题的所有核心套路。每个用例都是某个模式的具体实例——记住模式,就能自己推导出新场景的方案。
| # | 模式名称 | 一句话口诀 | 覆盖用例 |
|---|---|---|---|
| 1 | KV + TTL 万能临时状态 | 任何"有生命周期的状态"都能用 String + 过期时间表达 | 缓存、Session、验证码、短链接 |
| 2 | 原子占位 | SET NX = “谁先到谁占坑”,解决一切"只能有一个"的问题 |
分布式锁、幂等控制、缓存击穿互斥锁 |
| 3 | Score 即时间轴 | 把时间戳塞进 ZSet 的 Score,就得到了一条可查询的时间线 | 延时队列、排行榜、滑动窗口限流、时间线 Feed |
| 4 | 双向关系 + 集合运算 | 一个关系写两个 Set,交并差运算解决所有"共同/推荐/互相"问题 | 社交关系、标签系统、权限交集 |
| 5 | 实体 + 索引分离 | Hash 存内容,ZSet/Set 存索引——读写分离的 Redis 版 | 评论系统、商品列表、搜索结果缓存 |
| 6 | 概率换空间 | 用可控的误差换取数量级的内存节省 | HyperLogLog UV 统计、布隆过滤器 |
| 7 | Lua 原子胶水 | 多个命令需要"要么全做要么不做"时,用 Lua 脚本粘合 | 限流、延时队列消费、锁释放、令牌桶 |
flowchart LR
subgraph "记住这七个模式"
P1["① KV+TTL<br/>临时状态"]
P2["② 原子占位<br/>SET NX"]
P3["③ Score=时间戳<br/>时间轴"]
P4["④ 双Set<br/>集合运算"]
P5["⑤ Hash+ZSet<br/>实体+索引"]
P6["⑥ 概率结构<br/>换空间"]
P7["⑦ Lua 脚本<br/>原子胶水"]
end
P1 --> U1["缓存/Session/验证码"]
P2 --> U2["分布式锁/幂等/击穿"]
P3 --> U3["排行榜/延时队列/限流"]
P4 --> U4["社交关系/标签/权限"]
P5 --> U5["评论/商品列表"]
P6 --> U6["UV统计/穿透防护"]
P7 --> U7["贯穿所有需要原子性的场景"]
阅读建议:后文每个用例结束后,都会有一个 🔑 模式提炼 段落,把该用例抽象为可迁移的模式,并列出"换个参数就能解决"的同类场景。读完全文后,回到这张表,你会发现所有用例都是这七个模式的排列组合。
用例一:缓存策略
业务问题
数据库查询慢,热点数据被反复读取,如何降低数据库压力并提升响应速度?
为什么选 Redis
| 维度 | Redis | 本地缓存 (Guava/Caffeine) | CDN |
|---|---|---|---|
| 延迟 | 亚毫秒(网络开销) | 纳秒级 | 毫秒级 |
| 一致性 | 多实例共享,单点更新 | 每个实例独立,一致性差 | 适合静态资源 |
| 容量 | GB 级 | 受 JVM 堆限制 | 无限(但成本高) |
| 适用场景 | 动态热点数据 | 极热点、变化少的数据 | 静态资源 |
设计方案
Cache-Aside(旁路缓存)
这是最常用的缓存模式。应用层同时管理缓存和数据库。
sequenceDiagram
participant Client
participant App
participant Redis
participant DB
Client->>App: 查询数据
App->>Redis: GET cache:user:123
alt 缓存命中
Redis-->>App: 返回数据
App-->>Client: 返回数据
else 缓存未命中
Redis-->>App: nil
App->>DB: SELECT * FROM users WHERE id=123
DB-->>App: 用户数据
App->>Redis: SET cache:user:123 {data} EX 3600
App-->>Client: 返回数据
end
写操作:先更新数据库,再删除缓存(而非更新缓存)。删除比更新更安全,因为避免了并发写导致的脏数据。
1 | |
缓存三大问题及应对
flowchart LR
subgraph 缓存穿透
A1["查询不存在的数据"] --> A2["每次都打到 DB"]
A2 --> A3["解决:布隆过滤器<br/>或缓存空值"]
end
subgraph 缓存击穿
B1["热点 Key 过期"] --> B2["大量请求同时打到 DB"]
B2 --> B3["解决:互斥锁重建<br/>或永不过期+异步更新"]
end
subgraph 缓存雪崩
C1["大量 Key 同时过期"] --> C2["DB 瞬间压力暴增"]
C2 --> C3["解决:过期时间加随机值<br/>或多级缓存"]
end
缓存穿透:恶意或错误请求查询不存在的数据,缓存永远不会命中。
1 | |
缓存击穿:某个热点 Key 恰好过期,瞬间大量请求涌入数据库。
1 | |
缓存雪崩:大量 Key 在同一时刻过期。
1 | |
取舍分析
- 优势:读性能提升 10-100 倍,显著降低 DB 压力
- 代价:数据一致性是最终一致(有短暂的不一致窗口);增加了系统复杂度(缓存更新策略、异常处理)
- 不适合:对一致性要求极高的场景(如金融交易余额),或数据量极大且无明显热点的场景
用例二:分布式会话(Session)
业务问题
Web 应用部署多个实例,用户登录后的 Session 如何在多实例间共享?
为什么选 Redis
| 维度 | Redis | Sticky Session | DB 存储 Session |
|---|---|---|---|
| 性能 | 亚毫秒 | 无额外开销 | 毫秒级 |
| 可用性 | 实例宕机不影响 | 实例宕机丢 Session | 高 |
| 扩展性 | 水平扩展 | 受限于单实例 | 受限于 DB |
| 运维 | 需维护 Redis | 负载均衡配置复杂 | DB 压力大 |
设计方案
flowchart LR
User["用户"] --> LB["负载均衡器"]
LB --> App1["App 实例 1"]
LB --> App2["App 实例 2"]
LB --> App3["App 实例 3"]
App1 --> Redis["Redis<br/>session:token123 → user_data"]
App2 --> Redis
App3 --> Redis
1 | |
Key Schema:session:{session_token},Token 由服务端生成(UUID 或 JWT 的 jti),通过 Cookie 或 Header 传递给客户端。
取舍分析
- 优势:无状态部署,实例可随意扩缩容;Session 天然支持 TTL 自动过期
- 代价:每次请求多一次 Redis 网络往返;Redis 宕机影响所有用户登录态
- 缓解:Redis Sentinel/Cluster 保证高可用;本地缓存 Session 减少网络调用
🔑 模式提炼:KV + TTL 万能临时状态
回顾用例一(缓存)和用例二(Session),它们的底层结构完全相同:一个 String Key 存一段数据,配一个过期时间。区别仅在于 Key 的命名和 TTL 的长短。
模式公式:SET {业务域}:{标识符} {状态数据} EX {生命周期秒数}
这个模式可以直接迁移到以下场景——只需要换参数:
| 场景 | Key | Value | TTL | 说明 |
|---|---|---|---|---|
| 缓存 | cache:user:123 |
序列化的用户数据 | 3600s | 降低 DB 压力 |
| Session | session:token |
用户登录态 JSON | 1800s | 多实例共享 |
| 短信验证码 | sms:code:13800138000 |
"384729" |
300s | 5 分钟有效 |
| 邮箱验证链接 | verify:email:uuid |
"user_123" |
86400s | 24 小时有效 |
| 短链接 | short:abc123 |
"https://..." |
永不过期或 30 天 | 302 跳转 |
| 接口幂等 | idempotent:order:req_uuid |
"1" |
600s | 防重复提交 |
| 分布式限流标记 | blocked:ip:1.2.3.4 |
"1" |
3600s | IP 封禁 1 小时 |
核心洞察:任何"有生命周期的临时状态",都可以用这个模式一行命令解决。你不需要为每种场景设计不同的方案——它们本质上是同一个东西,只是 Key 的命名空间和 TTL 不同。
用例三:分布式锁
业务问题
多个服务实例并发操作同一资源(如扣减库存),如何保证互斥?
为什么选 Redis
| 维度 | Redis | ZooKeeper | 数据库行锁 |
|---|---|---|---|
| 性能 | 极高(万级 QPS) | 中等 | 低 |
| 可靠性 | 需要额外机制防死锁 | 临时节点天然防死锁 | 高 |
| 复杂度 | 低 | 高(需 ZK 集群) | 低 |
| 适用场景 | 高性能互斥 | 强一致性场景 | 已有 DB 的简单场景 |
设计方案
基础版:SETNX + 过期时间
sequenceDiagram
participant Client1
participant Client2
participant Redis
Client1->>Redis: SET lock:order:123 uuid1 NX EX 30
Redis-->>Client1: OK(获锁成功)
Client2->>Redis: SET lock:order:123 uuid2 NX EX 30
Redis-->>Client2: nil(获锁失败)
Note over Client1: 执行业务逻辑...
Client1->>Redis: Lua: if GET == uuid1 then DEL
Redis-->>Client1: 释放成功
1 | |
1 | |
进阶:Redlock 算法
当 Redis 是单点时,主节点宕机可能导致锁丢失。Redlock 使用 N 个独立的 Redis 实例(通常 5 个),客户端需要在多数节点(≥ N/2 + 1)上成功加锁才算获锁。
flowchart TD
Client["客户端"] --> R1["Redis 1: SET lock NX"]
Client --> R2["Redis 2: SET lock NX"]
Client --> R3["Redis 3: SET lock NX"]
Client --> R4["Redis 4: SET lock NX"]
Client --> R5["Redis 5: SET lock NX"]
R1 -->|"OK"| Check["获锁成功 ≥ 3/5?"]
R2 -->|"OK"| Check
R3 -->|"OK"| Check
R4 -->|"nil"| Check
R5 -->|"OK"| Check
Check -->|"是 (4/5)"| Success["获锁成功<br/>有效时间 = TTL - 获锁耗时"]
Check -->|"否"| Fail["获锁失败<br/>释放所有已获取的锁"]
看门狗机制(锁续期)
业务执行时间可能超过锁的过期时间。Redisson 等客户端库实现了"看门狗"机制:后台线程定期检查锁是否仍被持有,如果是则自动续期。
sequenceDiagram
participant Client
participant Watchdog as 看门狗线程
participant Redis
Client->>Redis: SET lock:res uuid1 NX EX 30
Redis-->>Client: OK
Note over Client: 开始执行业务(可能耗时 > 30s)
loop 每 10 秒检查一次
Watchdog->>Redis: if GET lock:res == uuid1 then EXPIRE 30
Redis-->>Watchdog: OK(续期成功)
end
Client->>Redis: Lua: if GET == uuid1 then DEL
Note over Watchdog: 停止续期
取舍分析
- 优势:性能极高,实现简单,适合大多数互斥场景
- 代价:Redis 主从切换时可能短暂丢锁;Redlock 有争议(Martin Kleppmann vs Antirez 的经典论战)
- 建议:对锁的正确性要求不是"生死攸关"时用 Redis 锁;金融级场景考虑 ZooKeeper 或数据库乐观锁
🔑 模式提炼:原子占位(SET NX)
分布式锁的本质不是"锁",而是**“原子占位”**——在一个共享命名空间中,谁先到谁占坑,后来者被拒绝。SET key value NX 这五个字母,解决了一整类"只能有一个"的问题。
模式公式:SET {命名空间}:{资源标识} {持有者标识} NX EX {超时秒数}
| 场景 | Key | Value | 含义 |
|---|---|---|---|
| 分布式锁 | lock:order:123 |
uuid |
同一时刻只有一个实例处理该订单 |
| 幂等控制 | idempotent:pay:req_abc |
"1" |
同一个支付请求只处理一次 |
| 缓存击穿互斥 | lock:rebuild:user:123 |
"1" |
只有一个请求去重建缓存 |
| 定时任务防重 | cron:daily_report:20250728 |
instance_id |
今天的日报只由一个实例生成 |
| 抢购/秒杀资格 | seckill:item:789:user:456 |
"1" |
每人限购一次 |
核心洞察:当你听到"只能有一个"、“不能重复”、"谁先谁得"这类需求时,脑中应该立刻浮现 SET NX。它们都是同一个模式的不同参数化实例。
进阶认知:原子占位模式有三个关键细节,无论应用在哪个场景都适用:
- 必须有 TTL(防死锁/防泄漏)
- Value 必须唯一(防误释放别人的占位)
- 释放必须原子(Lua 脚本:先比较再删除)
用例四:计数器与限流器
业务问题
计数器
统计文章阅读量、点赞数、库存数量等,要求高并发下的原子递增/递减。
限流器
防止 API 被恶意调用或突发流量打垮服务,需要对请求频率进行限制。
为什么选 Redis
- 原子性:
INCR/DECR是原子操作,天然线程安全 - 性能:单实例 10 万+ QPS,远超数据库
- TTL:天然支持过期,适合滑动窗口等时间相关的计数
设计方案
简单计数器
1 | |
固定窗口限流
最简单的限流:在固定时间窗口内限制请求次数。
flowchart LR
subgraph "固定窗口 (1分钟)"
A["12:00:00 - 12:00:59<br/>限制 100 次"]
B["12:01:00 - 12:01:59<br/>限制 100 次"]
end
C["⚠️ 临界问题:<br/>12:00:50-12:01:10 的 20 秒内<br/>可能通过 200 次请求"]
1 | |
滑动窗口限流(推荐)
使用 Sorted Set 实现真正的滑动窗口,解决固定窗口的临界问题。
flowchart TD
A["请求到达"] --> B["ZREMRANGEBYSCORE<br/>移除窗口外的旧请求"]
B --> C["ZCARD<br/>统计窗口内请求数"]
C --> D{"请求数 < 限制?"}
D -->|"是"| E["ZADD 记录本次请求<br/>EXPIRE 设置 Key 过期"]
D -->|"否"| F["拒绝请求"]
1 | |
为了保证原子性,应使用 Lua 脚本将上述步骤合并:
1 | |
令牌桶限流
令牌桶允许一定程度的突发流量,比滑动窗口更灵活。核心思想:令牌以固定速率放入桶中,每个请求消耗一个令牌,桶满则丢弃新令牌。
flowchart LR
A["令牌以固定速率<br/>放入桶中"] --> B["桶 (容量上限)"]
B --> C["每个请求<br/>消耗一个令牌"]
C --> D{"桶中有令牌?"}
D -->|"有"| E["放行"]
D -->|"无"| F["拒绝/等待"]
使用 Hash 存储令牌桶状态,Lua 脚本保证原子性:
1 | |
取舍分析
- 优势:原子操作天然线程安全;性能远超数据库方案;TTL 自动清理过期数据
- 代价:计数器数据在内存中,Redis 宕机可能丢失(可通过持久化缓解);滑动窗口的 ZSet 在高并发下内存占用较大
- 不适合:需要精确持久化的计数(如账户余额),应使用数据库
用例五:排行榜
业务问题
游戏积分排名、热门文章排行、销量排行等,需要实时更新分数并快速查询排名。
为什么选 Redis
排行榜的核心操作是"按分数排序 + 查询排名",这恰好是 Sorted Set 的原生能力。用 MySQL 实现需要 ORDER BY score DESC LIMIT N,在数据量大时性能急剧下降。
| 操作 | Redis ZSet | MySQL |
|---|---|---|
| 更新分数 | O(log N) | O(log N) + 写磁盘 |
| 查询排名 | O(log N) | O(N log N) 全表排序 |
| Top N | O(log N + N) | O(N log N) |
| 查询周围排名 | O(log N) | 复杂子查询 |
设计方案
flowchart TD
subgraph "Sorted Set: leaderboard:global"
direction LR
A["player_2: 2100"]
B["player_1: 1800"]
C["player_3: 1000"]
end
D["ZADD 更新分数"] --> A
E["ZREVRANK 查排名"] --> A
F["ZREVRANGE 0 N Top N"] --> A
G["ZINCRBY 加分"] --> A
核心命令速查
1 | |
多维度排行榜(日榜/周榜/月榜)
1 | |
ZSet 命令分类记忆
| 类别 | 命令 | 说明 | 时间复杂度 |
|---|---|---|---|
| 写入 | ZADD |
添加/更新成员,支持 NX/XX/GT/LT 选项 | O(log N) |
| 增量 | ZINCRBY |
原子加分 | O(log N) |
| 排名 | ZRANK / ZREVRANK |
升序/降序排名(0-based) | O(log N) |
| 分数 | ZSCORE |
查询单个成员的分数 | O(1) |
| 范围-按排名 | ZRANGE / ZREVRANGE |
按排名范围获取成员 | O(log N + M) |
| 范围-按分数 | ZRANGEBYSCORE / ZREVRANGEBYSCORE |
按分数区间获取成员 | O(log N + M) |
| 删除 | ZREM |
移除成员 | O(log N) |
| 集合运算 | ZUNIONSTORE / ZINTERSTORE |
合并多个 ZSet | O(N) |
| 计数 | ZCARD / ZCOUNT |
总数 / 分数区间内的数量 | O(1) / O(log N) |
性能提示:ZSCORE 是 O(1) 的哈希查找,而 ZRANGEBYSCORE 是 O(log N + M) 的范围查询(M 为结果集大小)。查单个成员的分数永远用 ZSCORE。区间查找即使在 Redis 里也应该加上 LIMIT,避免返回过多数据。
Range 索引规则
Redis 的 ZRANGE/ZREVRANGE/LRANGE 等命令的索引规则:
- 索引从 0 开始,第一个元素索引为 0
- 支持负数索引:-1 表示最后一个元素,-2 表示倒数第二个
- start 和 stop 都是闭区间(包含两端),这与 Python 切片(左闭右开)不同
- 常用组合:
0 0= 第一个元素,0 -1= 所有元素,0 9= 前 10 个,-5 -1= 最后 5 个
取舍分析
- 优势:实时排名,O(log N) 的更新和查询;天然支持分页和范围查询
- 代价:全量数据在内存中,百万级用户的排行榜约占几十 MB;不支持复杂的多条件排序
- 不适合:需要按多个维度联合排序的场景(如"先按等级排,等级相同按经验排"),需要额外设计 Score 编码(如将两个维度编码到一个 double 中)
用例六:延时队列
业务问题
订单 30 分钟未支付自动取消、邮件定时发送、定时任务调度等——需要在未来某个时间点触发某个操作。
为什么选 Redis
| 维度 | Redis ZSet | RabbitMQ 延时插件 | 数据库轮询 | 时间轮 (HashedWheelTimer) |
|---|---|---|---|---|
| 精度 | 秒级 | 秒级 | 取决于轮询间隔 | 毫秒级 |
| 可靠性 | 需要额外保证 | 高(消息确认机制) | 高 | 进程内,宕机丢失 |
| 复杂度 | 低 | 中(需要 MQ 基础设施) | 低 | 低 |
| 吞吐量 | 高 | 高 | 低 | 高 |
设计方案
核心思想:将消息的到期时间戳作为 ZSet 的 Score,Worker 定期查询 Score ≤ 当前时间的消息并处理。
sequenceDiagram
participant Producer as 生产者
participant Redis as Redis ZSet
participant Worker as Worker 消费者
Producer->>Redis: ZADD delayed_queue:order_timeout<br/>1678886700 '{"order_id":"123","action":"cancel"}'
Note over Redis: Score = 到期时间戳<br/>Member = 消息体 (JSON)
loop 每秒轮询
Worker->>Redis: ZRANGEBYSCORE delayed_queue:order_timeout<br/>0 {current_timestamp} LIMIT 0 10
alt 有到期消息
Redis-->>Worker: 返回到期消息列表
Worker->>Worker: 执行业务逻辑(取消订单)
Worker->>Redis: ZREM delayed_queue:order_timeout {message}
else 无到期消息
Redis-->>Worker: 空列表
Worker->>Worker: sleep(1)
end
end
核心命令
1 | |
关键点:ZRANGEBYSCORE 只是查询,不会移除元素。必须通过 ZREM 显式移除已处理的消息。
原子性保证:Lua 脚本
在多 Worker 场景下,"查询"和"移除"必须是原子操作,否则同一条消息可能被多个 Worker 重复处理。
1 | |
消费者伪代码
1 | |
设计要点
- 队列 Key 命名:
delayed_queue:{业务类型},如delayed_queue:order_timeout、delayed_queue:email_reminders - Member 是消息体本身(JSON 字符串),而不是指向 Hash 的引用
- 消息处理必须幂等:即使因故障重复处理,结果也应该一致
- 批量消费时的分页:如果一次取多条,取完后以本批最大 Score 作为下一次查询的起始值
取舍分析
- 优势:实现简单,无需额外中间件;精度可达秒级;支持任意延迟时间
- 代价:轮询有固有延迟(通常 1 秒);消息可靠性不如专业 MQ(无 ACK 机制,需自行实现);大量延时消息占用内存
- 建议:轻量级延时任务用 Redis;对可靠性要求高的场景用 RabbitMQ/RocketMQ 的延时消息功能
🔑 模式提炼:Score 即时间轴
回顾用例四的滑动窗口限流、用例五的排行榜、用例六的延时队列,它们用的都是 ZSet,但 Score 的含义不同:
| 用例 | Score 的含义 | Member 的含义 | 核心查询 |
|---|---|---|---|
| 排行榜 | 分数/积分 | 玩家 ID | ZREVRANGE(按分数从高到低) |
| 延时队列 | 到期时间戳 | 消息体 JSON | ZRANGEBYSCORE 0 now(到期的消息) |
| 滑动窗口限流 | 请求时间戳 | 请求唯一标识 | ZCARD(窗口内请求数) |
| 时间线 Feed | 发布时间戳 | 帖子 ID | ZREVRANGE(最新的帖子) |
| 优先级队列 | 优先级数值 | 任务 ID | ZRANGEBYSCORE(最高优先级) |
模式公式:ZADD {队列名} {时间戳或分数} {元素},然后用 ZRANGEBYSCORE 或 ZREVRANGE 做范围查询。
核心洞察:ZSet 的本质是一条可查询的数轴。Score 可以是任何有序的数值——时间戳、分数、优先级、权重、距离。只要你的业务需要"按某个数值排序并做范围查询",就应该想到 ZSet。
迁移清单——把 Score 换成不同含义,就得到不同的系统:
1 | |
用例七:社交关系(关注/粉丝/共同好友)
业务问题
社交网络中的关注、粉丝、共同好友、推荐好友等功能,核心是集合的交集、并集、差集运算。
为什么选 Redis
关系型数据库处理"共同好友"需要两次 JOIN 或子查询,性能随数据量增长急剧下降。Redis 的 Set 提供了 O(N) 的集合运算,且全在内存中完成。
设计方案
flowchart TD
subgraph "用户 A 的关系"
A1["following:A = {B, C, D}"]
A2["followers:A = {B, E}"]
end
subgraph "用户 B 的关系"
B1["following:B = {A, C, E}"]
B2["followers:B = {A, D}"]
end
A1 --- |"SINTER following:A following:B"| Common["共同关注: {C}"]
A1 --- |"SDIFF following:B following:A"| Recommend["推荐关注: {E}<br/>(B 关注了但 A 没关注)"]
数据结构
每个用户维护两个 Set:
1 | |
核心操作
1 | |
互相关注判断优化
判断两个用户是否互相关注,可以维护一个额外的"好友"Set:
1 | |
取舍分析
- 优势:集合运算是 Redis 的原生能力,性能极高;
SISMEMBERO(1) 判断关系 - 代价:大 V 用户的粉丝列表可能有千万级成员,单个 Set 过大会影响性能和内存;双向写入需要保证一致性
- 缓解:大 V 粉丝列表可以只存计数,不存完整列表;使用 Pipeline 或 Lua 脚本保证双向写入的原子性
- 不适合:需要复杂查询的关系(如"关注了 A 且同时关注了 B 的用户中,最近 7 天活跃的"),应使用图数据库(如 Neo4j)
🔑 模式提炼:双向关系 + 集合运算
社交关系的核心不是"关注"这个动作,而是**“一个关系写两个 Set,然后用集合运算回答所有关系问题”**。这个模式可以迁移到任何存在双向关系的场景。
模式公式:
- 建立关系:
SADD {关系}:{主体} {客体}+SADD {反向关系}:{客体} {主体} - 查询关系:
SINTER/SUNION/SDIFF
| 场景 | Set A | Set B | SINTER | SDIFF |
|---|---|---|---|---|
| 社交关注 | following:A |
following:B |
共同关注 | 推荐关注 |
| 标签系统 | tag:java的文章 |
tag:redis的文章 |
同时有两个标签的文章 | 只有 java 没有 redis 的文章 |
| 权限系统 | role:admin的权限 |
role:editor的权限 |
两个角色共有的权限 | admin 独有的权限 |
| 商品属性 | brand:nike的商品 |
color:red的商品 |
红色的 Nike 商品 | Nike 非红色商品 |
| 课程选修 | student:A的课程 |
student:B的课程 |
共同选修的课程 | A 选了 B 没选的课程 |
核心洞察:当你看到"共同的 XX"、“推荐 XX”、"互相 XX"这类需求时,本质上都是集合的交集、差集运算。一个关系建两个 Set,三种运算回答所有问题。
注意边界:当单个 Set 的成员数超过万级时(如大 V 的粉丝列表),SINTER/SDIFF 的 O(N) 复杂度会成为瓶颈。此时应考虑:只存计数不存完整列表,或将大 Set 拆分到多个 Key。
用例八:二级评论系统
业务问题
文章/帖子下的评论系统,支持一级评论和对评论的回复(二级评论),需要按时间排序和分页。
为什么选 Redis
评论系统的读远多于写,且需要按时间排序和分页——这正是 ZSet 的强项。Redis 可以作为评论系统的读缓存层,配合数据库做持久化。
设计方案
三层存储设计
flowchart TD
subgraph "数据模型"
H1["Hash: comment:1001<br/>content, user_id, post_id,<br/>parent_id=0, timestamp, status"]
H2["Hash: comment:1002<br/>content, user_id, post_id,<br/>parent_id=1001, timestamp, status"]
Z1["ZSet: post:123:comments<br/>member=1001, score=timestamp"]
Z2["ZSet: comment:1001:replies<br/>member=1002, score=timestamp"]
end
Z1 -->|"ZREVRANGE"| H1
Z2 -->|"ZREVRANGE"| H2
H2 -->|"parent_id"| H1
- 评论内容(Hash):每条评论一个 Hash,存储完整信息
- 文章的一级评论列表(ZSet):Score 为时间戳,Member 为评论 ID
- 评论的回复列表(ZSet):Score 为时间戳,Member 为回复 ID
1 | |
核心操作
1 | |
读取流程
sequenceDiagram
participant Client
participant App
participant Redis
Client->>App: GET /post/123/comments?page=1
App->>Redis: ZREVRANGE post:123:comments 0 9
Redis-->>App: [1001, 1003, 1005, ...]
Note over App: 使用 Pipeline 批量获取
App->>Redis: Pipeline: HGETALL comment:1001,<br/>HGETALL comment:1003, ...
Redis-->>App: 所有评论内容
App->>Redis: Pipeline: ZREVRANGE comment:1001:replies 0 2,<br/>ZREVRANGE comment:1003:replies 0 2, ...
Redis-->>App: 所有回复 ID 列表
App->>Redis: Pipeline: HGETALL comment:{reply_ids}...
Redis-->>App: 所有回复内容
App-->>Client: 组装后的评论树
设计要点:回复只加入父评论的 replies 列表,不加入文章的 comments 列表——这是"主表持有关联表主键控制权,从表通过 parent_id 寻找父节点"的设计模式。使用 Pipeline 批量发送命令,将多次网络往返合并为一次。
取舍分析
- 优势:读性能极高,天然支持按时间排序和分页
- 代价:数据冗余(Hash + ZSet 双写);不支持复杂查询(如"搜索包含某关键词的评论");需要配合数据库做持久化
- 建议:Redis 作为读缓存层,数据库作为持久化层;写操作先写 DB 再同步到 Redis
用例九:消息队列
业务问题
服务间异步通信、事件驱动架构、削峰填谷等场景需要消息队列。
Redis 提供了三种消息队列方案
flowchart TD
A["Redis 消息队列方案"] --> B["List<br/>LPUSH/BRPOP"]
A --> C["Pub/Sub<br/>PUBLISH/SUBSCRIBE"]
A --> D["Stream<br/>XADD/XREADGROUP"]
B --> B1["✅ 简单可靠<br/>❌ 不支持多消费者组<br/>❌ 无 ACK 机制"]
C --> C1["✅ 广播模式<br/>❌ 消息不持久化<br/>❌ 离线消费者丢消息"]
D --> D1["✅ 消费者组<br/>✅ 消息持久化<br/>✅ ACK 机制<br/>✅ 最接近专业 MQ"]
方案一:List 队列
最简单的消息队列,使用 List 的 LPUSH/BRPOP 实现。
1 | |
BRPOP vs RPOP:BRPOP 是阻塞版本,队列为空时会等待而非返回 nil,避免了空轮询消耗 CPU。
方案二:Pub/Sub 发布订阅
适合实时广播场景(如聊天室、实时通知),但消息不持久化。
1 | |
致命缺陷:如果订阅者不在线,消息会永久丢失。Pub/Sub 没有消息积压能力,不适合需要可靠投递的场景。
方案三:Stream(Redis 5.0+,推荐)
Stream 是最接近专业消息队列的方案,支持消费者组、消息确认、消息回溯。
sequenceDiagram
participant Producer
participant Stream as Redis Stream
participant C1 as Consumer 1
participant C2 as Consumer 2
Producer->>Stream: XADD mystream * key value
Note over Stream: 消息 ID: 1678886400000-0
C1->>Stream: XREADGROUP GROUP mygroup consumer1<br/>COUNT 1 BLOCK 5000 STREAMS mystream >
Stream-->>C1: 消息分配给 Consumer 1
C2->>Stream: XREADGROUP GROUP mygroup consumer2<br/>COUNT 1 BLOCK 5000 STREAMS mystream >
Stream-->>C2: 下一条消息分配给 Consumer 2
C1->>Stream: XACK mystream mygroup 1678886400000-0
Note over C1: 确认消息已处理
1 | |
三种方案对比
| 特性 | List | Pub/Sub | Stream |
|---|---|---|---|
| 消息持久化 | ✅ | ❌ | ✅ |
| 消费者组 | ❌ | ❌ | ✅ |
| 消息确认 (ACK) | ❌ | ❌ | ✅ |
| 阻塞读取 | ✅ (BRPOP) | ✅ (SUBSCRIBE) | ✅ (BLOCK) |
| 消息回溯 | ❌ | ❌ | ✅ |
| 广播 | ❌ | ✅ | ✅(多消费者组) |
| 适用场景 | 简单任务队列 | 实时通知/广播 | 可靠消息队列 |
取舍分析
- 优势:无需额外中间件,Stream 功能已接近 Kafka/RabbitMQ 的基础能力
- 代价:消息堆积受内存限制;不支持事务消息、延时消息等高级特性;集群模式下 Stream 不支持跨分片
- 建议:轻量级异步任务用 Redis Stream;高可靠、高吞吐场景用 Kafka/RocketMQ
🔑 模式提炼:实体 + 索引分离
回顾用例八(评论系统)和用例九(消息队列),它们都使用了同一个架构模式:Hash 存实体内容,ZSet/Set/List 存索引。这是 Redis 中最重要的复合数据建模模式。
模式公式:
- 实体存储:
HSET {实体类型}:{ID} field1 value1 field2 value2 ... - 索引维护:
ZADD {父实体}:{父ID}:{子实体列表} {排序分数} {子实体ID} - 读取流程:先查索引拿 ID 列表 → 再用 Pipeline 批量查实体内容
flowchart LR
subgraph "通用模式"
Index["索引层<br/>ZSet/Set/List<br/>存 ID + 排序信息"] -->|"Pipeline 批量查"| Entity["实体层<br/>Hash<br/>存完整内容"]
end
| 场景 | 实体 (Hash) | 索引 (ZSet) | Score |
|---|---|---|---|
| 评论系统 | comment:{id} → 评论内容 |
post:{id}:comments → 评论 ID 列表 |
时间戳 |
| 商品列表 | product:{id} → 商品详情 |
category:{id}:products → 商品 ID 列表 |
销量/价格 |
| 用户动态 | post:{id} → 帖子内容 |
user:{id}:timeline → 帖子 ID 列表 |
发布时间 |
| 搜索缓存 | doc:{id} → 文档内容 |
search:{keyword}:results → 文档 ID 列表 |
相关度 |
| 订单列表 | order:{id} → 订单详情 |
user:{id}:orders → 订单 ID 列表 |
下单时间 |
核心洞察:这个模式本质上是关系型数据库的"主表 + 索引"在 Redis 中的映射。Hash 相当于数据行,ZSet 相当于索引。区别在于:索引需要你手动维护(写入时双写),但读取性能远超数据库。
关键技巧:
- Pipeline 是必须的——先查索引拿到 N 个 ID,再用 Pipeline 一次性发 N 个
HGETALL,将 N 次网络往返压缩为 1 次 - 索引和实体的一致性——写入时必须同时写 Hash 和 ZSet(用 Pipeline 或 Lua 保证)
- 删除要清理两处——删实体时别忘了从索引中移除
用例十:地理位置服务(附近的人)
业务问题
“附近的人”、“附近的餐厅”、“3 公里内的门店”——基于经纬度的距离计算和范围查询。
为什么选 Redis
Redis 的 GEO 命令底层使用 GeoHash 编码 + Sorted Set,将二维坐标映射为一维分数,利用 ZSet 的范围查询能力实现地理搜索。
| 维度 | Redis GEO | MySQL 空间索引 | ElasticSearch |
|---|---|---|---|
| 性能 | 极高(内存计算) | 中等(磁盘 I/O) | 中等 |
| 功能 | 距离计算、圆形范围搜索 | 完整空间查询(多边形等) | 全文搜索 + 地理混合 |
| 精度 | 米级 | 高 | 高 |
| 适用场景 | 简单的"附近"查询 | 复杂空间关系 | 搜索 + 地理混合查询 |
设计方案
flowchart LR
subgraph "GEO 底层原理"
A["经纬度 (lng, lat)"] --> B["GeoHash 编码"]
B --> C["52位整数"]
C --> D["作为 ZSet 的 Score 存储"]
end
subgraph "范围查询"
E["中心点 + 半径"] --> F["计算 GeoHash 范围"]
F --> G["ZRANGEBYSCORE 范围查询"]
G --> H["过滤 + 精确距离计算"]
end
1 | |
取舍分析
- 优势:开箱即用,无需额外地理索引;性能极高(内存计算);API 简洁
- 代价:只支持圆形范围查询(不支持多边形围栏);不支持复杂空间关系(如"在某条路线上的");数据量大时内存占用高
- 不适合:需要多边形围栏、路径规划等复杂地理计算的场景,应使用 PostGIS 或 ElasticSearch
用例十一:UV 统计(HyperLogLog)
业务问题
统计网站的独立访客数(UV)、独立 IP 数等,要求去重计数。如果用 Set 存储每个访客 ID,百万级 UV 需要几十 MB 内存。
为什么选 Redis
HyperLogLog 是一种概率数据结构,用 固定 12 KB 内存 就能统计 2^64 个不同元素的基数,误差率仅 0.81%。
| 方案 | 内存占用(100 万 UV) | 精确度 | 支持合并 |
|---|---|---|---|
| Set | ~40 MB | 精确 | ✅ (SUNION) |
| Bitmap | ~125 KB(ID 为连续整数时) | 精确 | ✅ (BITOP OR) |
| HyperLogLog | 12 KB | 误差 ≤ 0.81% | ✅ (PFMERGE) |
设计方案
1 | |
取舍分析
- 优势:极低内存占用(12 KB/Key,无论元素数量多少);支持合并统计;O(1) 的添加和查询
- 代价:有 0.81% 的误差;不能查询"某个元素是否存在";不能获取元素列表
- 适用:UV、独立 IP 等对精确度要求不高的大规模去重计数
- 不适用:需要精确计数或需要知道具体元素的场景
用例十二:布隆过滤器
业务问题
判断一个元素是否可能存在于一个大集合中。典型场景:缓存穿透防护、垃圾邮件过滤、推荐系统去重(“用户是否已经看过这篇文章”)。
为什么选 Redis
Redis 4.0+ 通过 RedisBloom 模块 提供了布隆过滤器。也可以用 Bitmap 手动实现。
flowchart TD
subgraph "布隆过滤器原理"
A["元素 X"] --> B["Hash 函数 1 → 位置 3"]
A --> C["Hash 函数 2 → 位置 7"]
A --> D["Hash 函数 3 → 位置 11"]
B --> E["位数组: 将位置 3 设为 1"]
C --> F["位数组: 将位置 7 设为 1"]
D --> G["位数组: 将位置 11 设为 1"]
end
subgraph "查询"
H["查询元素 Y"] --> I["计算 3 个 Hash 值"]
I --> J{"所有对应位都是 1?"}
J -->|"是"| K["可能存在<br/>(有假阳性概率)"]
J -->|"否"| L["一定不存在<br/>(100% 准确)"]
end
核心特性:
- 说"不存在"→ 一定不存在(零假阴性)
- 说"存在"→ 可能存在(有假阳性概率,可通过参数控制)
设计方案
1 | |
缓存穿透防护实战
sequenceDiagram
participant Client
participant App
participant BloomFilter as 布隆过滤器
participant Cache as Redis Cache
participant DB
Client->>App: 查询 user_id=999999
App->>BloomFilter: BF.EXISTS user_filter 999999
alt 布隆过滤器说"不存在"
BloomFilter-->>App: 0(一定不存在)
App-->>Client: 返回空(不查 DB,拦截穿透)
else 布隆过滤器说"可能存在"
BloomFilter-->>App: 1
App->>Cache: GET cache:user:999999
alt 缓存命中
Cache-->>App: 数据
else 缓存未命中
App->>DB: SELECT ...
DB-->>App: 数据或空
App->>Cache: SET cache:user:999999 ...
end
App-->>Client: 返回结果
end
取舍分析
- 优势:极低内存占用(比 Set 小一个数量级);O(k) 的查询(k 为哈希函数数量,通常 3-7)
- 代价:有假阳性概率(可通过增大位数组和哈希函数数量降低);不支持删除元素(删除会影响其他元素的判断,因为多个元素可能共享同一个位)
- 变体:Cuckoo Filter(
CF.*命令)支持删除操作,但内存占用略高
🔑 模式提炼:概率换空间
HyperLogLog 和布隆过滤器看似是两个独立的数据结构,但它们背后是同一个设计哲学:用可控的误差换取数量级的内存节省。
模式公式:当精确方案的内存成本不可接受时,问自己——“我能容忍多大的误差?”
| 维度 | 精确方案 (Set) | HyperLogLog | 布隆过滤器 |
|---|---|---|---|
| 回答的问题 | “有哪些元素?有多少个?” | “大约有多少个不同元素?” | “这个元素是否可能存在?” |
| 100 万元素内存 | ~40 MB | 12 KB | ~1.2 MB (0.1% 误判率) |
| 节省倍数 | 基准 | 3300 倍 | 33 倍 |
| 误差 | 0 | ≤ 0.81% | 可配置(越低越耗内存) |
| 能否枚举元素 | ✅ | ❌ | ❌ |
| 能否删除元素 | ✅ | ❌ | ❌(Cuckoo Filter 可以) |
决策树——什么时候用概率结构:
flowchart TD
A["需要去重/计数"] --> B{"需要知道具体是哪些元素?"}
B -->|"是"| C["用 Set(精确)"]
B -->|"否"| D{"需要知道精确数量?"}
D -->|"是"| C
D -->|"否"| E{"只需要判断存在性?"}
E -->|"是"| F["布隆过滤器"]
E -->|"否"| G["HyperLogLog"]
可迁移场景:
| 场景 | 概率结构 | 替代的精确方案 | 节省 |
|---|---|---|---|
| UV 统计 | HyperLogLog | Set 存所有用户 ID | 3000x |
| 缓存穿透防护 | 布隆过滤器 | Set 存所有合法 ID | 30x |
| 推荐去重(“用户看过这篇文章吗”) | 布隆过滤器 | Set 存用户已读列表 | 30x |
| 垃圾邮件过滤 | 布隆过滤器 | Set 存所有垃圾邮件特征 | 30x |
| 数据库行存在性预检 | 布隆过滤器 | 每次查 DB | 减少无效 I/O |
核心洞察:在大规模系统中,"精确"往往是一种奢侈。当数据量达到百万、千万级时,先问自己:“我真的需要精确答案吗?” 如果 0.81% 的误差可以接受,你就能用 12 KB 替代 40 MB。
用例十三:分布式 ID 生成
业务问题
分布式系统中需要全局唯一、趋势递增的 ID,用于数据库主键、订单号等。
为什么选 Redis
Redis 的 INCR 命令是原子操作,天然保证唯一性和严格递增性。
| 方案 | 性能 | 趋势递增 | 全局唯一 | 依赖 |
|---|---|---|---|---|
| UUID | 极高(本地生成) | ❌ 无序 | ✅ | 无 |
| 数据库自增 | 低(磁盘 I/O) | ✅ | ✅ | DB |
| Redis INCR | 高(内存操作) | ✅ | ✅ | Redis |
| Snowflake | 极高(本地生成) | ✅ | ✅ | 时钟同步 |
设计方案
1 | |
带业务前缀的 ID 生成(Lua 脚本)
1 | |
取舍分析
- 优势:简单可靠,性能高(10 万+ QPS);严格递增
- 代价:依赖 Redis 可用性(Redis 宕机则无法生成 ID);单点瓶颈(可通过批量获取缓解);ID 连续可能暴露业务量
- 建议:中小规模系统用 Redis INCR;大规模系统用 Snowflake 算法(无中心依赖,本地生成)
用例十四:签到系统(Bitmap)
业务问题
用户每日签到、连续签到天数统计、月度签到日历等。
为什么选 Redis
Bitmap 用 1 bit 表示一天的签到状态,一个用户一年的签到数据只需 365 bits ≈ 46 bytes。百万用户一年的签到数据约 46 MB。
设计方案
flowchart LR
subgraph "Bitmap: sign:user:123:202507"
direction LR
B0["bit0=1<br/>7/1 ✅"]
B1["bit1=1<br/>7/2 ✅"]
B2["bit2=0<br/>7/3 ❌"]
B3["bit3=1<br/>7/4 ✅"]
B4["..."]
B30["bit30=1<br/>7/31 ✅"]
end
1 | |
取舍分析
- 优势:极低内存占用(1 bit/天/用户);
BITCOUNT高效统计;BITOP支持跨天聚合分析 - 代价:只能存储 0/1 二值状态;用户 ID 作为 offset 时必须是非负整数(且最好连续,否则稀疏 ID 会浪费空间)
- 适用:签到、在线状态、功能开关、A/B 测试分组等二值状态场景
全景对比:Redis 用例选型决策
数据结构 → 用例映射总览
mindmap
root((Redis 用例全景))
String
缓存
会话管理
分布式锁
计数器
分布式 ID
Hash
对象缓存
购物车
评论内容存储
令牌桶状态
List
简单消息队列
最新动态流
操作日志
Set
社交关系
标签系统
抽奖去重
Sorted Set
排行榜
延时队列
滑动窗口限流
时间线
评论排序列表
HyperLogLog
UV 统计
独立 IP 计数
Bitmap
签到打卡
在线状态
布隆过滤器
GEO
附近的人
门店搜索
Stream
可靠消息队列
事件溯源
Redis vs 替代方案决策矩阵
| 场景 | 首选 Redis 的条件 | 考虑替代方案的条件 | 推荐替代方案 |
|---|---|---|---|
| 缓存 | 动态热点数据,多实例共享 | 极热点数据,单实例内使用 | 本地缓存 (Caffeine) |
| 分布式锁 | 高性能互斥,容忍极端情况丢锁 | 强一致性要求,不容忍任何丢锁 | ZooKeeper / etcd |
| 消息队列 | 轻量级异步,消息量不大 | 高可靠、高吞吐、需要事务消息 | Kafka / RocketMQ |
| 排行榜 | 实时排名,单维度排序 | 复杂多维排序,全文搜索 | ElasticSearch |
| 社交关系 | 简单关系运算(共同好友等) | 复杂图遍历(六度分隔等) | Neo4j / JanusGraph |
| 地理位置 | 简单圆形范围查询 | 多边形围栏、路径规划 | PostGIS / ElasticSearch |
| 计数/统计 | 实时计数,允许宕机丢失 | 精确持久化计数(如余额) | 关系型数据库 |
| 全文搜索 | ❌ 不适合 | 任何搜索场景 | ElasticSearch / Solr |
| ACID 事务 | ❌ 不适合 | 需要跨表事务一致性 | 关系型数据库 |
Redis 的通用局限性
在选择 Redis 方案时,始终需要考虑以下约束:
- 内存限制:所有数据在内存中,成本高于磁盘存储。需要评估数据量和增长趋势,设置
maxmemory和淘汰策略。 - 持久性风险:即使开启 RDB/AOF,极端情况下仍可能丢失少量数据(RDB 丢失最后一次快照后的数据,AOF 在
everysec模式下最多丢 1 秒)。关键数据必须有数据库兜底。 - 单线程模型:Redis 6.0 前命令执行是单线程的(6.0 后 I/O 多线程,但命令执行仍是单线程)。单个慢命令(如
KEYS *、大 Key 的HGETALL、SMEMBERS)会阻塞所有请求。 - 数据结构限制:不支持 JOIN、聚合、全文搜索等复杂查询。
- 一致性模型:Redis 主从复制是异步的,主节点宕机可能丢失未同步到从节点的数据。
Key 命名最佳实践
1 | |
命名规则:
- 使用冒号
:分隔层级(Redis 社区惯例) - 使用小写字母和下划线
- Key 名要有业务含义,避免无意义缩写
- 控制 Key 长度(过长浪费内存和网络带宽,过短难以理解)
通用设计原则
- 先抽象操作,再选数据结构:不要先想"我要用 Redis 的什么命令",而是先想"我的业务本质上是什么操作"——排序?去重?计数?集合运算?
- Redis 是加速层,不是持久层:除非数据丢失完全可以接受(如缓存、限流计数),否则 Redis 应该配合数据库使用。
- 避免大 Key:单个 Key 的 Value 不要超过 10 MB。大 Set/ZSet/Hash 考虑分片(如按用户 ID 取模分到多个 Key)。
- 善用 Pipeline 和 Lua:Pipeline 减少网络往返(批量读取),Lua 脚本保证多命令的原子性(如"查询+删除"、“比较+设置”)。
- 设置 TTL:所有缓存类 Key 都应该有过期时间,防止内存泄漏。业务数据的 Key 也应该有合理的过期策略。
- 监控和告警:关注
used_memory(内存使用率)、keyspace_hits/misses(缓存命中率)、slowlog(慢查询日志)、connected_clients(连接数)。
模式速查表:从问题到方案的快速映射
读完全文,你应该能记住这张表。遇到任何新问题时,先在左列找到匹配的"问题特征",右列就是你的起点方案。
听到什么关键词 → 想到什么模式
| 你听到的需求关键词 | 对应模式 | Redis 方案 | 一句话口诀 |
|---|---|---|---|
| “缓存”、“加速”、“热点” | KV + TTL | SET key data EX ttl |
有生命周期的状态 = String + TTL |
| “Session”、“验证码”、“临时令牌” | KV + TTL | SET key data EX ttl |
同上,换个 Key 和 TTL |
| “锁”、“互斥”、“只能一个”、“幂等” | 原子占位 | SET key uuid NX EX ttl |
谁先到谁占坑 |
| “排行榜”、“Top N”、“排名” | Score 即时间轴 | ZADD + ZREVRANGE |
Score = 分数,ZSet = 数轴 |
| “延时”、“定时”、“30分钟后” | Score 即时间轴 | ZADD + ZRANGEBYSCORE |
Score = 到期时间戳 |
| “限流”、“频率控制”、“滑动窗口” | Score 即时间轴 + Lua | ZADD + ZCARD + Lua |
Score = 请求时间戳 |
| “共同好友”、“推荐”、“交集” | 双向关系 + 集合运算 | SADD + SINTER/SDIFF |
一个关系两个 Set |
| “评论列表”、“商品列表”、“动态流” | 实体 + 索引分离 | Hash + ZSet + Pipeline | Hash 存内容,ZSet 存索引 |
| “UV”、“独立访客”、“去重计数” | 概率换空间 | PFADD + PFCOUNT |
12 KB 统计 2^64 个元素 |
| “是否存在”、“穿透防护”、“预检” | 概率换空间 | BF.ADD + BF.EXISTS |
说不在就一定不在 |
| “消息队列”、“异步”、“事件” | 生产者-消费者 | Stream XADD/XREADGROUP |
轻量级用 Redis,重量级用 Kafka |
| “附近的人”、“距离”、“范围搜索” | Score 即时间轴(GEO 变体) | GEOADD + GEOSEARCH |
GEO 底层就是 ZSet |
| “签到”、“在线状态”、“开关” | Bitmap 二值状态 | SETBIT + BITCOUNT |
1 bit 表示一个布尔值 |
| “全局 ID”、“自增序号” | 原子递增 | INCR / INCRBY |
原子操作天然唯一 |
七大模式的组合使用
实际系统中,一个功能往往是多个模式的组合:
flowchart TD
subgraph "电商秒杀系统"
A["模式②原子占位<br/>SET NX 抢购资格"] --> B["模式⑦Lua原子胶水<br/>扣减库存"]
B --> C["模式③Score时间轴<br/>延时队列:未支付自动取消"]
C --> D["模式①KV+TTL<br/>订单缓存"]
end
subgraph "社交 Feed 系统"
E["模式④双Set集合运算<br/>关注/粉丝关系"] --> F["模式⑤实体+索引分离<br/>帖子内容+时间线索引"]
F --> G["模式③Score时间轴<br/>按时间排序的 Feed"]
G --> H["模式⑥概率换空间<br/>布隆过滤器去重已读"]
end
最后的建议:不要试图记住 14 个用例的所有细节。记住七个模式和这张速查表就够了。遇到新问题时,先识别它属于哪个模式,再查对应的命令和 Key 设计——这就是从"一个个问题"到"一类类问题"的思维跃迁。





