3. 缓存基础与常见模式:缓存穿透、缓存击穿、缓存雪崩、旁路缓存、读写穿透

在领域驱动设计中,缓存是保障聚合(Aggregate)查询性能与数据库一致性的关键战术工具。本章聚焦缓存的基础故障模式与标准读写模式,为后续定义“一致性边界”奠定技术基础。

3.1 三大缓存故障模式

缓存穿透、缓存击穿、缓存雪崩是分布式缓存最常见的三类故障,直接影响数据库负载与系统可用性。下表从触发原因、影响范围、典型场景三个维度进行对比:

故障模式 触发原因 影响范围 典型场景
缓存穿透 查询一个一定不存在的数据(如无效ID) 单个请求穿透至数据库,但可被恶意利用放大 攻击者遍历不存在的用户ID
缓存击穿 热点 key 在缓存过期瞬间,大量并发请求同时穿透 单个热点 key 失效导致数据库瞬时高负载 秒杀商品详情页的缓存失效
缓存雪崩 大量 key 在同一时间批量过期,或缓存节点宕机 全局性数据库压力激增,可能导致级联故障 缓存服务重启后大量 key 同时过期

3.1.1 缓存穿透

当请求查询一个缓存和数据库中都不存在的数据时,每次请求都会绕过缓存直达数据库。若攻击者利用此漏洞大量请求无效 key,数据库可能直接被压垮。

解决方案:
  • 缓存空值:将 null 结果也缓存,设置较短 TTL(如 30~60 秒)。
  • 布隆过滤器:在缓存前置一层 Bloom Filter,快速判断 key 是否可能存在。
  • 参数校验:在应用层拦截明显不合法的请求(如 ID ≤ 0)。

3.1.2 缓存击穿

与穿透不同,击穿针对的是热点 key。当该 key 恰好过期时,大量并发请求同时发现缓存缺失,全部涌向数据库重建缓存。

关键指标:热点 key 的 QPS 通常占整体请求的 30% 以上,失效瞬间数据库连接数可能飙升 10 倍。

常用解决方案包括:

  • 互斥锁(Mutex):只允许一个线程去数据库加载数据,其他线程等待或降级。
  • 逻辑过期:缓存永不过期,但数据内嵌过期时间字段,异步线程负责刷新。
  • 热点 key 预热:在流量高峰前主动加载热点数据到缓存。

3.1.3 缓存雪崩

雪崩是击穿的放大版。当大量 key 在同一时刻过期,或缓存节点整体宕机时,数据库会收到远超其处理能力的请求。

避坑提示:不要将所有 key 的 TTL 设置为相同的固定值。即使业务上要求统一过期,也应加入随机偏移量(如基础 TTL + 随机 0~300 秒)。

核心防御策略:

  • TTL 随机化:在基础过期时间上增加 ±20% 的随机偏移。
  • 缓存高可用:使用 Redis Sentinel 或 Cluster 避免单点故障。
  • 限流降级:在数据库访问层前置熔断器(如 Hystrix、Sentinel)。
  • 本地缓存兜底:在应用 JVM 内缓存热点数据,作为分布式缓存失效的第二道防线。
三大缓存故障模式对比 缓存穿透 查询不存在的数据 每次穿透到数据库 方案:空值缓存 / 布隆过滤器 缓存击穿 热点 key 过期瞬间 大量并发穿透 方案:互斥锁 / 逻辑过期 缓存雪崩 大量 key 同时过期 或缓存节点宕机 方案:TTL 随机化 / 高可用 数据库(Database) 单次穿透 单 key 并发 全局洪峰

3.2 缓存读写标准模式

在领域驱动设计的应用层与基础设施层之间,缓存模式决定了数据流的走向与一致性保证强度。以下两种模式是业界最通用的实践。

3.2.1 旁路缓存(Cache-Aside)

旁路缓存是应用最广泛的模式,应用代码同时管理缓存和数据库的读写。其核心流程如下:

  1. 读请求:先查缓存,命中则直接返回;未命中则查数据库,将结果写入缓存后返回。
  2. 写请求:先更新数据库,然后删除缓存(而非更新缓存)。
为什么写操作要删除缓存而不是更新? 因为并发环境下,更新缓存可能引入“脏数据”。删除缓存让下一次读请求自然重建,能有效避免写写冲突与读写冲突。

旁路缓存的优缺点:

  • 优点:实现简单,缓存与数据库解耦,适合读多写少的场景。
  • 缺点:存在“删除缓存后、重建缓存前”的短暂不一致窗口;应用层需处理缓存删除失败的重试逻辑。

3.2.2 读写穿透(Read/Write-Through)

读写穿透模式将缓存抽象为数据存储的主访问层,应用只与缓存交互,由缓存组件负责与数据库同步。

  • 读穿透:缓存未命中时,缓存组件自动从数据库加载数据并返回。
  • 写穿透:应用写入缓存,缓存组件同步或异步将数据写入数据库。
适用场景:当领域层希望屏蔽基础设施细节,或使用 Redis 企业版、GemFire 等支持 Write-Through 的缓存中间件时,读写穿透能简化应用代码。

两种模式的核心差异对比:

对比维度 旁路缓存(Cache-Aside) 读写穿透(Read/Write-Through)
控制权 应用代码显式管理缓存 缓存组件代理数据库操作
一致性窗口 存在短暂不一致(删除缓存后) 同步模式下一致性更强
实现复杂度 低,只需缓存客户端 中高,需要缓存中间件支持
领域层侵入 基础设施层需感知缓存逻辑 领域层完全无感知
典型选型 Redis + 应用层代码 Redis 企业版 / Hazelcast
旁路缓存 vs 读写穿透流程对比 旁路缓存(Cache-Aside) 应用(Application) 缓存(Cache) 数据库(DB) 读/写 未命中时回源 回填 读写穿透(Read/Write-Through) 应用(Application) 缓存代理(Cache Proxy) 数据库(DB) 只与代理交互 代理自动同步 关键区别:旁路缓存由应用显式管理缓存与数据库的同步;读写穿透将同步逻辑封装在缓存代理层,领域层无感知。 在 DDD 中,旁路缓存适合 Repository 实现层,读写穿透适合 CQRS 的写模型与读模型分离场景。

3.3 模式选型与一致性边界初探

在领域驱动设计中,缓存模式的选择直接影响聚合一致性边界的定义:

  • 旁路缓存适合最终一致性要求较高的查询场景(如商品列表、用户资料),领域层通过 Repository 接口控制缓存失效时机。
  • 读写穿透适合强一致性或读写对称的场景(如库存扣减、账户余额),但需要缓存中间件支持事务性写入。
  • 故障模式防御(穿透/击穿/雪崩)应作为基础设施层的横切关注点,通过 AOP 或中间件配置统一治理,不侵入领域逻辑。
实践建议:在 DDD 分层架构中,将缓存故障防御逻辑封装在基础设施层的 CacheRepository 装饰器内,领域层只依赖 Repository 接口,不感知缓存策略。这样当缓存模式切换时,领域模型无需改动。