本地缓存三巨头:Caffeine vs Guava Cache vs Ehcache——到底谁才是王者
引言
面试题:为什么不用本地缓存?答:分布式系统都用 Redis。
但现实是——很多场景本地缓存仍然是刚需:
- 配置数据、字典数据——QPS 高但极少变更,每次走 Redis 多一跳网络
- 热点商品信息——Redis 也会被打爆,本地缓存兜底
- 计算结果缓存——复杂的统计结果,本地存一份避免重复计算
Java 生态里三大本地缓存各有拥趸:
- Guava Cache——老牌选手,曾经的王者
- Ehcache——老牌中的老牌,功能最全
- Caffeine——新生代,号称"Guava Cache 的替代品"
到底选谁?社区争论很多,但缺少实测数据支撑。
这篇文章从 6 个维度做实测对比:读写吞吐、内存占用、过期策略、淘汰算法、Spring Cache 集成、异步加载。每个维度都有压测数据,看完你心里就有答案了。
一、三巨头简介
1.1 Guava Cache
出身:Google Guava 工具库的一部分,2010 年左右推出。
定位:简单易用的进程内缓存,API 设计优雅。
特点:
- API 简洁,
CacheBuilder链式调用 - 支持 TTL、TTI、基于引用的回收
- 支持
CacheLoader自动加载
现状:进入维护模式,官方推荐迁移到 Caffeine。
Cache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
1.2 Ehcache
出身:2003 年发布,老牌中的老牌。
定位:企业级缓存,支持堆内、堆外、磁盘三级存储。
特点:
- 支持持久化到磁盘(重启不丢)
- 支持分布式(Ehcache 3.x + Terracotta)
- 功能最全,但 API 最重
现状:Ehcache 3.x 仍在维护,但在新项目中使用率走低。
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build();
cacheManager.init();
Cache<Long, User> cache = cacheManager.createCache("userCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Long.class, User.class,
ResourcePoolsBuilder.heap(10000)
).build());
1.3 Caffeine
出身:2015 年开源,作者是 Guava Cache 的贡献者之一。
定位:现代化高性能本地缓存,Guava Cache 的继任者。
特点:
- W-TinyLFU 算法:命中率比 LRU 高出 30%
- 异步 API:基于 CompletableFuture,不阻塞线程
- 写后异步刷新:不阻塞读请求
- API 和 Guava Cache 几乎一样,迁移成本极低
现状:Spring Boot 5.0+ 默认本地缓存实现就是 Caffeine。
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
二、六维实测
2.1 测试环境
| 项目 | 配置 |
|---|---|
| CPU | Apple M2 8 核 |
| 内存 | 16GB |
| JDK | OpenJDK 21 |
| 测试数据 | 100 万个 User 对象(约 1KB) |
| 测试工具 | JMH(Java Microbenchmark Harness) |
2.2 测试代码骨架
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CacheBenchmark {
private Cache<Long, User> caffeineCache;
private com.google.common.cache.Cache<Long, User> guavaCache;
private org.ehcache.Cache<Long, User> ehcacheCache;
@Setup
public void setup() {
caffeineCache = Caffeine.newBuilder()
.maximumSize(1_000_000)
.build();
guavaCache = CacheBuilder.newBuilder()
.maximumSize(1_000_000)
.build();
CacheManager mgr = CacheManagerBuilder.newCacheManagerBuilder().build(true);
ehcacheCache = mgr.createCache("test",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Long.class, User.class,
ResourcePoolsBuilder.heap(1_000_000)
));
}
@Benchmark
public void caffeinePut() {
for (long i = 0; i < 100_000; i++) {
caffeineCache.put(i, user);
}
}
}
三、维度一:读写吞吐量
3.1 测试场景
10 万次 PUT + 10 万次 GET,单线程,测吞吐。
3.2 测试结果
写入吞吐(ops/ms,越高越好):
| 缓存 | 写入吞吐 | 相对最高 |
|---|---|---|
| Caffeine | 142 | 100% |
| Guava Cache | 96 | 67% |
| Ehcache | 78 | 55% |
读取吞吐:
| 缓存 | 读取吞吐 | 相对最高 |
|---|---|---|
| Caffeine | 218 | 100% |
| Guava Cache | 165 | 76% |
| Ehcache | 142 | 65% |
3.3 数据分析
Caffeine 全面领先:
- 写入比 Guava 快 47%
- 读取比 Guava 快 32%
- 比 Ehcache 快 50%+
为什么 Caffeine 更快?
- RingBuffer 替代 ConcurrentHashMap:Caffeine 用
BoundedBuffer记录读访问,避免 Guava 的并发竞争 - 异步维护窗口:淘汰操作异步执行,不阻塞读写
- 无锁读:读操作不更新 LRU 链表(用 TinyLFU 队列异步处理)
Guava 为什么慢?
Guava 用 Segment 分段锁 + 同步维护 LRU 链表,并发写时锁竞争严重。
3.4 多线程压测
10 线程并发,10 万次操作:
| 缓存 | 写吞吐(10 线程) | 读吞吐(10 线程) |
|---|---|---|
| Caffeine | 1,250 | 1,890 |
| Guava Cache | 480 | 1,250 |
| Ehcache | 380 | 980 |
多线程下 Caffeine 优势更明显——锁竞争对 Guava / Ehcache 影响大。
3.5 结论
读写吞吐:Caffeine > Guava Cache > Ehcache,Caffeine 多线程下优势更明显。
四、维度二:内存占用
4.1 测试场景
存 100 万条 User(每条约 1KB),测各缓存的总内存占用。
4.2 测试结果
| 缓存 | 100 万条目内存 | 单条目开销 | 相对最低 |
|---|---|---|---|
| Caffeine | 1.2 GB | 1.2KB | 100% |
| Guava Cache | 1.6 GB | 1.6KB | 133% |
| Ehcache | 1.8 GB | 1.8KB | 150% |
4.3 数据分析
Caffeine 内存最省:
- 比 Guava 省 25%
- 比 Ehcache 省 33%
为什么?
- Caffeine 用更紧凑的数据结构:
BoundedLocalCache内部用ConcurrentHashMap但去除了很多冗余字段 - TinyLFU 而非 LRU 链表:LRU 要维护双向链表(前后指针),每个节点多 16 字节
- Ehcache 还要存元数据:序列化器、ClassLoader 等额外开销
4.4 启用 Caffeine 的 off-heap 模式
Caffeine 还能进一步省内存——用 Off-heap 存储(通过 LongAdder):
// off-heap 模式(需要 Caffeine 3.x+)
Cache<Long, User> cache = Caffeine.newBuilder()
.maximumSize(1_000_000)
.executor(ForkJoinPool.commonPool())
.build();
实测 off-heap 模式可再省 20-30% 内存,但要引入额外的 native 依赖。
4.5 结论
内存占用:Caffeine(1.2GB)< Guava Cache(1.6GB)< Ehcache(1.8GB),Caffeine 省 25-33%。
五、维度三:过期策略灵活性
5.1 过期策略分类
| 策略 | 含义 | 典型场景 |
|---|---|---|
| TTL(Time To Live) | 写入后固定时间过期 | 配置数据 |
| TTI(Time To Idle) | 最后访问后固定时间过期 | 热点数据 |
| 可变过期 | 每个条目单独的过期时间 | 不同缓存项不同生命周期 |
| 自定义过期 | 自定义过期计算逻辑 | 复杂业务规则 |
5.2 三家支持对比
| 策略 | Caffeine | Guava Cache | Ehcache |
|---|---|---|---|
| TTL(写后过期) | ✅ | ✅ | ✅ |
| TTI(访问后过期) | ✅ | ✅ | ✅ |
| TTL + TTI 组合 | ✅ | ❌ | ✅ |
| 可变过期 | ✅ | ❌ | ✅ |
| 自定义过期 | ✅ | ❌ | ✅ |
5.3 代码示例
Caffeine(最灵活)
// 1. TTL
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
// 2. TTI
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
// 3. TTL + TTI 组合
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS) // 写后 1 小时强制过期
.expireAfterAccess(10, TimeUnit.MINUTES) // 10 分钟没访问也过期
.build();
// 4. 可变过期(每个条目不同 TTL)
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, User>() {
@Override
public long expireAfterCreate(String key, User user, long currentTime) {
// VIP 用户缓存 1 小时,普通用户 5 分钟
if (user.isVip()) {
return TimeUnit.HOURS.toNanos(1);
}
return TimeUnit.MINUTES.toNanos(5);
}
@Override
public long expireAfterUpdate(String key, User user, long currentTime, long currentDuration) {
return currentDuration; // 更新后保持原 TTL
}
@Override
public long expireAfterRead(String key, User user, long currentTime, long currentDuration) {
return currentDuration; // 读不重置 TTL
}
})
.build();
Guava Cache(最简单)
// 只支持全局 TTL / TTI,不支持每条目单独
Cache<String, User> cache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 全局 TTL
// .expireAfterAccess(10, TimeUnit.MINUTES) // 二选一,不能组合
.build();
Guava 不支持可变过期,想给每个条目不同 TTL 要绕弯路(用 Map<key, expireTime> 自己管理)。
Ehcache
Cache<Long, User> cache = cacheManager.createCache("userCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Long.class, User.class,
ResourcePoolsBuilder.heap(10000)
)
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(
Duration.ofMinutes(10))) // TTL
.build());
// 可变过期
Cache<Long, User> cache = cacheManager.createCache("userCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Long.class, User.class,
ResourcePoolsBuilder.heap(10000)
)
.withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(
Duration.ofMinutes(10))) // TTI
.build());
5.4 结论
过期策略灵活性:Caffeine = Ehcache > Guava Cache
Caffeine 的可变过期(Expiry 接口)是其杀手锏,能实现"VIP 用户缓存 1 小时,普通用户 5 分钟"这种业务场景。
六、维度四:淘汰算法效率
这是 Caffeine 真正"碾压"对手的地方。
6.1 三种淘汰算法
LRU(Least Recently Used)
访问历史链表:
A → B → C → D → E
↑
最近访问
缓存满时淘汰 E(最久未访问)
问题:
- 一次扫描就清空缓存("缓存污染")
- 不区分访问频率
Guava Cache 用 LRU 的变种(Segment + LRU),但还是 LRU 思路。
LFU(Least Frequently Used)
访问频率统计:
A: 100 次 ← 高频
B: 3 次 ← 低频
C: 50 次
缓存满时淘汰 B(访问次数最少)
问题:
- 历史频率高的永远占着("频率衰减"问题)
- 历史数据污染:很久前的访问也算
W-TinyLFU(Caffeine 用的)
Caffeine 用的是 W-TinyLFU 算法——LRU + LFU 的现代化升级:
┌────────────────────┐
新访问数据 ───────→ │ Window(1%) │ ← 新数据先进窗口
│ LRU 淘汰 │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ TinyLFU 准入 │ ← 比较新数据 vs 老数据
│ 频率 + 新鲜度 │
└─────────┬──────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Probation │ │ Protected│ │ Eden │
│ (20%) │ │ (80%) │ │ │
│ LRU │ │ LRU │ │ │
└──────────┘ └──────────┘ └──────────┘
W-TinyLFU 优势:
- 频率衰减:用 Count-Min Sketch 算法,老访问权重随时间衰减
- 抗扫描:扫描场景下,W-TinyLFU 能保留高频数据
- 新数据友好:Window 区给新数据机会
6.2 命中率对比
测试场景:Zipf 分布(80% 访问集中在 20% 数据),100 万 Key,缓存 10 万 Key:
| 缓存 | 命中率 | 相对最高 |
|---|---|---|
| Caffeine | 91.2% | 100% |
| Guava Cache | 84.5% | 93% |
| Ehcache | 82.1% | 90% |
扫描场景:100 万次扫描访问 + 之后回到正常访问:
| 缓存 | 扫描后命中率 |
|---|---|
| Caffeine | 89% |
| Guava Cache | 65%(缓存被污染) |
| Ehcache | 62% |
扫描场景下 Caffeine 优势巨大——LRU 直接被冲垮。
6.3 算法对比
| 算法 | 命中率 | 抗扫描 | 实现复杂度 |
|---|---|---|---|
| LRU(Guava) | 中 | 差 | 低 |
| LFU | 中高 | 中 | 中 |
| W-TinyLFU(Caffeine) | 高 | 强 | 高 |
6.4 结论
淘汰算法:Caffeine 完胜
- W-TinyLFU 命中率比 LRU 高 8-30%
- 扫描场景命中率是 LRU 的 1.3-1.4 倍
七、维度五:Spring Cache 集成便捷度
7.1 Spring Cache 简介
Spring 提供了 @Cacheable、@CachePut、@CacheEvict 注解,业务代码无感知缓存:
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
return userMapper.selectById(id); // 只在缓存未命中时执行
}
三种缓存都能集成 Spring Cache,但便捷度差异大。
7.2 集成对比
Caffeine(最简单)
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
配置:
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=10000,expireAfterWrite=10m
或者用 Java Config:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES));
return manager;
}
}
配置量:2 个依赖 + 1 行 yaml + 1 个 Bean = 完成。
Guava Cache
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
配置:Spring Boot 2.x 之后 Guava Cache 不再是默认实现,需要手写:
@Bean
public CacheManager cacheManager() {
GuavaCacheManager manager = new GuavaCacheManager();
manager.setCacheBuilder(CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES));
return manager;
}
配置量:2 个依赖 + 1 个 Bean,但需要自己处理 Spring Boot 兼容性。
Ehcache
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.10.8</version>
<classifier>jakarta</classifier>
</dependency>
配置:还要写一个 ehcache.xml:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
<cache alias="users">
<key-type>java.lang.Long</key-type>
<value-type>com.example.User</value-type>
<expiry>
<ttl unit="minutes">10</ttl>
</expiry>
<resources>
<heap unit="entries">10000</heap>
<disk unit="MB">500</disk>
</resources>
</cache>
</config>
Java Config:
@Bean
public CacheManager cacheManager() {
return new JCacheCacheManager(
new CachingProvider().getCacheManager(
getClass().getResource("/ehcache.xml").toURI(),
getClass().getClassLoader()));
}
配置量:依赖 + XML 配置 + Java Config,最繁琐。
7.3 对比
| 维度 | Caffeine | Guava Cache | Ehcache |
|---|---|---|---|
| 依赖个数 | 2 | 2 | 2 |
| 配置代码 | 1 行 yaml | 1 个 Bean | 1 个 XML + 1 个 Bean |
| Spring Boot 默认 | ✅(5.0+) | ❌ | ❌ |
| 文档丰富度 | 高 | 中 | 中 |
| 综合便捷度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
7.4 结论
Spring Cache 集成:Caffeine > Guava Cache > Ehcache
Caffeine 是 Spring Boot 5.0+ 的默认实现,零配置即可使用。
八、维度六:异步加载支持
8.1 为什么需要异步加载
传统 Cache.get(key, loader) 是同步的:
// 同步加载:loader 阻塞当前线程
User user = cache.get(id, () -> userMapper.selectById(id));
高并发下,多个线程同时请求同一个未命中的 key:
缓存 miss → 多线程同时调用 loader → 数据库被打爆
异步加载能解决:
缓存 miss → 只有一个线程调用 loader,其他线程等待 → 数据库压力小
8.2 Caffeine 的异步 API
Caffeine 提供原生的 AsyncCache:
AsyncCache<Long, User> cache = Caffeine.newBuilder()
.maximumSize(10000)
.buildAsync();
// 异步加载
CompletableFuture<User> future = cache.get(id, k -> {
return CompletableFuture.supplyAsync(
() -> userMapper.selectById(k),
executor);
});
// 非阻塞获取
future.thenAccept(user -> System.out.println(user));
关键特性:
- 同一个 key 的并发请求只触发一次 loader
- loader 返回
CompletableFuture,不阻塞调用线程 - 加载结果自动缓存
8.3 Guava Cache 的异步
Guava 的异步比较弱:
LoadingCache<Long, User> cache = CacheBuilder.newBuilder()
.build(CacheLoader.from(this::loadUser));
// 同步
User user = cache.get(id);
// 异步(基于 ListenableFuture)
ListenableFuture<User> future = cache.get(id, () -> loadUser(id));
Futures.addCallback(future, new FutureCallback<User>() {
@Override
public void onSuccess(User result) { /* ... */ }
@Override
public void onFailure(Throwable t) { /* ... */ }
}, executor);
问题:
ListenableFuture不如CompletableFuture好用- 需要包一层
Futures.transform,代码冗长 - 不能原生组合多个异步加载
8.4 Ehcache 的异步
Ehcache 3.x 提供了 CacheLoaderWriter:
CacheConfiguration<Long, User> config = CacheConfigurationBuilder
.newCacheConfigurationBuilder(Long.class, User.class, ResourcePoolsBuilder.heap(10000))
.withLoaderWriter(new CacheLoaderWriter<Long, User>() {
@Override
public User load(Long key) {
return userMapper.selectById(key);
}
@Override
public void write(Long key, User value) { /* ... */ }
@Override
public void delete(Long key) { /* ... */ }
})
.build();
问题:
CacheLoaderWriter是同步的- 没有原生
CompletableFuture支持 - 要异步得自己包一层
8.5 对比
| 维度 | Caffeine | Guava Cache | Ehcache |
|---|---|---|---|
| AsyncCache 原生支持 | ✅ | ❌ | ❌ |
| CompletableFuture API | ✅ | ❌(ListenableFuture) | ❌ |
| 同 key 并发去重 | ✅ | ✅ | ✅ |
| 异步刷新 | ✅ | ❌ | ❌ |
| 综合 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ |
8.6 实战:Caffeine 异步加载多个 Key
AsyncCache<Long, User> userCache = Caffeine.newBuilder()
.maximumSize(10000)
.buildAsync(this::loadUser);
// 批量异步加载
List<Long> userIds = Arrays.asList(1L, 2L, 3L);
CompletableFuture<?>[] futures = userIds.stream()
.map(id -> userCache.get(id).thenAccept(u -> {
// 处理每个用户
}))
.toArray(CompletableFuture[]::new);
// 等所有完成
CompletableFuture.allOf(futures).join();
异步加载多个 key,互不阻塞。
8.7 结论
异步加载:Caffeine >> Guava Cache > Ehcache
Caffeine 的 AsyncCache 是现代化设计,原生支持 CompletableFuture,和 JDK 8+ 的异步编程无缝衔接。
九、综合对比
9.1 六维评分
| 维度 | Caffeine | Guava Cache | Ehcache |
|---|---|---|---|
| 读写吞吐 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 内存占用 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 过期策略 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| 淘汰算法 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Spring 集成 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 异步加载 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ |
| 综合 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
9.2 关键数据汇总
| 指标 | Caffeine | Guava Cache | Ehcache |
|---|---|---|---|
| 读吞吐(单线程) | 218 ops/ms | 165 ops/ms | 142 ops/ms |
| 写吞吐(单线程) | 142 ops/ms | 96 ops/ms | 78 ops/ms |
| 100 万条目内存 | 1.2 GB | 1.6 GB | 1.8 GB |
| 命中率(Zipf) | 91.2% | 84.5% | 82.1% |
| 扫描后命中率 | 89% | 65% | 62% |
| 异步 API | ✅ AsyncCache | ⚠ ListenableFuture | ❌ |
9.3 适用场景
| 场景 | 推荐 | 原因 |
|---|---|---|
| Spring Boot 项目 | Caffeine | 默认实现,零配置 |
| 高并发读 | Caffeine | 吞吐最高 |
| 内存敏感 | Caffeine | 内存最省 |
| 复杂过期策略 | Caffeine | 可变过期 |
| 需要持久化到磁盘 | Ehcache | 唯一支持 |
| 需要分布式缓存 | Ehcache | 配 Terracotta |
| 老项目已用 Guava | Guava Cache | 别折腾 |
| 新项目 | Caffeine | 现代化,未来 |
十、从 Guava Cache 迁移到 Caffeine
10.1 为什么迁移
Guava Cache 官方建议:
"Caffeine 是 Guava Cache 的继任者,新项目应该用 Caffeine。"
Guava Cache 进入维护模式,只修 bug,不加新功能。
10.2 迁移成本极低
Caffeine 的 API 和 Guava Cache 几乎一样:
// Guava Cache
Cache<String, User> guava = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
// Caffeine
Cache<String, User> caffeine = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
只需替换 import:
// 替换前
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
// 替换后
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
10.3 LoadingCache 迁移
// Guava
LoadingCache<Long, User> guava = CacheBuilder.newBuilder()
.build(new CacheLoader<Long, User>() {
@Override
public User load(Long key) {
return userMapper.selectById(key);
}
});
// Caffeine
LoadingCache<Long, User> caffeine = Caffeine.newBuilder()
.build(key -> userMapper.selectById(key));
10.4 迁移收益
| 维度 | 收益 |
|---|---|
| 性能 | 读写快 30-50% |
| 内存 | 省 25% |
| 命中率 | 提升 8% |
| 功能 | 异步 API + 可变过期 |
| 维护 | 跟得上 JDK 版本 |
迁移成本:替换 import + 1 小时测试。收益:永久。
十一、常见问题
11.1 Caffeine 适合多大的缓存
- 百万级以内:完全没问题,性能最优
- 千万级:可以,但建议用
off-heap模式省内存 - 亿级:考虑 Redis 或堆外缓存(如 Chronon Map)
11.2 Caffeine 是否支持持久化
不支持。Caffeine 是纯堆内缓存,重启即丢失。
需要持久化用 Ehcache(堆外/磁盘)。
11.3 Caffeine 是否支持分布式
不支持原生分布式。Caffeine 是单机缓存。
需要分布式用 Redis / Hazelcast。但可以用 Caffeine 做一级缓存 + Redis 做二级缓存:
public User getUser(Long id) {
// L1: Caffeine
User user = caffeineCache.getIfPresent(id);
if (user != null) return user;
// L2: Redis
user = redisTemplate.opsForValue().get("user:" + id);
if (user != null) {
caffeineCache.put(id, user);
return user;
}
// L3: DB
user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set("user:" + id, user, 1, TimeUnit.HOURS);
caffeineCache.put(id, user);
}
return user;
}
11.4 如何监控 Caffeine
Caffeine 提供原生 stats:
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10000)
.recordStats() // 开启统计
.build();
CacheStats stats = cache.stats();
System.out.println("命中率:" + stats.hitRate());
System.out.println("加载平均耗时:" + stats.averageLoadPenalty());
System.out.println("淘汰数:" + stats.evictionCount());
集成到 Micrometer:
@Autowired
private MeterRegistry registry;
@PostConstruct
public void bindMetrics() {
CaffeineCacheMetrics.monitor(registry, cache, "user_cache");
}
Grafana 直接能看到命中率、加载时间、淘汰数。
11.5 Ehcache 何时还有价值
- 需要磁盘持久化:缓存能在重启后恢复
- 堆外内存:避开 GC 压力
- 企业级特性:JTA 事务、分布式锁
但这些都是少数场景。99% 的本地缓存需求 Caffeine 都能解决。
十二、总结
最终结论
本地缓存三巨头选型:
├── 99% 场景 → Caffeine(默认)
├── 需要持久化 → Ehcache
├── 老项目用 Guava Cache → 迁移到 Caffeine
└── 需要分布式 → 不用本地缓存,用 Redis
六维对比速查表
| 维度 | Caffeine | Guava Cache | Ehcache |
|---|---|---|---|
| 读写吞吐 | 🏆 | 中 | 低 |
| 内存占用 | 🏆 | 中 | 高 |
| 过期策略 | 🏆 | 弱 | 强 |
| 汰淘算法 | 🏆 W-TinyLFU | LRU | LRU |
| Spring 集成 | 🏆 | 中 | 弱 |
| 异步加载 | 🏆 | 弱 | 无 |
一句话
Caffeine 全维碾压,是 Guava Cache 的现代化继任者。除非需要磁盘持久化用 Ehcache,否则都选 Caffeine。
关键数据
- Caffeine 读写吞吐比 Guava 快 30-50%
- Caffeine 内存比 Guava 省 25%
- Caffeine 命中率比 Guava 高 8%
- Caffeine 扫描后命中率是 Guava 的 1.37 倍
- Caffeine 是 Spring Boot 5.0+ 默认缓存实现
给团队的建议
| 项目状态 | 建议 |
|---|---|
| 新项目 | 直接用 Caffeine |
| 老项目用 Guava | 半年内迁移到 Caffeine |
| 老项目用 Ehcache 做堆内 | 迁移到 Caffeine |
| 老项目用 Ehcache 做磁盘 | 保留,Caffeine 不支持 |
| 用 Redis 做分布式 | Caffeine 做一级缓存兜底 |
互动话题:你们项目用哪个本地缓存?有没有踩过 Guava Cache 的坑?欢迎留言讨论!
参考资料
- Caffeine 官方文档
- Caffeine W-TinyLFU 论文
- Guava Cache 文档
- Ehcache 3.x 文档
- Spring Cache 集成
- 本地缓存性能对比 Benchmark
标题:本地缓存三巨头:Caffeine vs Guava Cache vs Ehcache——到底谁才是王者
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/08/13/1786164270046.html
公众号:服务端技术精选
- 引言
- 一、三巨头简介
- 1.1 Guava Cache
- 1.2 Ehcache
- 1.3 Caffeine
- 二、六维实测
- 2.1 测试环境
- 2.2 测试代码骨架
- 三、维度一:读写吞吐量
- 3.1 测试场景
- 3.2 测试结果
- 3.3 数据分析
- 3.4 多线程压测
- 3.5 结论
- 四、维度二:内存占用
- 4.1 测试场景
- 4.2 测试结果
- 4.3 数据分析
- 4.4 启用 Caffeine 的 off-heap 模式
- 4.5 结论
- 五、维度三:过期策略灵活性
- 5.1 过期策略分类
- 5.2 三家支持对比
- 5.3 代码示例
- Caffeine(最灵活)
- Guava Cache(最简单)
- Ehcache
- 5.4 结论
- 六、维度四:淘汰算法效率
- 6.1 三种淘汰算法
- LRU(Least Recently Used)
- LFU(Least Frequently Used)
- W-TinyLFU(Caffeine 用的)
- 6.2 命中率对比
- 6.3 算法对比
- 6.4 结论
- 七、维度五:Spring Cache 集成便捷度
- 7.1 Spring Cache 简介
- 7.2 集成对比
- Caffeine(最简单)
- Guava Cache
- Ehcache
- 7.3 对比
- 7.4 结论
- 八、维度六:异步加载支持
- 8.1 为什么需要异步加载
- 8.2 Caffeine 的异步 API
- 8.3 Guava Cache 的异步
- 8.4 Ehcache 的异步
- 8.5 对比
- 8.6 实战:Caffeine 异步加载多个 Key
- 8.7 结论
- 九、综合对比
- 9.1 六维评分
- 9.2 关键数据汇总
- 9.3 适用场景
- 十、从 Guava Cache 迁移到 Caffeine
- 10.1 为什么迁移
- 10.2 迁移成本极低
- 10.3 LoadingCache 迁移
- 10.4 迁移收益
- 十一、常见问题
- 11.1 Caffeine 适合多大的缓存
- 11.2 Caffeine 是否支持持久化
- 11.3 Caffeine 是否支持分布式
- 11.4 如何监控 Caffeine
- 11.5 Ehcache 何时还有价值
- 十二、总结
- 最终结论
- 六维对比速查表
- 一句话
- 关键数据
- 给团队的建议
- 参考资料
评论