2. 缓存基础与常见模式:穿透、击穿、雪崩与三大读写模式

缓存是后端架构中提升读性能、降低数据库压力的核心手段,但若使用不当,会引入数据不一致、缓存失效风暴等风险。本章从两类问题出发:一是缓存失效引发的三类经典故障(穿透、击穿、雪崩),二是缓存与数据库之间的三种主流同步模式(旁路缓存、读写穿透、异步刷写)。理解这些基础,是构建安全合规缓存体系的前提。

2.1 缓存穿透、击穿、雪崩:定义与区别

这三类问题均与缓存未命中或缓存失效有关,但触发场景、影响范围和应对策略截然不同。下表从核心定义、典型场景、影响范围三个维度进行对比。

故障类型 核心定义 典型场景 影响范围
缓存穿透 请求查询一个缓存和数据库中都不存在的数据,导致每次请求都穿透缓存直达数据库。 恶意攻击者构造大量不存在的 ID(如 -1、999999)发起查询。 数据库连接数飙升,严重时导致数据库宕机。
缓存击穿 一个热点 key 在缓存失效的瞬间,大量并发请求同时回源查询数据库。 秒杀商品详情页的 key 刚好过期,数千请求同时打到数据库。 单点数据库压力激增,可能引发该热点数据相关的服务雪崩。
缓存雪崩 大量 key 在同一时间或短时间内集中过期,导致大量请求同时回源数据库。 缓存服务重启、大量 key 设置了相同的过期时间(如统一凌晨 0 点过期)。 数据库瞬间被海量请求打满,整体服务不可用。
合规视角提示: 缓存穿透常被用于数据爬取或撞库攻击,属于安全合规审计中的“异常数据访问模式”。建议在网关层或缓存层对非法参数(如负数 ID、超长字符串)进行前置拦截,并记录审计日志。

2.1.1 三类故障的应对策略对比

  • 缓存穿透:
    • 缓存空对象:将数据库查询为 null 的结果也缓存(设置较短的 TTL,如 30 秒)。
    • 布隆过滤器:在缓存前置一个布隆过滤器,快速判断 key 是否可能存在。
    • 参数校验:接口层对请求参数进行合法性校验,拒绝明显非法的 key。
  • 缓存击穿:
    • 互斥锁(Mutex):回源查询数据库时加锁,只允许一个线程去加载数据,其他线程等待。
    • 逻辑过期:不设置物理 TTL,而是使用逻辑过期时间,由后台线程异步刷新。
    • 热点 key 永不过期:结合后台定时更新策略。
  • 缓存雪崩:
    • 过期时间打散:在基础 TTL 上增加随机偏移量(如 ±10%)。
    • 多级缓存:本地缓存(如 Caffeine)+ 分布式缓存(如 Redis)分层防护。
    • 缓存高可用:使用 Redis 哨兵或集群模式,避免缓存服务单点故障。
关键区别记忆点: 穿透是“查不到”,击穿是“一个热点扛不住”,雪崩是“大量热点一起倒”。
缓存穿透、击穿、雪崩故障示意图 缓存三类故障对比示意图 缓存穿透 请求不存在的数据 缓存 & DB 均无 每次穿透到数据库 缓存击穿 热点 key 过期瞬间 大量并发回源 单点数据库压力激增 缓存雪崩 大量 key 集中过期 或缓存宕机 数据库整体被打满 应对策略总结 • 穿透:缓存空对象 + 布隆过滤器 + 参数校验 • 击穿:互斥锁 + 逻辑过期 + 热点 key 永不过期 • 雪崩:过期时间打散 + 多级缓存 + 缓存高可用 • 合规要求:所有回源操作必须记录审计日志

2.2 缓存与数据库一致性三大模式

缓存与数据库之间的数据同步模式决定了系统的最终一致性、实时性和实现复杂度。业界主流有三种模式:旁路缓存(Cache-Aside)、读写穿透(Read/Write-Through)和异步刷写(Write-Behind)。

2.2.1 模式对比总览

模式 读流程 写流程 一致性 复杂度 典型应用
旁路缓存 先读缓存,miss 则回源 DB 并回填缓存 先更新 DB,再删除/更新缓存 最终一致(有窗口期) 低 Redis + MySQL 手动管理
读写穿透 缓存层代理读请求,自动回源 缓存层代理写请求,同步写 DB 和缓存 强一致(同步写) 中 Redis Enterprise、Oracle Coherence
异步刷写 读缓存(可能脏读) 只写缓存,异步批量刷入 DB 弱一致(有丢失风险) 高 写密集型日志、计数器

2.2.2 旁路缓存(Cache-Aside)

这是最常用的模式,应用程序直接管理缓存和数据库的读写。读时先查缓存,未命中则查 DB 并回填;写时先更新 DB,再删除缓存(或更新缓存)。

常见陷阱: 写操作如果先删除缓存再更新 DB,会导致并发读请求在 DB 更新前回填旧数据,造成数据不一致。推荐采用“先更新 DB,后删除缓存”的策略,并配合延迟双删或消息队列补偿。
// 旁路缓存读流程伪代码
public User getUser(String userId) {
    // 1. 先查缓存
    User user = cache.get("user:" + userId);
    if (user != null) {
        return user;
    }
    // 2. 缓存 miss,查数据库
    user = db.query("SELECT * FROM user WHERE id = ?", userId);
    if (user != null) {
        // 3. 回填缓存,设置过期时间
        cache.set("user:" + userId, user, 300);
    }
    return user;
}

// 旁路缓存写流程伪代码
public void updateUser(User user) {
    // 1. 先更新数据库
    db.execute("UPDATE user SET name = ? WHERE id = ?", user.getName(), user.getId());
    // 2. 再删除缓存(或更新缓存)
    cache.del("user:" + user.getId());
}

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

缓存层作为数据访问的唯一入口,应用程序只与缓存交互,缓存组件内部负责与数据库同步。读穿透由缓存组件在 miss 时自动回源;写穿透由缓存组件同步写入数据库和缓存。

  • 优点: 应用程序逻辑简化,一致性由缓存层保证。
  • 缺点: 缓存组件复杂度高,写操作延迟增加(需等待 DB 写入完成)。
  • 适用场景: 对一致性要求高、写并发不极端的业务,如用户账户信息。

2.2.4 异步刷写(Write-Behind)

写操作只更新缓存,立即返回成功,然后由后台线程异步批量写入数据库。读操作直接读取缓存,可能读到尚未持久化的数据。

  • 优点: 写吞吐量极高,适合日志、计数、埋点等写密集型场景。
  • 风险: 缓存宕机可能导致数据丢失;异步刷写失败需要重试或补偿机制。
  • 合规要求: 涉及资金、订单等关键数据时,必须开启 WAL(Write-Ahead Log)或使用可靠消息队列保证最终一致性。
三种缓存读写模式架构对比 三种缓存读写模式架构对比 旁路缓存 (Cache-Aside) App 直接管理缓存和 DB App Cache DB 读写穿透 (Read/Write-Through) 缓存层代理所有数据访问 App Cache DB 异步刷写 (Write-Behind) 只写缓存,异步批量刷 DB App Cache DB 异步 模式选择建议 • 旁路缓存:通用首选,适合大多数业务,需自行处理一致性问题。 • 读写穿透:适合强一致场景,但写延迟较高,需缓存组件支持。 • 异步刷写:适合写吞吐量极高、可接受短时数据不一致的场景,需做好数据补偿。

2.3 模式选择与合规考量

在实际架构中,选择哪种模式需要结合业务对一致性、性能和合规的要求:

  • 金融/交易类: 优先使用读写穿透或旁路缓存 + 强一致性补偿(如分布式锁、版本号),确保缓存与数据库的最终一致性可审计。
  • 内容/社交类: 旁路缓存即可满足需求,配合缓存穿透防护和过期时间打散。
  • 日志/监控类: 异步刷写模式可大幅提升写入性能,但需配置 WAL 或消息队列防止数据丢失。
合规红线: 无论采用哪种模式,所有缓存回源操作、写操作、异步刷写失败记录都必须纳入审计日志。对于涉及个人隐私或资金的数据,缓存层不得存储明文敏感信息,必须脱敏或加密。