第 4 章 本地缓存选型:Caffeine vs Ehcache vs Guava Cache
系统边界与分层职责
本地缓存位于应用进程内部,作为 JVM 堆内或堆外的高速数据暂存层。其典型系统边界如下:
- 调用端:业务服务、DAO 层、网关过滤器——通过缓存抽象接口(如 Spring Cache、JCache)发起 get/put 操作。
- 本地缓存实例:嵌入在应用进程中,与业务代码直接共享堆内存。相比分布式缓存(Redis),无需网络开销,延迟通常在微秒级。
- 数据源:数据库、文件系统、远程 RPC——当缓存未命中时,由业务方自行回源加载,并回调缓存实例的 put 方法填充。
本章讨论的三款缓存组件均覆盖此边界,但它们在堆外内存支持、淘汰算法、统计指标以及Spring 集成方案上存在显著差异。
组件职责与核心对比
三者在 JVM 内存中的职责划分可借助以下表格快速理解:
| 维度 | Caffeine | Ehcache 3.x | Guava Cache |
|---|---|---|---|
| 堆内内存 | 支持(通过 size 控制计数或权重) | 支持 | 支持 |
| 堆外内存 | 不支持 | 支持(off-heap 指定大小) | 不支持 |
| 淘汰算法 | TinyLFU(W-TinyLFU,接近最优命中率) | LRU / LFU / FIFO(可配置) | 基于访问顺序的 LRU(基础版) |
| 统计指标 | 丰富:命中率、加载时间、驱逐计数、容量感知 | 中等:命中/未命中计数、持久化统计 | 基础:hitCount/missCount,缺少驱逐详情 |
| Spring 集成 | 原生支持 Spring Cache 注解(自动配置) | Spring Boot Starter 认证完善 | 需手动封装 CacheManager |
| JCache(JSR 107) | 不完全实现 | 官方实现 | 不支持 |
| 持久化 | 不提供 | 支持磁盘持久化(通过缓存管理器) | 不提供 |
| 性能(吞吐量) | 极高(设计上针对高并发优化) | 较高(IO 线程池可调优) | 一般(同步锁较重) |
关键判断:若需要堆外内存以规避 GC 压力,或需要磁盘持久化,Ehcache 是唯一选项;若追求极致性能与高效淘汰,Caffeine 是首选;Guava Cache 适合旧项目迁移或 JCache 绑定。
数据流与信任链路
如下图所示,三款组件的核心数据流均由应用层驱动,差异主要体现在淘汰算法和内存管理方式。
信任链路方面:应用层通过缓存抽象接口直接调用组件实例,不信任本地缓存的持久性(允许丢失),因此回源策略必须由业务保证一致性。Ehcache 的堆外内存和持久化可选地提供更高可靠性,但代价是引入 IO 边界。
选型权衡
从架构角度,推荐按照以下优先级做决策:
- 是否需要堆外内存或持久化? → 选 Ehcache。
- 是否已有 Guava Cache 代码且无法重构? → 维持 Guava Cache,但考虑逐步替换为 Caffeine(性能收益明显)。
- 新项目 / 追求吞吐量 → 选 Caffeine,Spring 集成近乎零配置。
- 需要 JCache 标准兼容? → 选 Ehcache(官方 JCache 实现)。
注意:Guava Cache 的淘汰算法在热点数据频繁更新的场景下命中率下降明显,且缺少驱逐回调与权重控制。不推荐在要求高命中率的中间件中使用。
风险点
- 堆内 OOM:Caffeine 和 Guava Cache 仅限制元素个数或权重,若每个元素体积巨大,仍可能撑爆堆内存。需配合监控限制最大权重。
- 堆外内存泄漏:Ehcache 堆外内存管理不当可能导致 Direct Memory 溢出,需设置合理的 maxBytesLocalOffHeap 并手动触发 clear。
- Spring 双缓存冲突:当同时引入 Caffeine 和 Ehcache 的 Starter 时,默认 CacheManager 可能导致注解指向混乱,需通过
spring.cache.type显式指定。 - 淘汰延迟:Guava Cache 的淘汰是懒惰式的(在 get 时检查),大量过期条目堆积可能引起瞬时性能抖动。
落地步骤
- 选择组件并引入依赖(如适用):
// Maven 示例 - Caffeine <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency> // Spring Boot 自动配置 spring.cache.type=caffeine spring.cache.caffeine.spec=maximumSize=10000,expireAfterWrite=5m - 配置缓存规范:通过 CaffeineSpec 或 Ehcache XML 定义最大容量、过期策略、统计收集开关。
- 集成 Spring Cache:使用
@Cacheable/@CacheEvict注解,并注册对应的CacheManagerBean。 - 监控与告警:开启统计指标 exporter(如 Micrometer),配置命中率阈值告警。
- 压测验证:用 JMH 或 Gatling 对比淘汰算法命中率与 GC 影响。
验收标准
| 验收项 | 指标 | 方式 |
|---|---|---|
| 缓存可被正确命中 | 同 key 第二次访问不执行回源方法 | 单元测试 + 日志 |
| 淘汰策略生效 | 达到最大容量后,继续 put 不会 OOM | 压力测试(循环写入) |
| 堆外内存(如适用) | Direct Buffer 使用量不超过设定值 | jconsole / NMT |
| 统计指标暴露 | 命中率、驱逐计数可通过 Actuator 读取 | GET /actuator/metrics/cache.gets |
| Spring 注解兼容 | 多 Service 共用同一缓存区域,key 隔离正常 | 功能回归 |
最佳实践:在灰度环境中,先使用 Caffeine 的
recordStats() 采集一周实际命中率,再与 Ehcache 对比分析,避免无依据的选型决策。