3. 缓存中间件基础:Redis核心数据结构、缓存穿透/击穿/雪崩、缓存一致性策略

3.1 Redis 核心数据结构与选型

Redis 提供了多种高性能数据结构,每种结构对应不同的业务场景。选型错误会导致内存浪费或性能下降。下表总结了五种最常用的数据结构及其典型应用:

数据结构 底层编码 时间复杂度 典型场景 避坑要点
String int / embstr / raw O(1) 计数器、分布式锁、Session 共享 大 Value 会导致阻塞;单个 Key 不超过 512MB
Hash ziplist / hashtable O(1) 平均 对象缓存(用户信息、商品详情) field 过多时 ziplist 转为 hashtable,内存开销上升
List quicklist O(1) 头尾操作 消息队列、时间线、最新动态 避免 LINDEX 随机访问长列表;改用 Stream
Set intset / hashtable O(1) 增删查 标签系统、好友关系、抽奖去重 大数据量交集运算消耗 CPU
Sorted Set ziplist / skiplist O(log N) 范围查询 排行榜、延迟队列、限流滑动窗口 score 精度问题;大 Key 迁移耗时
选型原则:优先使用 Hash 代替 String 存储对象,减少 Key 数量;需要排序时使用 Sorted Set;需要消息队列时优先考虑 Redis Stream(支持消费者组和 ACK)。

3.2 缓存穿透、击穿与雪崩

这三个问题是缓存层最常见的故障模式,理解其成因和解决方案是架构师的基本功。

3.2.1 缓存穿透

定义:查询一个数据库中也不存在的数据,每次请求都穿过缓存直达数据库。

  • 危害:数据库压力剧增,可能引发连接池耗尽。
  • 解决方案:
    • 缓存空对象(Null Value):将 null 也缓存,设置较短 TTL(如 30s)。
    • 布隆过滤器(Bloom Filter):在缓存前加一层位图过滤器,拦截不存在 Key。

3.2.2 缓存击穿

定义:一个热点 Key 在缓存失效的瞬间,大量并发请求同时穿透到数据库。

  • 危害:数据库单点压力飙升,可能导致雪崩。
  • 解决方案:
    • 互斥锁(Mutex Key):只允许一个线程重建缓存,其他线程等待或回退。
    • 逻辑过期:不设置 TTL,而是存储逻辑过期时间,异步刷新。

3.2.3 缓存雪崩

定义:大量 Key 在同一时间失效,或缓存节点宕机,导致请求全部落到数据库。

  • 危害:数据库瞬间被打垮,系统整体不可用。
  • 解决方案:
    • TTL 随机化:在基础过期时间上增加随机偏移(如 ±30%)。
    • 多级缓存:本地缓存(Caffeine) + 分布式缓存(Redis)分层保护。
    • 缓存高可用:Redis Sentinel / Cluster 保证节点故障自动切换。
架构警示:缓存穿透、击穿、雪崩往往同时发生。生产环境必须同时部署布隆过滤器、互斥锁、TTL 随机化三种机制,并配合限流和熔断。
缓存穿透、击穿、雪崩对比流程图 缓存穿透 / 击穿 / 雪崩 对比流程 缓存穿透 请求 → 缓存 Miss → 数据库查询(无数据) → 返回空,不缓存 方案:布隆过滤器 + 缓存空对象 缓存击穿 热点 Key 失效瞬间 → 大量并发穿透 → 数据库单点压力 方案:互斥锁 + 逻辑过期 缓存雪崩 大量 Key 同时失效 或节点宕机 → 数据库被打垮 方案:TTL 随机化 + 多级缓存 三者可叠加发生:穿透 + 击穿 → 雪崩,必须分层防御

3.3 缓存一致性策略

缓存与数据库之间的数据一致性是分布式系统中最棘手的挑战之一。没有完美的方案,只有根据业务场景选择最合适的策略。

3.3.1 常见更新策略对比

策略 操作顺序 一致性级别 并发风险 推荐场景
Cache Aside 读 Miss → 查 DB → 写缓存
写 → 更新 DB → 删除缓存
最终一致 删除缓存后,旧数据可能被并发读写入 读多写少,通用场景
Read Through 读 Miss → 缓存组件自动加载 DB 最终一致 缓存组件内部需处理并发加载 需要统一缓存加载逻辑
Write Through 写 → 缓存先更新 → 缓存同步写 DB 强一致(同步) 写延迟增加,缓存故障影响写入 对一致性要求极高,可接受写性能损失
Write Behind 写 → 缓存更新 → 异步批量写 DB 弱一致 缓存宕机导致数据丢失 写吞吐量极高,可容忍短暂不一致

3.3.2 延迟双删与最终一致性

在 Cache Aside 模式中,延迟双删是解决并发写导致脏数据的常用手段:

  1. 先删除缓存。
  2. 更新数据库。
  3. 休眠一小段时间(如 500ms),再次删除缓存。

第二次删除的目的是清除在第一步和第二步之间可能被并发读线程写入的旧缓存数据。

工程实践:延迟双删的休眠时间应大于“读数据库 + 写缓存”的平均耗时。可以使用异步消息队列触发第二次删除,避免阻塞主线程。

3.3.3 基于 Binlog 的缓存同步

对于无法接受延迟双删带来的短暂不一致的场景,可以采用 监听数据库 Binlog 的方式:

  • 使用 Canal / Maxwell 等工具订阅 MySQL Binlog。
  • 解析变更事件(INSERT / UPDATE / DELETE)。
  • 通过消息队列异步更新或删除缓存。

这种方式实现了业务代码与缓存操作解耦,且不会因为缓存操作失败而影响主流程。

基于 Binlog 的缓存一致性架构 基于 Binlog 的缓存一致性架构 业务应用 写入数据库 MySQL 生成 Binlog Canal / Maxwell 解析 Binlog 消息队列 RocketMQ / Kafka Redis 缓存 删除 / 更新 优点:业务无侵入,不依赖 TTL,最终一致性高;缺点:引入 Canal 和 MQ 增加运维复杂度

3.4 本章小结

  • Redis 数据结构选型直接影响性能和内存效率,务必根据访问模式选择。
  • 缓存穿透、击穿、雪崩是三种不同但可叠加的故障模式,需要分层防御。
  • 缓存一致性没有银弹:Cache Aside + 延迟双删适合大多数场景;Binlog 同步适合对一致性要求高且愿意接受额外基础设施的场景。
  • 生产环境必须配合监控(Redis 慢查询、缓存命中率、数据库 QPS)来持续优化策略。