缓存失效策略注意事项:别让过期数据拖慢你的系统

缓存不是万能药,用不好反而添乱

很多开发团队一开始引入缓存,是为了减轻数据库压力、提升接口响应速度。但很快就会发现,缓存数据和数据库不一致的问题开始冒头——用户刷新页面看到的还是旧信息,订单状态迟迟不更新,后台配置修改了前端却没反应。这些问题背后,往往是因为缓存失效策略设计得不够细致。

别盲目使用定时过期

很多人一上来就给缓存设个 TTL(Time To Live),比如 60 秒自动过期。听起来简单粗暴有效,但在实际场景中容易出问题。比如你有个商品详情页,库存信息缓存 60 秒,这期间如果库存被抢光,用户下单时才发现“已售罄”,体验就很差。相反,如果商品长期没人看,却每秒都在刷新缓存,又浪费资源。

更合理的做法是结合业务频率动态调整。热门商品可以短一些,冷门商品可以长一点,甚至在关键操作后主动清除缓存,而不是被动等待过期。

写操作后记得清理相关缓存

当数据库发生更新时,对应的缓存必须及时处理。常见的做法是在更新数据库之后,立即删除对应 key 的缓存。比如用户修改了昵称:

// 更新数据库
userRepository.updateNickname(userId, newNickname);

// 删除缓存
redis.delete("user:profile:" + userId);

这样下次请求进来,会重新从数据库加载最新数据并重建缓存。注意顺序不能颠倒,否则可能在缓存重建时读到旧数据。

小心缓存穿透和雪崩

缓存穿透是指查询一个根本不存在的数据,每次都会打到数据库。比如恶意攻击者频繁查 id=-1 的用户。解决办法是对这类请求也做缓存,但值设为 null,并设置较短的过期时间。

缓存雪崩则是大量缓存在同一时间点集体失效,导致瞬间流量全部压向数据库。为了避免这种情况,可以在设置 TTL 时加一点随机偏移,比如基础 300 秒,再随机加 0~60 秒:

int expireTime = 300 + random.nextInt(60);
redis.setex(key, expireTime, data);

考虑使用延迟双删防止脏读

在高并发场景下,写操作发生时,可能有其他线程正在读取旧缓存并准备回填,这时直接删一次可能不够。可以采用“延迟双删”策略:先删一次缓存,等数据库写完后,再睡几百毫秒,再次删除缓存。

redis.delete("user:profile:" + userId);

userRepository.update(user);

Thread.sleep(500); // 生产环境建议用异步任务代替sleep

redis.delete("user:profile:" + userId);

虽然 sleep 不够优雅,但在某些强一致性要求高的场景下能有效减少脏数据出现的概率。

监控缓存命中率很重要

再好的策略也要靠数据验证。定期查看缓存命中率,如果发现突然下降,可能是失效逻辑出了问题,或者业务行为发生了变化。比如原本每天只改一次的配置项,现在频繁修改,导致缓存频繁失效,这时候就得重新评估策略。

还可以给关键缓存加上日志,记录什么时候被写入、被删除、被命中,排查问题时能省不少事。