我为什么把“缓存”从“银弹”降级成了“双刃剑”
我为什么把“缓存”从“银弹”降级成了“双刃剑”
刚学会用 Redis 那阵子,我觉得缓存简直是性能优化的万能钥匙。什么接口慢?加缓存。什么查询耗时?加缓存。数据库压力大?加缓存。那段时间我几乎给所有接口都套了一层 RedisTemplate.opsForValue().get/set,响应时间确实好看,P99 从 500ms 掉到 30ms,我一度以为自己掌握了高并发的终极密码。
直到一次大促零点,缓存集群突然大面积超时,整个系统像多米诺骨牌一样倒下——订单、商品、用户,所有依赖缓存的接口全部 500。我手忙脚乱去查,发现是缓存击穿:一个热点商品 key 过期瞬间,几万请求直接穿透到数据库,DB CPU 秒飙 100%,连接池打满,连带拖垮了其他不相关的业务。更惨的是,缓存恢复后,大量请求回源又触发了缓存雪崩,DB 再次被打挂。
那次事故之后,我花了三天重构缓存层,也彻底改变了我对缓存的认知:缓存不是银弹,是给系统加了一层不确定性。
一、缓存带来的三个经典坑,我全踩过
1. 缓存穿透:查一个永远不存在的数据
早期商品详情接口,我先查 Redis,没有再查 DB。结果有人写脚本刷不存在的商品 ID,每次都穿透到数据库。DB 虽然没崩,但连接数一直高位运行,正常请求被拖慢。
解法:对空结果也缓存,设短过期时间(比如 2 分钟),或者用布隆过滤器提前拦截。
Product product = redis.get(productId);
if (product == null) {
product = db.query(productId);
if (product == null) {
redis.set(productId, NULL_OBJ, 2, MINUTES); // 缓存空值
} else {
redis.set(productId, product, 30, MINUTES);
}
}
2. 缓存击穿:热点 key 过期瞬间,请求全打到 DB
就是那次大促的事故。热点商品 key 过期那一刻,几万并发请求发现缓存没数据,全部去查 DB。
解法:用互斥锁,只让一个请求回源,其他请求等待重试。
String value = redis.get(key);
if (value == null) {
if (redis.setIfAbsent("lock:" + key, "1", 10, SECONDS)) {
try {
value = db.query(key);
redis.set(key, value, 30, MINUTES);
} finally {
redis.delete("lock:" + key);
}
} else {
Thread.sleep(50); // 短暂等待后重试
return get(key); // 递归或重试
}
}
3. 缓存雪崩:大量 key 同时过期
有次我给一批缓存统一设了 1 小时过期,结果一小时后集体失效,DB 瞬间被几万请求淹没。
解法:过期时间加随机值,打散失效时间点。
int expire = 3600 + new Random().nextInt(600); // 1小时±10分钟
redis.set(key, value, expire, SECONDS);
二、缓存一致性:最让人头疼的问题
缓存和数据库的双写一致性,是永远的痛点。我试过几种方案:
方案 A:先更新 DB,再删缓存(Cache Aside)
这是最常用的,但有个极小概率的并发问题:请求 A 读缓存未命中,读 DB(旧值),同时请求 B 更新 DB 并删缓存,然后 A 把旧值写入缓存。结果缓存里是脏数据。
虽然概率极低,但在金融场景不能接受。我们的解法是:延迟双删——更新 DB 后删一次缓存,500ms 后再删一次,覆盖并发窗口。
方案 B:先删缓存,再更新 DB
也有问题:删缓存后、更新 DB 前,如果有读请求,会把旧值重新加载到缓存。
方案 C:订阅 binlog 异步更新缓存
用 Canal 监听 MySQL binlog,异步刷新缓存。这是最彻底的方案,但对架构复杂度要求高,小团队不一定值得。
我们最终选了方案 A + 延迟双删,配合缓存过期时间兜底。不是完美,但够用。
三、缓存不是“更快”,是“换一种慢法”
很多人加缓存是为了“快”,但其实缓存真正的价值是削峰——把数据库的随机 IO 变成缓存的顺序访问,把热点数据挡在 DB 前面。如果你只是为了让接口从 50ms 变成 5ms,而数据更新频繁、命中率低,那缓存带来的复杂度可能不值得。
我现在加缓存前会问自己三个问题:
- 命中率能到多少? 低于 70% 的缓存,大概率是负优化。
- 数据实时性要求多高? 能接受 1 秒延迟还是必须实时?
- 如果缓存全挂,系统能撑住吗? 如果答案是“不能”,那你的架构有单点风险。
四、一个心态的转变
以前我觉得缓存是“性能优化的第一步”,现在觉得应该是最后一步。先优化 SQL、加索引、拆服务、做异步,这些做完还不够快,才考虑缓存。而且缓存不是加了就完事,要监控命中率、内存使用、慢查询,要设计降级方案——缓存挂了,系统还能用,只是慢一点。
如果你现在的系统里缓存用得很随意,不妨今晚查一下命中率。如果低于 50%,可能你加的不是缓存,是负担。
- 点赞
- 收藏
- 关注作者
评论(0)