去年夏天,昌平的出租屋里空调嗡嗡响,我在给订单系统加缓存。当时觉得这事儿简单:把数据库查出来的数据往Redis一塞,下次查就直接从内存取,性能不得起飞?结果上线当晚,运维电话就打来了——缓存雪崩,数据库CPU飙到90%。这是我第一次踩坑,也是我真正开始理解“缓存不是银弹”的起点。

第一次:全量缓存,雪崩来袭

当时做的电商活动页,商品详情一天变一次。我心想:既然变化不大,那把全部商品都缓存起来呗。于是在应用启动时,把所有商品详情load进Redis,设置过期时间24小时。访问量上来后,那个丝滑——页面唰唰的。直到某天凌晨,运维监控报数据库连接数暴增。看日志,所有缓存键在同一秒过期,瞬间几万个请求直接砸向MySQL,活动页直接白屏。

为什么这么选?无知者无畏。觉得“缓存自然是全量才好”,没考虑过期时间一致带来的集中失效问题,也没考虑冷启动时如果Redis里没数据该怎么办。

如果现在让我做,我会给过期时间加上随机偏移,比如24小时±1小时内随机,避免雪崩。还会做个兜底的互斥锁,当缓存失效时,让只有一个线程能去查库重建缓存。

第二次:预热+随机过期,穿透又来了

吃了上次的亏,我学聪明了——启动时异步拉取热门商品预热,过期时间随机化,还加了个互斥锁。平稳运行两个月,直到运营搞了个“秒杀预告”,放了几个不存在的商品ID做测试。好家伙,大量请求命中不存在的ID,缓存里没有,互斥锁也没用(因为每次请求都不同ID),全部透传到数据库,又一个慢查询拖垮了服务。

为什么这么选?只考虑了“热数据”的缓存,完全没想到无效数据造成的穿透。认为数据来源只有运营后台录入,不会有恶意请求或错误ID,忽略了现实世界的不确定性。

现在我会在Redis中缓存一个空对象,哪怕ID不存在,也存个“empty”标记,过期时间设短一点(比如5分钟)。或者在网关层用布隆过滤器核验ID是否存在。

第三次:布隆过滤器+空对象,热key又爆了

我自信满满地上了布隆过滤器,空对象也存了,以为万事大吉。结果大促某一天,一个爆款商品的详情页突然卡得要死。查监控,发现这个key的访问量涨了10倍,直接打满了某一台Redis的CPU。好巧不巧,那个key的slot刚好分到了单线程处理的哈希槽,其他请求都排队。

为什么这么选?忽略了热点数据倾斜问题。以为Redis单线程都能扛,没想到一个热key在突发流量下能成为瓶颈。当时脑子里的缓存体系,只有“如何防击穿、穿透”,却没有“如何分散热点”这一环。

现在面对可预知的热点,我会在前端加一层本地缓存(比如Caffeine),或在Redis里对这个key做分片:key_1、key_2……随机读一个。甚至在活动前用脚本预先加载到各节点。

回顾这三次打脸,我总算想明白一件事:缓存本质上不是提速工具,而是异步状态的收敛问题。你得考虑数据一致性、失效策略、冷热分布、异常流量……这些点环环相扣。每一次踩坑,都让我离“理解系统”更近一步。现在如果让我给刚入行的自己一句建议,我会说:先想清楚“缓存失效时会发生什么”,再动手写代码。毕竟,线上的报错,比出租屋里的空调声刺耳多了。