第 11 章 Redis:缓存原理与实践
Redis 在系统设计中的定位不只是“快一点的 KV”。它既可以做缓存,也可以承担计数、排行榜、延时任务、分布式协调等角色。要用好 Redis,关键在于先理解它为什么快、擅长什么,再明确哪些问题不该交给它解决。
Redis 的核心模型与高性能来源
Redis 的核心特征可以概括为三点:内存存储、单线程命令执行模型、面向场景设计的数据结构。绝大多数请求直接命中内存,避免了磁盘寻址;单线程把命令串行化,减少锁竞争;而 String、Hash、List、Set、ZSet 又让很多业务需求能以接近 O(1) 或 O(log n) 的方式完成。
| 特性 | 价值 |
|---|---|
| 内存存储 | 微秒到毫秒级访问延迟 |
| 单线程命令执行 | 避免复杂锁竞争与上下文切换 |
| 多路复用 IO | 用一个线程处理大量连接 |
| 场景化数据结构 | 减少业务层重复造轮子 |
“Redis 为什么快”通常可以从四个角度回答:
- 数据主要在内存中,避免磁盘 IO。
- 数据结构专门为高频操作设计。
- 命令执行串行化,减少锁开销。
- 网络层使用 IO 多路复用支撑高并发连接。
但这套高性能模型也天然带来边界:大 Key、复杂脚本、阻塞命令、全量扫描都会拖慢整个实例。Redis 的快,建立在“单次操作足够短小”这个前提之上。
数据结构与典型使用模式
String。 Redis String 的底层是 SDS,而不是 C 风格字符串。SDS 通过记录长度与剩余空间,避免了频繁遍历和越界问题,适合计数器、分布式 ID、简单对象缓存、限流键等场景。
常见模式:
INCR做访问计数、库存扣减、限流计数。SET key value EX ttl做简单对象缓存。- 短字符串走
embstr编码,整数值可走int编码,节省内存和分配成本。
Hash。 Hash 适合存储字段较少、需要局部更新的对象,例如商品基础信息、Session、用户状态。相比把整个 JSON 放进 String,Hash 的优势是可以只更新一个字段,不必整对象反序列化再回写。
Redis 会在“小对象”与“大对象”之间切换编码:
- 小 Hash 倾向使用紧凑编码,节省内存。
- 字段数或字段长度超过阈值后切到哈希表,实现更快查找。
工程上要警惕“大 Hash”:
HGETALL可能阻塞。- 集群中容易形成热点。
- 持久化和复制的代价会明显上升。
List。 List 适合先进先出队列、最近浏览、时间线等场景。现代 Redis 使用 quicklist,把多个小块组织起来,在插入效率和内存占用之间做折中。
典型模式:
LPUSH+RPOP做简单队列。BRPOP做阻塞消费。LPUSH+LTRIM保留“最近 10 条浏览记录”。
如果业务需要可靠消费、回溯和消费组语义,List 往往只是过渡方案,最终还是会演进到专业 MQ。
Set。 Set 适合标签、去重、共同好友、点赞用户集合等场景。它擅长的是“存在性”和“集合运算”,例如交集、并集、差集。
ZSet。 ZSet 通过 score 排序,典型用于排行榜、延时队列、推荐权重排序。它在小数据量时可以走紧凑编码,规模变大后通常用跳表加哈希表组合,同时兼顾按成员查询和按分值范围查询。
一个实用判断是:如果需求里同时出现“排名”“TopN”“定时到期”“按权重取前几名”,优先想到 ZSet。
持久化、复制与集群
Redis 的数据虽然主要在内存里,但线上系统通常不会接受“进程一挂数据全没”,因此要根据业务要求配置持久化与高可用。
持久化。
| 方案 | 特点 | 适用场景 |
|---|---|---|
| RDB | 周期快照,文件紧凑,恢复快 | 可接受少量数据回退 |
| AOF | 追加写命令,恢复更细粒度 | 更关注数据完整性 |
RDB 的优势是生成文件紧凑、恢复速度快,对冷备和全量恢复友好;缺点是两次快照之间的数据可能丢失。AOF 的优势是更接近操作日志,数据损失窗口更小;代价是文件更大、重写和恢复时间更长。很多生产环境会结合两者使用,在恢复速度和数据完整性之间做平衡。
主从复制。 主从模式主要解决读扩展和基础灾备:
- 主节点负责写入。
- 从节点异步追赶主节点。
- 可以承接只读流量。
它的问题也很明确:存在复制延迟,且主节点故障后还需要额外机制完成自动切换。
Sentinel 与 Cluster。 Sentinel 在主从之上补上监控、故障检测和自动切换能力,适合单分片但希望具备自动主备切换的场景。它解决的是“主挂了怎么办”,不解决“容量不够怎么办”。
Cluster 则进一步提供数据分片能力,把 key 映射到 16384 个槽位,实现横向扩容。它更适合缓存容量和吞吐都持续增长的业务,但同时引入了新的限制:
- 多 key 操作必须考虑槽位分布。
- 跨槽事务与 Lua 脚本受到限制。
- 热点 key 依然可能把单节点打爆。
因此,Redis Cluster 不是“开了就自动水平扩展一切问题”,它只是把容量问题显式搬到了分片治理层。
缓存设计与一致性问题
缓存设计的核心不是“把什么放进去”,而是先想清楚:失效后能不能接受短暂不一致,回源流量能不能扛住,以及命中率下降时怎么保护数据库。
常见缓存模式。
| 模式 | 做法 | 特点 |
|---|---|---|
| Cache Aside + TTL | 读缓存未命中回源 DB,再回填缓存 | 最常见,简单但有不一致窗口 |
| 定时刷新 | 后台任务周期性刷新 | 读路径稳定,但实现复杂 |
| 写 DB 同时写缓存 | 一次请求更新两处 | 并发下顺序问题多 |
| 写 DB 后删缓存 | 工程上最常见 | 需处理删失败与并发覆盖 |
大多数业务会落在“先更新 DB,再删除缓存”这一范式上,因为直接写缓存更容易在并发场景里出现旧值覆盖新值的问题。但它也不是银弹,还要考虑:
- 删除缓存失败怎么办
- 主从延迟导致回源读到旧值怎么办
- 热点 key 失效瞬间如何避免打穿 DB
因此工程上常会叠加重试、延迟双删或 MQ 补偿,但要注意复杂度不能超过问题本身。
三类典型异常。
-
缓存穿透:大量请求访问根本不存在的数据。 常见治理是布隆过滤器、空值缓存和参数校验。
-
缓存击穿:单个热点 key 过期瞬间,大量流量回源。 常见治理是互斥锁、singleflight、热点预热和永不过期加异步刷新。
-
缓存雪崩:一批 key 同时失效,导致回源洪峰。 常见治理是 TTL 加随机抖动、多级缓存、限流降级和熔断保护。
容量与淘汰。 内存规划至少要回答三个问题:
- 目标命中率是多少
- 能接受什么淘汰策略
- 过期删除对 CPU 与内存的平衡如何取舍
Redis 常见淘汰策略包括 allkeys-lru、allkeys-lfu、volatile-lru 等。业务热点分布明显时,LFU 往往比 LRU 更稳;而“永不过期 + noeviction”如果没有容量保护,通常只是在把风险后移。
分布式锁与常见误区
Redis 可以实现分布式锁,但它适合的是“高性能互斥协调”,而不是“绝对强一致事务锁”。
最基础的正确姿势是:
- 使用
SET key value NX EX seconds原子加锁。 - value 要写唯一请求标识,解锁时校验“是不是自己加的锁”。
- 解锁要通过 Lua 脚本保证比较与删除原子执行。
只做 SETNX 而不带过期时间,是最典型的线上事故源头之一,因为进程崩溃后锁会永久残留。另一类常见误区是:
- 锁超时时间拍脑袋设置,业务执行时间稍长就提前过期。
- 误把锁当作幂等,实际上锁只能限并发,不能替代状态校验。
- 在 Cluster 下对多 key 脚本与锁语义理解不清。
对于库存扣减、支付状态流转这类关键链路,更稳妥的做法通常是“数据库约束/乐观锁 + Redis 锁或限流”组合,而不是把一致性全压在 Redis 锁上。
热点问题与线上治理
Redis 的线上治理重点,不是单纯“调大机器”,而是尽量把问题前移到模型和 key 设计阶段。
大 Key。 大 Key 典型定义可以粗略理解为:
- String 值很大,例如超过 10KB 甚至更高。
- Hash/List/Set/ZSet 元素数过多,例如上万级。
风险包括:
- 单次命令耗时长,阻塞主线程。
- 复制、持久化和迁移成本高。
- 集群迁移时容易形成长尾。
治理手段包括拆 key、拆对象、避免 HGETALL/LRANGE 0 -1 之类全量命令,以及用 redis-cli --bigkeys 做巡检。
热 Key。 热 Key 问题的本质是流量集中到单个 key 所在实例。即便集群分片足够多,只要热点集中,单点依然会被打满。治理思路通常包括:
- 本地缓存或多级缓存分担读流量。
- 热点 key 主动预热、延长 TTL。
- 在极端场景下做 key 复制或业务层读扩散。
阻塞型命令与扫描。 生产环境应避免直接使用 KEYS * 这类 O(N) 命令。全量扫描优先用 SCAN,大集合遍历要限制批次,避免在单线程模型里把维护脚本跑成线上事故。
监控维度。
| 类别 | 指标 |
|---|---|
| 延迟 | 命令耗时、慢查询日志 |
| 内存 | used_memory、碎片率、淘汰次数 |
| 键分布 | 大 Key、热 Key、过期键数量 |
| 持久化 | RDB/AOF 执行时长、失败次数 |
| 复制 | 主从延迟、断链、全量同步次数 |
本章小结
Redis 的工程价值在于用合适的数据结构快速解决高频访问问题,但它的所有优势都建立在“数据模型简洁、单次命令短小、容量边界清楚”之上。
这一章可以提炼为四个判断:
- 把 Redis 当缓存时,先想一致性与回源保护。
- 把 Redis 当数据结构服务时,先按访问模式选 String、Hash、List、Set、ZSet。
- 把 Redis 当高可用系统时,先分清持久化、主从、Sentinel、Cluster 各自解决什么问题。
- 把 Redis 当分布式协调组件时,先明确它只能提供有限互斥,不替代数据库事务语义。
本章面试题与追问
-
Redis 为什么快? 追问:单线程是不是性能瓶颈,什么时候它反而是优势?
-
String、Hash、List、Set、ZSet 分别适合哪些业务场景? 追问:商品详情缓存为什么很多场景更适合 Hash 而不是整段 JSON String?
-
Redis Hash 的紧凑编码和哈希表编码分别适合什么场景? 追问:为什么大 Hash 会成为线上风险?
-
RDB 和 AOF 怎么选? 追问:如果业务既想恢复快,又不想丢太多数据,通常怎么组合?
-
Sentinel 和 Cluster 分别解决什么问题? 追问:为什么 Cluster 解决了容量扩展,却没有自动解决热 Key 问题?
-
你会如何设计缓存和 DB 的一致性方案? 追问:为什么很多团队选择“先更新 DB,再删除缓存”,而不是直接更新缓存?
-
缓存穿透、击穿、雪崩分别是什么? 追问:如果是明星商品详情页热点 key 过期,你会优先怎么保护 DB?
-
Redis 分布式锁的正确实现方式是什么? 追问:为什么解锁必须校验 value,并且通常要用 Lua 脚本?
-
什么是大 Key 和热 Key,分别有什么危害? 追问:如果你发现某个 key 读 QPS 极高,但又不能简单加机器,你会从哪几种方向治理?
-
什么时候不应该继续用 Redis,而应该引入专业 MQ 或数据库能力? 追问:如果需要可靠消费、消息回溯和消费组,你为什么不会优先选择 Redis List?