Caffeine:最快的本地缓存

本地缓存用过 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 倍,代码几乎不用改。

相关推荐
YOU OU2 小时前
Redis基础常识与命令
数据库·redis·缓存
吳所畏惧7 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh
Y38153266212 小时前
SERP API + Redis 缓存层:4 种方案对比与选型
数据库·redis·缓存
海兰14 小时前
【高速缓存】RedisVL 高级查询(全文搜索、混合搜索和 多向量搜索)
数据库·人工智能·redis·缓存
流星白龙1 天前
【Redis】4.基本全局命令
数据库·redis·缓存
流星白龙1 天前
【Redis】3.Redis安装与命令行客户端
数据库·redis·缓存
我不想名字重复2 天前
redis缓存和数据库数据保持一致
数据库·redis·缓存
时空无限2 天前
vllm 大模型启动缓存相关环境变量 export
linux·缓存·vllm
流星白龙2 天前
【Redis】6.计数其他命令
数据库·redis·缓存
正儿八经的少年2 天前
redis 的大 key 和热 key 详解
数据库·redis·缓存