去年我团队就栽了个跟头——某个周五晚上十点,我突然接到报警短信,说是API响应时间从平均200ms飙到了2秒以上。用户投诉像雪片一样飞进来,那会儿真是焦头烂额。折腾到凌晨三点才发现,问题出在一个看似简单的缓存失效上。其实这事儿挺常见,但每次都能以不同方式让你失眠。

缓存失败说白了就是系统该用缓存的时候没用上,或者用了不该用的旧数据。就像人的短期记忆突然紊乱,该想起的事情想不起来,不该信的旧信息却深信不疑。我常跟团队说,缓存机制是性能的加速器,但一旦失效就成了最隐蔽的绊脚石。
那次让我失眠的缓存故障
当时我们的电商应用商品详情页突然变得极慢,最初怀疑是数据库压力过大。但查看监控发现数据库查询量并没什么变化,反倒是Redis缓存命中率从平时的95%跌到了不足60%。这就很奇怪了——明明缓存集群看着正常,为什么命中率会暴跌?
查日志时发现大量这样的错误:
ERROR [redis-worker] Failed to refresh cache key: product_12345
Caused by: ConnectionTimeoutException: Unable to acquire connection from pool
表面上是连接超时,但深层原因其实是缓存客户端配置不当。我们用的连接池最大连接数设置为100,而突然的流量高峰导致连接不够用。线程阻塞在获取连接的过程中,既无法读取缓存,也无法更新缓存,最终全部请求直接穿透到数据库。
更麻烦的是,缓存更新时使用了简单的delete-after-update策略:先删缓存再更新数据库。这在高并发场景下就是个坑——当多个请求同时发现缓存不存在,就会同时去查数据库并写缓存,导致缓存雪崩和数据库压力激增。
那晚的紧急修复过程是这样的:首先临时增加了Redis连接池大小到200(虽然这只是应急),然后在缓存更新策略上加了分布式锁,确保同一时间只有一个请求去更新缓存。最后还得手动刷新一批热点商品的缓存数据。折腾完天都亮了,响应时间总算回到了300ms以内。
浏览器缓存:最容易被忽视的角落
不只是服务端缓存会出问题,浏览器缓存也是重灾区。我记得有次用户反馈说刚更新的页面样式就是不生效,死活看到的是旧版本。排查发现是Nginx配置里给静态资源设置了过长的过期时间:
location ~* \.(js|css|png)$ {
expires 1y;
add_header Cache-Control public;
}
虽然设置了很长的过期时间,但更新后文件名没做哈希处理,导致用户浏览器一直使用本地缓存。后来我们改成用webpack给文件名加内容哈希,比如从app.js变成app.3a7b8c.js,这样每次更新文件名自动变化,强制浏览器下载新资源。
用Chrome DevTools排查这类问题特别方便。我一般先打开Network面板,勾选Disable cache模拟新用户,然后取消勾选查看正常用户的加载情况。关注哪些资源是从memory cache或disk cache读取的,状态码是不是304 Not Modified。话说我觉得Chrome的缓存管理比Firefox更直观,特别是清除特定资源缓存的功能。
APP端的缓存陷阱
移动端缓存的坑更多。我们有个APP曾经因为图片缓存策略失误,导致用户存储空间被快速占满。本来是想提升图片加载速度的,结果用了无限制的磁盘缓存,还没实现良好的过期机制。有些用户手机里竟然存了几个G的缓存图片,最后只能整个清空缓存,连登录状态都丢了。
后来我们改用了LRU(最近最少使用)算法来自动清理旧缓存,并根据手机存储空间大小动态调整缓存上限。代码大致是这样的:
// Android示例:设置图片缓存大小
long maxCacheSize = Runtime.getRuntime().maxMemory() / 8; // 使用最大内存的1/8
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxCacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
系统级缓存:那些看不见的影响
系统层面缓存失败往往更隐蔽。有一次我们的服务器性能突然下降,CPU使用率很高但就是找不到原因。最后发现是文件系统缓存被某个运维脚本意外清空了,导致磁盘IO暴增。
Linux系统可以用free命令查看缓存情况:
$ free -h
total used free shared buff/cache available
Mem: 16G 5.2G 2.1G 1.1G 8.7G 9G
Swap: 2.0G 0B 2.0G
这里的buff/cache就是系统缓存,如果突然大幅减少,很可能就是性能问题的根源。我们后来写了个监控脚本,专门跟踪这个值的变化。
DNS缓存失效也是常见问题。有次我们的服务突然无法解析内部域名,最后发现是DNS缓存过期时间设置太短,而且客户端没有实现重试机制。换句话讲,缓存失败时没有降级方案,系统直接就崩了。
修复缓存失败的实用步骤
经过这么多坑,我现在排查缓存问题基本按这个流程来:
先看监控数据。缓存命中率是首要指标,如果发现命中率下降,立即检查缓存服务状态。我们用的是Prometheus + Grafana监控Redis,关键指标包括缓存命中数、未命中数、内存使用率和连接数。
再查日志。找缓存相关的错误信息,特别是连接超时、序列化错误这些。有时候缓存客户端版本升级也会引入兼容性问题,我就遇到过Jackson版本升级导致缓存反序列化失败的情况。
然后验证缓存内容。Redis里面直接看key是否存在,数据是否正确:
redis-cli
> exists product_12345
> get product_12345
对于浏览器缓存,用DevTools看请求头和响应头。重点关注Cache-Control、ETag和Last-Modified这些字段是否正确设置。
最后才是考虑重启服务或清理缓存。这是最后手段,因为清缓存可能造成缓存穿透,瞬间压力可能压垮后端服务。如果非要清,最好在低峰期分批进行,或者使用延迟双删策略:
// 先删缓存
redis.delete(key);
// 更新数据库
db.update(data);
// 延迟几秒再删一次
Thread.sleep(2000);
redis.delete(key);
预防胜过治疗:缓存设计之道
我现在设计系统时,会把缓存失败作为必然发生的场景来考虑。这意味着必须有降级方案——缓存失效时系统还能正常工作,哪怕性能暂时下降一些。
设置合理的过期时间很重要。完全不过期不行,太短了又失去缓存意义。我们的做法是基础数据设置较长过期时间(比如24小时),动态数据设置较短时间(比如5分钟),并且支持主动更新。
多重缓存策略也挺实用。比如本地缓存+分布式缓存的组合,即使Redis挂了,本地缓存还能撑一会儿。不过要注意数据一致性问题,我们用了发布订阅机制来同步各个节点的本地缓存。
监控报警必不可少。除了缓存命中率,我们还设置了缓存更新时间、缓存大小增长趋势等报警指标。一旦发现异常,立即告警,不等用户反馈就能介入处理。
最后说点个人偏见:我觉得很多团队过度依赖缓存了,把缓存当作系统设计缺陷的遮羞布。其实如果底层架构和数据库查询优化做好了,对缓存的依赖会小很多。缓存应该是性能加速器,而不是系统的救命稻草。
缓存失败这事儿,说大不大说小不小。但每次处理都能让你对系统有更深的理解。反正记住一点:设计系统时假定缓存会失效,编码时假定缓存会命中,监控时假定缓存会出错。这样就能在不可避免的缓存问题面前,保持从容不迫了。
再啰嗦一句,文档写再好看一百遍,不如实际踩一次坑记得牢。遇到缓存问题别慌,一步步拆解定位,往往没想象中那么复杂。


评论