几乎每个做过后端的人都写过缓存:加一个 Redis,把查询结果塞进去,加个过期时间,性能立刻从几百毫秒降到几毫秒。然后项目继续往前跑,直到某天有人问一句「为什么用户改了资料,页面上还是旧的?」——这时候你才意识到,缓存的难点从来不在「怎么加」,而在「什么时候该失效」。这篇文章想聊的不是 Redis 的 API,而是缓存的失效策略,以及那些真正让人半夜爬起来改代码的坑。
一、缓存的本质:一次用钱换时间的交易

先把最基本的事情说清楚。缓存不是免费的午餐,它是一次交易:你用「可能读到旧数据」的风险,换「读得更快」的收益。任何一次缓存的引入,本质上都是在做这笔交易,问题只在于你有没有意识到自己签了这份合同。
所以设计缓存时,第一个该问的问题不是「怎么缓存」,而是「这份数据能容忍多旧?」。
不同类型的数据,答案差别巨大。商品的浏览量、点赞数,容忍度可能是几分钟甚至半小时——反正用户自己也记不清刚才点了几下。而用户的余额、订单状态、库存数量,容忍度几乎为零,读到一次旧数据就是一次资损或一次客诉。介于两者之间的,是那些「旧一点没关系,但一直旧就有问题」的数据,比如商品详情、文章列表,这类通常用几分钟到几十分钟的 TTL 就够了。
把数据按这个维度分类,比直接上缓存有意义得多。很多线上事故的根因,不是缓存写错了,而是把一个零容忍的数据,按高容忍的规格缓存了。比如把用户的权限列表用 30 分钟 TTL 缓存起来,管理员刚收回了某人的权限,那个人还能继续操作半小时——这在安全审计里是明确的缺陷。
还有一个常被忽略的点:缓存读得快,也意味着错误传播得快。如果没有缓存,一个脏数据只影响一次请求;有了缓存,一个脏数据会在 TTL 内被复制成千上万次。缓存不只放大性能,也放大错误。这就是为什么后面要讲的失效策略,比缓存本身更值得认真设计。
二、三种失效策略,以及它们各自的坑
策略一:过期即失效(TTL)
最朴素的做法,给每个 key 设一个过期时间,时间到了自然删掉,下次读时重新加载。它的优点是简单、无状态、不需要任何额外的失效通知机制,非常适合容忍度高的数据。
它的坑在于「过期时间」这个值本身。设太短,缓存形同虚设,请求全部落到数据库;设太长,数据陈旧的时间就长。更麻烦的是过期时间集中:如果一批 key 是在同一时刻批量写入的,它们也会在同一时刻集中失效,然后所有请求同时涌向数据库——这就是经典的缓存雪崩。解法是给 TTL 加一个随机抖动,比如基础 600 秒上下浮动 ±60 秒,把失效时间打散。
策略二:写时更新/删除(Cache Aside)

这是最主流的策略:读的时候先查缓存,没有再查数据库并回填;写的时候先更新数据库,再删除缓存。注意这里是「删除」而不是「更新」,因为删除是一个幂等且必然正确的操作,而更新缓存则可能因为并发顺序问题把旧值写回去。
但即便是「先更新库、再删缓存」,在极端并发下仍有一个窄窗口:读请求 A 在缓存未命中后查到了旧值,此时写请求 B 完成了更新并删除了缓存,然后 A 才把它读到的旧值回填进缓存——结果缓存里躺着一个不会再被更新的旧数据。这个窗口很窄,但真实存在。常见的缓解手段是延迟双删(写完后延迟几百毫秒再删一次),或者给缓存设一个较短的兜底 TTL,让这个脏值最多活几分钟。工程上更实用的态度是:承认它存在,估算它的影响面,用短 TTL 兜底,而不是试图设计一个理论完美的方案。
策略三:主动失效与版本号
当数据之间有关联时(比如一个用户的资料变了,所有引用它的页面都该更新),逐个删除缓存会变成一场噩梦。这时更好的做法是引入版本号:给数据打一个 version,缓存 key 里带上版本,更新时直接让版本号加一,旧版本的缓存自然再也没人读,等着被淘汰即可。
这种方式的好处是失效变成了一次原子性的版本递增,而不是「找出所有相关 key 再删」这种易漏易错的遍历。它的代价是多占一点内存——旧版本的缓存会短暂滞留——但通常完全可以接受。
三、比策略更重要的三条纪律
策略讲完了,但在实际项目里,比选哪种策略更关键的,是下面这三条纪律。
第一,缓存必须有降级路径。Redis 挂掉的那一刻,所有请求会瞬间打到数据库上。如果你的代码在缓存不可用时直接抛异常,那就是一次全站故障;如果它能退化成直接查库(哪怕慢一点),那只是一次性能抖动。上线前一定要问:缓存整个没了,系统还活着吗?
第二,给缓存加可观测性。命中率、平均加载耗时、key 的数量增长曲线,这三个指标能提前告诉你很多事。命中率从 95% 掉到 60%,往往意味着 key 的设计出了问题或者 TTL 设得太短,这比等到数据库被打满才发现要好得多。
第三,把「不缓存」当成一个正当选项。不是所有查询都值得缓存。如果一个查询本身只要 5 毫秒,缓存反而可能因为多一次网络往返而变慢,还额外引入了一致性问题。缓存的引入应该由数据说话——先测出瓶颈,再决定缓存什么,而不是「反正是性能优化,加上总没错」。
这三条听起来像常识,但常识之所以是常识,恰恰因为大多数人是在踩过坑之后才承认它。

结语
缓存是一门关于「取舍」的手艺。它没有银弹式的正确答案,只有在具体场景下更合适或更不合适的选择。真正把缓存用好的团队,不是记住了多少种模式,而是始终清楚自己在拿什么换什么:这份数据能容忍多旧,这份风险能不能接受,以及最坏情况下系统会不会塌。
下次你往代码里加一行 cache.set() 的时候,不妨先停三秒,问自己一句:它什么时候该失效?如果这个问题答不上来,那这行缓存可能不该现在加。