本地缓存用过 Guava Cache?但 Caffeine 比它快5-10 倍,而且 API 几乎一样。
Spring Boot 2.x 默认就用的 Caffeine 作为本地缓存。
为什么选 Caffeine
Guava Cache: ~300,000 ops/s
Caffeine: ~3,000,000 ops/s
性能提升: 10 倍!
Caffeine 为什么这么快:
- 内部用 RingBuffer 存储(和 Disruptor 一样的设计)
- 无锁设计,读操作几乎无锁
- 热点探测,自动提升热点数据的优先级
基础配置
依赖
xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
Spring Boot 2.x 默认带了,不用单独加。
最简单的用法
java
Cache<String, User> cache = Caffeine.newBuilder()
.build();
// 写入
cache.put("user:1", new User(1L, "张三"));
// 读取
User user = cache.getIfPresent("user:1"); // 不存在返回 null
// 读取,不存在则加载
User user = cache.get("user:1", () -> loadFromDb(1L));
淘汰策略
Caffeine 支持多种淘汰策略,根据场景选择。
1. 基于容量
java
// 超过 1000 条后淘汰
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(1000)
.build();
2. 基于权重
java
// 每条数据的权重不同,总权重超限后淘汰
Cache<String, User> cache = Caffeine.newBuilder()
.maximumWeight(10000)
.weigher((key, user) -> user.getName().length()) // 权重 = 名字长度
.build();
3. TTL(过期时间)
java
// 写入后 5 分钟过期
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
// 最后访问后 10 分钟过期
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
// 自定义过期策略
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, User>() {
@Override
public long expireAfterCreate(String key, User value, long currentTime) {
return TimeUnit.HOURS.toNanos(1); // 创建后 1 小时
}
@Override
public long expireAfterUpdate(String key, User value, long currentTime, long currentDuration) {
return currentDuration; // 更新后保持原有过期时间
}
@Override
public long expireAfterRead(String key, User value, long currentTime, long currentDuration) {
return currentDuration; // 读取不影响
}
})
.build();
淘汰算法
LRU(默认,最常用)
java
// 最近最少使用,淘汰最久没访问的
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats() // 开启统计
.build();
W-TinyLFU(频率统计,最优命中率)
java
// Caffeine 默认算法,比 LRU 命中率更高
// Caffeine 会记录每条数据的访问频率,定期淘汰频率最低的数据
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10000)
.recordStats()
.build();
FIFO
java
// 先进先出,按创建顺序淘汰
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(1000)
.scheduler(Scheduler.forScheduledExecutorService(executor))
.build();
异步加载
java
// 异步加载,不阻塞
AsyncCache<String, User> asyncCache = Caffeine.newBuilder()
.buildAsync();
CompletableFuture<User> future = asyncCache.get("user:1", id -> loadFromDb(id));
future.thenAccept(user -> System.out.println(user.getName()));
同步异步混用也行:
java
AsyncCache<String, User> asyncCache = Caffeine.newBuilder()
.buildAsync();
Cache<String, User> cache = asyncCache.synchronous();
统计和监控
java
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats() // 必须开启
.build();
// 获取统计信息
Stats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate()); // 0.85
System.out.println("命中次数: " + stats.hitCount()); // 8500
System.out.println("淘汰次数: " + stats.evictionCount()); // 1500
System.out.println("加载耗时: " + stats.totalLoadTime()); // 纳秒
Spring Boot 集成
配置类
java
@Configuration
public class CacheConfig {
@Bean
public Cache<String, User> userCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
}
@Bean
public Cache<String, List<Product>> productCache() {
return Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
}
}
使用
java
@Service
@RequiredArgsConstructor
public class UserService {
private final Cache<String, User> userCache;
private final UserMapper userMapper;
public User getUser(Long id) {
String key = "user:" + id;
// 查缓存
User user = userCache.getIfPresent(key);
if (user != null) {
return user;
}
// 缓存没有,查数据库
user = userMapper.selectById(id);
if (user != null) {
userCache.put(key, user);
}
return user;
}
// 更新时清缓存
@Transactional
public void updateUser(User user) {
userMapper.updateById(user);
userCache.invalidate("user:" + user.getId());
}
}
Spring Cache 注解
也可以用 @Cacheable 注解:
java
@Service
@CacheConfig(cacheNames = "users")
public class UserService {
@Cacheable(key = "#id")
public User getUser(Long id) {
return userMapper.selectById(id);
}
@CacheEvict(key = "#user.id")
public void updateUser(User user) {
userMapper.updateById(user);
}
}
yaml
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
多级缓存:本地 + Redis
实际项目中,本地缓存配合 Redis 用,既快又能保证一致性:
java
@Service
@RequiredArgsConstructor
public class UserService {
private final Cache<String, User> localCache; // Caffeine 本地缓存
private final RedisTemplate<String, User> redis; // Redis 分布式缓存
private final UserMapper userMapper;
public User getUser(Long id) {
String key = "user:" + id;
// 1. 查本地缓存
User user = localCache.getIfPresent(key);
if (user != null) {
return user;
}
// 2. 查 Redis
user = redis.opsForValue().get(key);
if (user != null) {
localCache.put(key, user); // 回填本地缓存
return user;
}
// 3. 查数据库
user = userMapper.selectById(id);
if (user != null) {
redis.opsForValue().set(key, user, 1, TimeUnit.HOURS);
localCache.put(key, user);
}
return user;
}
// 更新时清两级缓存
@Transactional
public void updateUser(User user) {
userMapper.updateById(user);
String key = "user:" + user.getId();
localCache.invalidate(key);
redis.delete(key);
}
}
缓存流程:
查询请求
↓
本地缓存命中? → 直接返回 ✅
↓ 否
Redis 命中? → 回填本地缓存 → 返回 ✅
↓ 否
查数据库 → 写入 Redis → 写入本地缓存 → 返回 ✅
为什么这么设计:
- 本地缓存快,但数据可能不一致
- Redis 保证多实例一致性,但有网络开销
- 组合起来,取长补短
和 Guava Cache 对比
| 特性 | Guava Cache | Caffeine |
|---|---|---|
| 吞吐量 | ~300K ops/s | ~3000K ops/s |
| 淘汰算法 | LRU | W-TinyLFU(更优) |
| API | 相同 | 几乎一样 |
| 过期策略 | 支持 | 支持(更丰富) |
| 统计 | 支持 | 支持(更详细) |
| 异步 | 不支持 | 支持 |
迁移成本:API 几乎一样,改个类名就行。
注意事项
1. 内存问题
本地缓存存在 JVM 堆内存,数据太大会 OOM:
java
// 设置合理大小,监控内存
.maximumSize(10000)
2. 缓存穿透
java
// 用 null 值占位,防止穿透
Cache<String, User> cache = Caffeine.newBuilder()
.build();
// 查询
User user = cache.get(key, k -> {
User u = db.find(k);
return u != null ? u : User.NULL; // null 值也缓存
});
3. 缓存一致性问题
多实例部署时,本地缓存无法跨机器同步:
- 数据更新时,广播通知所有实例清缓存
- 或者缩短 TTL,让数据自然过期
总结
Caffeine 使用场景:
- 数据变化不频繁(用户信息、配置数据)
- 读多写少
- 对性能要求高
- 需要本地缓存 + Redis 配合
Spring Boot 2.x 默认就是 Caffeine,不用配置就有不错的性能。
如果项目里还在用 Guava Cache,换成 Caffeine,性能提升 10 倍,代码几乎不用改。