2. 缓存基础回顾:本地缓存 vs 分布式缓存,缓存穿透、缓存击穿、缓存雪崩的定义与经典解法

2.1 本地缓存与分布式缓存:架构选型对比

在服务边界视角下,缓存首先面临一个基础决策:数据应该缓存在应用进程内(本地),还是缓存在独立的外部服务中(分布式)。两种模式在延迟、一致性、容量和运维复杂度上存在本质差异。

对比维度 本地缓存(Local Cache) 分布式缓存(Distributed Cache)
数据存储位置 应用进程堆内 / 堆外内存 独立缓存集群(如 Redis、Memcached)
访问延迟 纳秒级(无网络开销) 微秒~毫秒级(网络 RTT 主导)
数据一致性 各节点独立,天然不一致 可通过主从 / 集群协议保证最终一致性
容量上限 受限于单机堆内存(通常 GB 级) 可水平扩展至 TB 级
典型实现 Guava Cache、Caffeine、Ehcache Redis Cluster、Redis Sentinel、Memcached
适用场景 配置信息、静态字典、热点 Key 本地副本 会话状态、全局计数器、跨服务共享数据
架构决策提示: 在微服务架构中,本地缓存适合存放“写少读多、变更可容忍短暂不一致”的元数据;分布式缓存则适合需要全局一致视图或跨服务共享的热点数据。两者常组合为“本地缓存兜底 + 分布式缓存主存”的多级缓存模式。

2.2 缓存穿透、击穿、雪崩:定义与经典解法

这三个问题是缓存层最常见的故障模式,理解其本质差异是设计高可用缓存系统的前提。

2.2.1 缓存穿透(Cache Penetration)

定义: 请求查询一个根本不存在的数据(既不在缓存,也不在数据库),导致每次请求都穿透缓存直达数据库,造成数据库压力陡增。

经典解法:

  • 空值缓存: 将查询结果为 null 的 Key 也写入缓存,设置较短的 TTL(如 30~60 秒),避免重复穿透。
  • 布隆过滤器(Bloom Filter): 在缓存前加一层布隆过滤器,快速判断 Key 是否可能存在。若过滤器判定不存在,直接拒绝请求。
  • 参数校验: 在入口层对请求参数进行合法性校验,拦截明显无效的 Key(如 id <= 0)。

2.2.2 缓存击穿(Cache Breakdown)

定义: 一个热点 Key 在缓存中过期失效的瞬间,大量并发请求同时涌入,全部穿透到数据库,导致数据库负载瞬间飙升。

经典解法:

  • 互斥锁(Mutex Key): 当缓存失效时,只允许一个线程去数据库加载数据,其他线程等待或重试。常用 Redis SETNX 或本地锁实现。
  • 逻辑过期(预加载): 缓存中不设置物理 TTL,而是存储一个逻辑过期时间字段。后台异步线程定期刷新热点数据,保证缓存始终有效。
  • 永不过期 + 主动更新: 对极端热点 Key 设置永不过期,通过消息队列或定时任务主动更新缓存内容。

2.2.3 缓存雪崩(Cache Avalanche)

定义: 大量缓存 Key 在同一时间集中过期失效,或缓存节点整体宕机,导致海量请求直接压向数据库,引发连锁故障。

经典解法:

  • 过期时间打散(TTL 抖动): 在基础 TTL 上增加随机偏移量(如 ±10%~30%),避免大量 Key 同时过期。
  • 缓存高可用: 采用 Redis Sentinel 或 Cluster 模式,避免单点故障;配合多级缓存(本地 + 分布式)降低穿透比例。
  • 限流 & 降级: 在数据库访问层配置限流(如 Sentinel、Hystrix),当缓存失效时,部分请求直接返回降级结果或错误提示。
  • 提前预热: 在流量高峰期前,手动或自动将热点数据提前加载到缓存中。
记忆口诀: 穿透查“无”,击穿打“热”,雪崩倒“片”。—— 穿透针对不存在的数据,击穿针对单个热点 Key 过期,雪崩针对大量 Key 同时过期或节点宕机。

2.3 三种故障模式的对比与应对策略总览

故障模式 触发条件 影响范围 核心应对策略
缓存穿透 查询不存在的数据 单个 Key 持续穿透 空值缓存、布隆过滤器、参数校验
缓存击穿 热点 Key 过期瞬间 单个热点 Key 高并发穿透 互斥锁、逻辑过期、永不过期 + 主动更新
缓存雪崩 大量 Key 同时过期 / 节点宕机 大面积缓存失效 TTL 打散、缓存高可用、限流降级、预热

2.4 缓存故障模式与应对策略流程图

缓存故障模式与应对策略流程图 客户端请求 缓存中是否存在 Key? 返回缓存数据 故障模式判断 缓存穿透 数据不存在 缓存击穿 热点 Key 过期 缓存雪崩 大量 Key 同时过期 应对策略 空值缓存 / 布隆过滤器 参数校验 应对策略 互斥锁 / 逻辑过期 永不过期 + 主动更新 应对策略 TTL 打散 / 缓存高可用 限流降级 / 预热

2.5 代码示例:互斥锁防止缓存击穿

以下代码展示了使用 Redis SETNX 实现互斥锁,防止热点 Key 过期时大量请求穿透到数据库的典型模式。

public String getWithMutex(String key) {
    // 1. 尝试从缓存获取
    String value = redis.get(key);
    if (value != null) {
        return value;
    }

    // 2. 缓存未命中,尝试获取分布式锁
    String lockKey = "lock:" + key;
    boolean locked = redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS);
    if (locked) {
        try {
            // 3. 双重检查:可能其他线程已更新缓存
            value = redis.get(key);
            if (value != null) {
                return value;
            }
            // 4. 从数据库加载
            value = db.query(key);
            redis.setex(key, 300, value);
            return value;
        } finally {
            redis.del(lockKey);
        }
    } else {
        // 5. 未获取到锁,等待后重试
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return getWithMutex(key); // 递归重试
    }
}
注意事项: 互斥锁方案会引入额外的锁等待时间,适用于并发量极高但数据库负载敏感的场景。对于可以容忍短暂不一致的业务,逻辑过期方案通常具有更好的吞吐性能。