结果导向 · 怎么选 · 为什么 · 生产上长啥样 · 有什么坑
本地缓存用 Caffeine(Java 本地缓存事实标准,比 Guava 快 30%-80%);多台机器共享用 Redis;不想写缓存代码就用 Spring @Cacheable 注解。HashMap 做缓存 = 定时炸弹,生产环境绝对不要用。
| 你的需求 | 选什么 | 一句话原因 |
|---|---|---|
| 单机本地缓存(配置字典/热点数据) | Caffeine | Java 本地缓存王者,W-TinyLFU 淘汰算法,自带过期/统计/异步刷新 |
| 多台机器共享缓存 | Redis | 独立进程,所有机器访问同一份缓存,数据一致 |
| 不想写缓存代码 | Spring @Cacheable | 注解一行搞定,底层可切换 Caffeine/Redis |
| 既要快又要一致 | Caffeine + Redis 多级缓存 | L1 本地(纳秒级)→ L2 Redis(毫秒级)→ DB |
| HashMap 做缓存 | ❌ 别用 | 没有淘汰策略 → OOM;线程不安全 → 并发死循环 |
每个技术一句话说清楚,不用记原理,知道它是干啥的就够了。
| 技术 | 是什么 | 一句话定位 |
|---|---|---|
| Caffeine | Java 本地缓存库 | Guava Cache 的继任者,W-TinyLFU 淘汰算法,性能比 Guava 高 30%-80%,Java 本地缓存事实标准 |
| Redis | 内存数据库(独立进程) | 独立于应用进程运行,所有机器访问同一份数据,支持持久化、过期、发布订阅 |
| Spring @Cacheable | 注解化缓存抽象 | 不是缓存实现,是封装层 — 注解一行搞定缓存,底层可切换 Caffeine/Redis/Ehcache |
| Guava Cache | Google 的本地缓存库 | Caffeine 的前辈,已停更,新项目用 Caffeine 替代 |
| Ehcache | 老牌 Java 缓存库 | 支持本地+分布式,但配置繁琐,新项目基本不用了 |
| HashMap | JDK 自带的 Map | 不是缓存库 — 没淘汰策略(OOM)、没线程安全(并发问题),生产环境别拿来当缓存 |
| HashMap | Caffeine | Redis | |
|---|---|---|---|
| 在哪运行 | JVM 进程内 | JVM 进程内 | 独立进程(单独的服务器) |
| 速度 | 纳秒级(最快) | 纳秒级(最快) | 毫秒级(要走网络) |
| 多机共享 | ❌ 各自一份 | ❌ 各自一份 | ✅ 所有机器同一份 |
| 淘汰策略 | ❌ 没有 → OOM | ✅ W-TinyLFU(最优) | ✅ LRU/LFU/随机/TTL |
| 过期时间 | ❌ 没有 | ✅ 写后过期/访问后过期 | ✅ TTL |
| 线程安全 | ❌ 不安全 | ✅ 安全 | ✅ 安全(单线程模型) |
| 断电丢失 | 丢 | 丢 | 有持久化(RDB/AOF) |
| 适合场景 | ❌ 别当缓存用 | 单机热点数据/配置字典 | 多机共享/分布式缓存 |
Caffeine 不只是"存个 Map",它是个专业缓存库,自带淘汰、过期、刷新、统计等能力。
recordStats() 一行开启,接 Spring Boot Actuator 暴露到监控面板。| 能力 | 能做什么 | 类比 |
|---|---|---|
| 自动淘汰 | 设 maxSize,满了自动淘汰冷数据 | 书架满了自动收回最久没看的书 |
| 写后过期 | expireAfterWrite(10, MINUTES) — 写入 10 分钟后失效 | 牛奶上的保质期,过了就扔 |
| 访问后过期 | expireAfterAccess(5, MINUTES) — 最后访问 5 分钟后失效 | 长时间没翻的书收回柜子 |
| 异步刷新 | refreshAfterWrite — 后台刷新,旧值继续返回 | 换班时老员工先顶着,新员工后台接手 |
| 统计监控 | 命中率/加载耗时/驱逐数/缓存大小 | 书架上的统计表:这周借了多少本、退了多少本 |
| 线程安全 | 基于 ConcurrentHashMap,并发安全 | 多人同时取书不会打架 |
开启 recordStats() 后,通过 Spring Boot Actuator 可以看到缓存统计。下面模拟一个生产环境的缓存监控面板:
| 维度 | 本地缓存(Caffeine) | 分布式缓存(Redis) |
|---|---|---|
| 在哪 | JVM 进程内存里 | 独立进程(单独服务器) |
| 速度 | 纳秒级(最快) | 毫秒级(要走网络) |
| 多机共享 | ❌ 每台机器各自一份 | ✅ 所有机器同一份 |
| 数据一致 | ❌ 各机器可能不一致 | ✅ 一致 |
| 容量 | 受 JVM 堆内存限制(GB 级) | 可独立扩展(TB 级) |
| 断电 | 丢失 | 有持久化(RDB/AOF) |
| 适合 | 配置字典/元数据/极少变更/单机热点 | 用户会话/商品信息/多机共享数据 |
| 搭配 | 是啥 | 为啥搭配 |
|---|---|---|
| Caffeine + Redis | L1 本地 + L2 分布式 | 90% 请求 L1 秒回(不走网络),9% L2 共享,1% 查 DB。兼顾速度和一致性 |
| @Cacheable + Caffeine | 注解 + 本地实现 | 不想写缓存代码,@Cacheable 注解搞定,底层用 Caffeine 存 |
| @Cacheable + Redis | 注解 + 分布式实现 | 同上,底层切到 Redis,多机共享 |
| Redis + 分布式锁 | SETNX 实现锁 | 防止缓存击穿 — 大量请求同时查 DB,用锁只让一个查 |
| Redis + MQ | 缓存更新通知 | 数据变更时发 MQ 消息,所有机器清本地 Caffeine 缓存 |
现象:某天 10:00 整,系统突然响应变慢,DB CPU 飙到 100%。排查发现 10:00 有大批缓存 key 同时过期(因为设置了相同的过期时间),所有请求穿透到 DB。
Redis 监控面板:看到大量 key 同时过期
现象:有人恶意请求 /api/user/-1(不存在的 ID),缓存里当然没有 → 每次都查 DB → DB 被打爆。Caffeine 监控面板显示命中率断崖式下跌。
Caffeine 监控面板:命中率异常下跌
userId=-1,DB 里没有 → 返回 null → 不放缓存 → 下次又查 DB → 循环穿透。现象:大促时某个爆款商品 product:10086 的 QPS 占了 80%,Redis 单 key 所在的分片 CPU 打满,其他分片闲着。
Redis 监控面板:热点 key 一目了然
现象:某服务用 static HashMap 缓存用户数据,运行 3 天后 OOM 重启。
原因:HashMap 没有淘汰策略,数据只进不出,内存持续增长直到 OOM。
解法:换 Caffeine,设 maxSize + expireAfterWrite。
教训:HashMap 不是缓存库。没有淘汰策略 = 定时炸弹。
现象:加了 @Cacheable,但缓存命中率 0%,每次都查 DB。
原因:@Cacheable 基于 Spring AOP 代理。同一个类内部方法调用,不经过代理 → 注解失效。
解法:把缓存方法拆到另一个类,或者注入自己(self.getUserFromDb)。
教训:@Cacheable 必须跨类调用才生效。这是 Spring AOP 的通病。
现象:某接口偶发超时,Redis 延迟从 1ms 飙到 200ms。
原因:有人存了一个 50MB 的 JSON 到一个 Redis key(all_products)。Redis 是单线程的,读写这个大 key 会阻塞所有其他操作。
解法:大 key 拆分 — all_products → products:category:1 / products:category:2... 每个 key 控制在 10KB 以内。
教训:Redis 单 key 不要超过 10KB(String)或 1万个元素(Hash/List/Set)。大 key 是 Redis 性能杀手。
现象:商品改价 1 元 → 100 元,但用户下单还是 1 元。缓存里还是旧价格。
原因:先改 DB 再删缓存,但删缓存失败了 → 缓存还是旧值 → 用户看到 1 元 → 下单 → DB 里已经是 100 元 → 价格不一致。
解法:
教训:缓存一致性是最终一致,不是强一致。核心交易数据不要缓存,或用极短 TTL(如 5 秒)。
选 Caffeine。Guava Cache 已停更,Caffeine 是它的精神继承者,API 几乎一样(迁移成本低),但性能高 30%-80%(W-TinyLFU 算法)。新项目无脑 Caffeine,老项目有空就迁移。
简单场景用 @Cacheable(注解一行搞定,代码干净)。复杂场景手动写(多级缓存、条件缓存、异步刷新等 @Cacheable 不好搞)。
@Cacheable 的坑:自调用失效(同类内部调用不经过代理)、不支持多级缓存(需要自定义 CacheManager)、条件不好控制。
| 雪崩 | 穿透 | 击穿 | |
|---|---|---|---|
| 是什么 | 大量 key 同时过期 | 查不存在的数据 | 热点 key 过期 |
| 结果 | DB 被大量请求打爆 | 每次都查 DB | 一个 key 过期,大量请求同时查 DB |
| 解法 | 过期时间加随机值 | 缓存空值/布隆过滤器 | 热点 key 永不过期 + 分布式锁 |
记忆口诀:雪崩是"一批过期",穿透是"查没有的",击穿是"一个热点过期"。
必须设。即使数据很少变更,也要设一个较长的 TTL(如 30 分钟)作为兜底。原因:
配置字典这类极少变更的数据:TTL 30 分钟 + maxSize 5000。
热点商品这类变更频繁的数据:TTL 5 分钟 + maxSize 10000。
🗄️ 缓存方案选型与实战 · 全栈资料库