Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 11 章 Redis:缓存原理与实践

Redis 在系统设计中的定位不只是“快一点的 KV”。它既可以做缓存,也可以承担计数、排行榜、延时任务、分布式协调等角色。要用好 Redis,关键在于先理解它为什么快、擅长什么,再明确哪些问题不该交给它解决。

Redis 的核心模型与高性能来源

Redis 的核心特征可以概括为三点:内存存储、单线程命令执行模型、面向场景设计的数据结构。绝大多数请求直接命中内存,避免了磁盘寻址;单线程把命令串行化,减少锁竞争;而 String、Hash、List、Set、ZSet 又让很多业务需求能以接近 O(1) 或 O(log n) 的方式完成。

特性价值
内存存储微秒到毫秒级访问延迟
单线程命令执行避免复杂锁竞争与上下文切换
多路复用 IO用一个线程处理大量连接
场景化数据结构减少业务层重复造轮子

“Redis 为什么快”通常可以从四个角度回答:

  1. 数据主要在内存中,避免磁盘 IO。
  2. 数据结构专门为高频操作设计。
  3. 命令执行串行化,减少锁开销。
  4. 网络层使用 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 补偿,但要注意复杂度不能超过问题本身。

三类典型异常。

  1. 缓存穿透:大量请求访问根本不存在的数据。 常见治理是布隆过滤器、空值缓存和参数校验。

  2. 缓存击穿:单个热点 key 过期瞬间,大量流量回源。 常见治理是互斥锁、singleflight、热点预热和永不过期加异步刷新。

  3. 缓存雪崩:一批 key 同时失效,导致回源洪峰。 常见治理是 TTL 加随机抖动、多级缓存、限流降级和熔断保护。

容量与淘汰。 内存规划至少要回答三个问题:

  • 目标命中率是多少
  • 能接受什么淘汰策略
  • 过期删除对 CPU 与内存的平衡如何取舍

Redis 常见淘汰策略包括 allkeys-lruallkeys-lfuvolatile-lru 等。业务热点分布明显时,LFU 往往比 LRU 更稳;而“永不过期 + noeviction”如果没有容量保护,通常只是在把风险后移。

分布式锁与常见误区

Redis 可以实现分布式锁,但它适合的是“高性能互斥协调”,而不是“绝对强一致事务锁”。

最基础的正确姿势是:

  1. 使用 SET key value NX EX seconds 原子加锁。
  2. value 要写唯一请求标识,解锁时校验“是不是自己加的锁”。
  3. 解锁要通过 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 当分布式协调组件时,先明确它只能提供有限互斥,不替代数据库事务语义。

本章面试题与追问

  1. Redis 为什么快? 追问:单线程是不是性能瓶颈,什么时候它反而是优势?

  2. String、Hash、List、Set、ZSet 分别适合哪些业务场景? 追问:商品详情缓存为什么很多场景更适合 Hash 而不是整段 JSON String?

  3. Redis Hash 的紧凑编码和哈希表编码分别适合什么场景? 追问:为什么大 Hash 会成为线上风险?

  4. RDB 和 AOF 怎么选? 追问:如果业务既想恢复快,又不想丢太多数据,通常怎么组合?

  5. Sentinel 和 Cluster 分别解决什么问题? 追问:为什么 Cluster 解决了容量扩展,却没有自动解决热 Key 问题?

  6. 你会如何设计缓存和 DB 的一致性方案? 追问:为什么很多团队选择“先更新 DB,再删除缓存”,而不是直接更新缓存?

  7. 缓存穿透、击穿、雪崩分别是什么? 追问:如果是明星商品详情页热点 key 过期,你会优先怎么保护 DB?

  8. Redis 分布式锁的正确实现方式是什么? 追问:为什么解锁必须校验 value,并且通常要用 Lua 脚本?

  9. 什么是大 Key 和热 Key,分别有什么危害? 追问:如果你发现某个 key 读 QPS 极高,但又不能简单加机器,你会从哪几种方向治理?

  10. 什么时候不应该继续用 Redis,而应该引入专业 MQ 或数据库能力? 追问:如果需要可靠消费、消息回溯和消费组,你为什么不会优先选择 Redis List?