4. 缓存策略与缓存击穿:本地缓存 vs 分布式缓存
缓存这东西,说白了就是用空间换时间。但换不好,系统反而会崩得更快。我见过太多团队,上来就怼 Redis,结果热点 Key 一挂,数据库直接被打死。今天咱们就把缓存这摊事彻底捋清楚。
4.1 本地缓存 vs 分布式缓存:选型不是拍脑袋
先问自己一个问题:你的数据需要共享吗?如果答案是「不需要」,那本地缓存往往更香。
| 维度 | 本地缓存(Caffeine) | 分布式缓存(Redis) |
|---|---|---|
| 访问延迟 | 纳秒级(堆内访问) | 毫秒级(网络 IO) |
| 数据一致性 | 单机强一致 | 最终一致(需额外处理) |
| 容量上限 | 受限于堆内存 | 可水平扩展 |
| 适用场景 | 配置、字典、不共享的热点数据 | 全局共享、跨服务数据 |
| 典型实现 | Caffeine、Guava Cache | Redis、Memcached |
我个人习惯是:能用本地缓存解决的,绝不引入 Redis。为什么?少一次网络开销,少一个故障点。但要注意,本地缓存最大的坑是「数据孤岛」——每个节点各存各的,更新时容易不一致。
4.2 缓存穿透:别让请求打到数据库
缓存穿透,就是查一个根本不存在的数据。每次请求都绕过缓存,直接怼到数据库。我见过最夸张的一次,某个接口被刷了 10 万次,数据库连接池瞬间打满。
解决方案就三个:
- 缓存空值:查不到数据时,也缓存一个空对象(TTL 设短一点,比如 30 秒)。
- 布隆过滤器:请求进来先过布隆,不存在直接返回。注意布隆有误判率,但可以接受。
- 参数校验:非法参数直接拦截,别让它们进缓存层。
避坑指南: 我曾经用布隆过滤器时,忘了考虑布隆不支持删除。结果数据删了,布隆还说存在,导致空查询穿透。后来改用 Counting Bloom Filter 才解决。
4.3 缓存击穿:热点 Key 过期那几秒
缓存击穿,指的是一个热点 Key 在过期瞬间,大量请求同时涌入,直接打穿到数据库。这比穿透更危险——因为数据是真实存在的,只是缓存刚好过期。
我的处理三板斧:
- 互斥锁(Mutex):第一个请求发现缓存过期,加锁去查数据库,其他请求等待。注意锁的粒度要细,别锁住整个缓存。
- 逻辑过期:缓存永不过期,但存一个过期时间字段。后台异步线程去刷新。这样即使数据变了,用户拿到的也是旧数据,但系统不会崩。
- 热点 Key 不过期:对于已知的热点,直接设置不过期,由后台任务定期更新。
// 伪代码:互斥锁方案
public Object getWithMutex(String key) {
Object value = cache.get(key);
if (value != null) return value;
String lockKey = "lock:" + key;
if (redis.setNx(lockKey, "1", 3, TimeUnit.SECONDS)) {
try {
value = db.query(key);
cache.set(key, value, 60, TimeUnit.SECONDS);
} finally {
redis.del(lockKey);
}
} else {
// 等待 50ms 后重试
Thread.sleep(50);
return getWithMutex(key);
}
return value;
}
4.4 缓存雪崩:大面积过期引发的灾难
缓存雪崩,就是大量 Key 在同一时间过期,或者缓存节点挂了,导致请求全部打到数据库。这往往是「设计失误」造成的。
预防措施:
- 过期时间加随机值:比如基础 TTL 60 秒,再加 0~30 秒随机偏移。避免集体过期。
- 多级缓存:本地缓存 + Redis 双层。Redis 挂了,本地缓存还能扛一阵。
- 限流降级:缓存失效时,对数据库查询做限流。比如每秒只允许 100 个查询,其余返回默认值。
- 缓存预热:系统启动时,提前加载热点数据到缓存,别等用户来触发。
4.5 热点 Key 发现与处理
热点 Key 是缓存系统的头号杀手。一个 Key 的 QPS 达到几十万,Redis 单节点扛不住,数据库更扛不住。
怎么发现热点 Key?
- 客户端统计:在缓存客户端(如 Jedis、Lettuce)里埋点,统计每个 Key 的访问频率。
- Redis 自身监控:用
hotkeys参数(Redis 4.0+),或者MONITOR命令(注意性能开销)。 - 代理层分析:如果用了 Twemproxy 或 Codis,可以在代理层做流量分析。
发现之后怎么处理?
- 本地缓存分摊:把热点 Key 复制到每个应用节点的本地缓存里。注意数据一致性,可以接受短暂不一致。
- Key 分散:把一个热点 Key 拆成多个子 Key,比如
hot:1、hot:2……请求时随机选一个。 - 读写分离:Redis 集群模式下,热点 Key 的读请求分散到多个从节点。
4.6 整体缓存策略架构
下面这张图,是我在实际项目中沉淀下来的缓存分层架构。你照着搭,至少能扛住 90% 的缓存问题。
4.7 总结:缓存调优的黄金法则
说了这么多,其实核心就几条:
- 分层:本地缓存扛热点,分布式缓存扛共享,数据库做最后防线。
- 兜底:穿透、击穿、雪崩,每种场景都要有预案。
- 监控:热点 Key 不是靠猜的,用数据说话。
- 简单:能用本地缓存解决的,别引入 Redis。多一层缓存,多一个故障点。
我记得有一次线上事故,就是因为一个热点 Key 过期,导致数据库被打爆。从那以后,我所有项目的缓存层都强制加了「逻辑过期 + 互斥锁」双保险。别嫌麻烦,线上出问题的时候,你会感谢这些「多余」的代码。