先聊一个我印象挺深的线上问题。
有段时期我们某个详情页接口突然从 30ms 涨到 1.2s,数据库 CPU 直接飙上去。查下去发现那条数据并不存在,但前端因为埋点问题疯狂请求同一个不存在的 id,Redis 里又没有缓存这个“不存在”,于是几千 QPS 全部穿过缓存打到了 MySQL。
这种事故非常典型。Spring Boot 项目里引入 Redis 后,大家的第一反应通常是给 Service 方法加 @Cacheable。这没错,缓存注解确实能把“读多写少”的数据访问成本降下来。但等你在生产环境扛过一阵就会发现,@Cacheable 只解决了存取问题,没有解决风险问题:缓存穿透、缓存击穿、缓存雪崩、缓存与数据库的一致性、消息类场景的可靠性,这些才是真正拉开初级开发者和资深开发者差距的地方。
这篇文章不打算重复讲 @Cacheable 怎么用,而是把一个 Spring Boot + Redis 项目里我认为最值得抄走的 4 个实战模式拆开揉碎,每个模式都讲清楚适用场景、实现方案、为什么这样设计,以及我在实际项目中踩过的坑。适合已经会用 Redis 基础命令和 Spring Cache,但想进一步把 Redis 用到生产可观测、可治理、可长期维护的开发者。
1. 先承认一件事:@Cacheable 只解决“存取”,不解决“边界情况”
很多教程展示 @Cacheable 的代码只有三行,看起来优雅无比。但把这样一个注解铺到整个 Service 层后,问题会从几个方向冒出来。
1.1 注解帮你缓存,不帮你扛并发
@Cacheable 底层走的是 Spring Cache 抽象,CacheManager 找到对应的 Cache,然后 get/put。它默认的语义是“先查缓存,没有就执行方法,把结果放进去”。
这听起来没问题,但高并发下有几个缺口:
- 如果查询的 key 在数据库中不存在,方法返回 null,Spring Cache 默认不缓存 null,导致每次请求都穿过缓存打到数据库。
- 如果某个热点 key 刚好过期,恰好有一万个请求同时到达,Spring Cache 本身无法保证多个实例之间只有一个线程去执行回源查询。虽然本地 Caffeine 的同步机制可以保护单实例,但分布式环境下每个服务节点各查一次,数据库照样扛不住。
- 一
