4. API缓存策略:HTTP缓存头、本地缓存与分布式缓存

缓存是API性能调优中最立竿见影的手段,但也是最容易引发数据不一致和雪崩风险的环节。本章从HTTP协议缓存、应用层本地缓存、分布式缓存三个维度展开,并重点剖析缓存穿透、击穿、雪崩三大经典问题的成因与解决方案。

4.1 HTTP缓存头:让浏览器和网关替你扛流量

HTTP缓存是距离用户最近的缓存层,合理配置Cache-Control和ETag可以大幅减少后端请求量。

指令取值示例作用
max-agemax-age=3600资源在缓存中可保存的最大秒数
no-cacheno-cache每次使用前必须向源服务器验证
no-storeno-store禁止任何缓存(敏感数据)
publicpublic允许中间代理缓存
privateprivate仅允许浏览器缓存
ETag"abc123"资源版本标识,配合If-None-Match做条件请求
最佳实践:静态资源(图片、CSS、JS)设置public, max-age=31536000, immutable;API响应中对于变化不频繁的数据(如配置、字典)设置private, max-age=60,并配合ETag实现零带宽验证。
// 服务端设置示例(Spring Boot)
@GetMapping("/config")
public ResponseEntity<Config> getConfig() {
    Config config = configService.get();
    return ResponseEntity.ok()
        .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePrivate())
        .eTag(config.getVersion())
        .body(config);
}

4.2 本地缓存 vs 分布式缓存:选型与协同

本地缓存(如Caffeine、Guava Cache)和分布式缓存(如Redis)各有适用场景,实践中常组合使用形成多级缓存。

维度本地缓存分布式缓存(Redis)
访问延迟纳秒级(堆内)毫秒级(网络IO)
容量受限于JVM堆内存可水平扩展至TB级
数据一致性各节点独立,易出现不一致集中存储,强一致(单节点)
典型场景热点数据、字典、配置会话、计数器、排行榜、分布式锁
淘汰策略LRU/LFU/定时过期LRU/LFU/TTL/惰性删除
多级缓存策略:优先查询本地缓存(L1),未命中则查询Redis(L2),仍未命中再回源到数据库。写入时采用“先更新数据库,再删除缓存”的模式,配合消息队列异步补偿,保证最终一致性。
多级缓存查询流程 客户端请求 L1 本地缓存(Caffeine) 命中 → 直接返回 L2 分布式缓存(Redis) 命中 → 回填L1并返回 数据库(回源) 回填L2和L1,返回结果 缓存回填策略 先更新DB → 删除缓存 异步补偿保证最终一致

4.3 缓存穿透、击穿、雪崩:成因与解决方案

这三个问题是缓存系统最典型的故障模式,理解其区别是制定治理策略的前提。

问题现象根因核心解决方案
缓存穿透请求大量不存在的数据,直接打到DB恶意攻击或查询不存在key布隆过滤器 + 空值缓存
缓存击穿热点key过期瞬间,高并发请求击穿到DB热点key过期 + 并发互斥锁 + 逻辑过期
缓存雪崩大量key同时过期或Redis宕机,DB被打垮批量过期 / 缓存节点故障过期时间打散 + 多级缓存 + 限流降级

4.3.1 缓存穿透:布隆过滤器与空值缓存

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它允许一定的误判率(假阳性),但绝不漏判(假阴性率为0)。

// 布隆过滤器使用示例(Guava)
BloomFilter<String> bloomFilter = BloomFilter.create(
    Funnels.stringFunnel(Charset.defaultCharset()),
    100_000,   // 预期插入数量
    0.01       // 期望误判率
);
// 预热:将所有合法ID插入布隆过滤器
for (Long id : validIds) {
    bloomFilter.put(id.toString());
}

// 查询时先检查布隆过滤器
public Object getData(String key) {
    if (!bloomFilter.mightContain(key)) {
        return null; // 直接返回,避免穿透
    }
    // 继续查询缓存和数据库
}
注意:布隆过滤器无法删除元素。如果业务需要频繁删除ID,建议使用布隆过滤器的变种(如Counting Bloom Filter)或换用Redis的BitMap自行实现。空值缓存策略中,对不存在的key缓存一个特殊标记(如NULL),TTL设置较短(30~60秒),防止恶意攻击持续穿透。

4.3.2 缓存击穿:互斥锁与逻辑过期

热点key过期瞬间,大量并发请求同时回源到数据库。解决方案有两种主流思路:

  • 互斥锁(Mutex Lock):只允许一个线程去加载数据,其他线程等待。适用于对一致性要求高的场景。
  • 逻辑过期(Logical Expiration):缓存永不过期,但数据中携带过期时间字段。异步线程发现过期后主动更新。适用于允许短暂不一致的场景。
// 互斥锁方案(Redis SETNX实现)
public String getWithLock(String key) {
    String value = redis.get(key);
    if (value != null) {
        return value;
    }
    // 尝试获取分布式锁
    String lockKey = "lock:" + key;
    if (redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS)) {
        try {
            // 双重检查:可能其他线程已经加载完成
            value = redis.get(key);
            if (value != null) return value;
            // 回源数据库
            value = db.query(key);
            redis.set(key, value, 300, TimeUnit.SECONDS);
            return value;
        } finally {
            redis.del(lockKey);
        }
    } else {
        // 等待并重试
        Thread.sleep(50);
        return getWithLock(key); // 递归重试
    }
}

4.3.3 缓存雪崩:过期时间打散与多级防护

缓存雪崩的破坏力最大,需要从多个层面联合治理:

  1. 过期时间打散:在基础TTL上增加随机偏移量(如±10%~30%),避免大量key同时过期。
  2. 多级缓存:本地缓存(L1)作为Redis(L2)的降级方案,即使Redis宕机,L1仍能扛住部分流量。
  3. 限流降级:对回源数据库的请求做限流(如Sentinel、Hystrix),超出阈值直接返回降级数据或错误提示。
  4. Redis高可用:部署主从+哨兵或Redis Cluster,避免单点故障。
缓存雪崩防护体系 客户端请求 限流层(Sentinel) L1 本地缓存 L2 Redis集群 数据库(降级保护) 过期打散 主从切换

4.4 缓存治理最佳实践清单

  • 预热:系统启动时主动加载热点数据到缓存,避免冷启动穿透。
  • 监控:监控缓存命中率、回源QPS、Redis内存使用率、大key和热key。
  • 大key治理:拆分大value(如超过10KB的字符串或超过5000个元素的集合),避免阻塞Redis单线程。
  • 热key发现:使用Redis的hotkeys命令或客户端侧统计,对热key做本地缓存分摊。
  • 缓存与DB一致性:优先采用“先更新DB,后删除缓存”策略,配合延迟双删或Binlog监听补偿。
总结:缓存策略不是“一刀切”的配置,而是根据数据特征(热点分布、更新频率、一致性要求)动态组合HTTP缓存、本地缓存、分布式缓存三层体系。穿透、击穿、雪崩的治理核心在于“分层防御、限流兜底、异步补偿”。