Java 中的 Caffeine 缓存详解

一、简介

Caffeine 是一个基于 Java 8 开发的高性能、近乎最优的本地缓存库,由 Google 工程师 Ben Manes 受 Guava Cache 启发而设计,并在其基础上大幅优化了命中率、内存效率和并发性能。它被广泛用于替代 Guava Cache,成为 Spring Boot 2.x 默认的本地缓存实现(spring-boot-starter-cache 中的 CaffeineCacheManager)。

二、Caffeine 的核心特点

  1. 高性能:读写性能接近 ConcurrentHashMap,命中率和淘汰策略经过精心调优。
  2. 丰富的驱逐策略:基于大小、时间、引用(软/弱)以及自定义权重。
  3. 异步加载:支持 CacheLoader 和 AsyncCacheLoader,可以异步加载缓存值。
  4. 统计功能:提供命中率、加载成功率、平均加载耗时等指标。
  5. 灵活过期:支持创建后固定时间过期、最后一次访问后过期、最后一次写入后过期、自定义过期策略。
  6. 弱/软引用:允许键或值使用软引用、弱引用,便于 GC 回收。
  7. 与 Spring 无缝集成:通过 @Cacheable 注解即可使用。

三、快速入门

3.1 Maven 依赖

xml 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

3.2 基本用法

java 复制代码
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class CaffeineDemo {
    public static void main(String[] args) {
        // 构建缓存
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)          // 最大条目数
                .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
                .recordStats()             // 开启统计
                .build();
        
        // 存入
        cache.put("key1", "value1");
        
        // 获取(若不存在则返回 null)
        String value = cache.getIfPresent("key1");
        
        // 获取(若不存在则调用函数加载)
        String value2 = cache.get("key2", k -> loadFromDB(k));
    }
    
    private static String loadFromDB(String key) {
        // 模拟从数据库加载
        return "dbValue";
    }
}

四、核心配置详解

4.1 容量限制

方法 说明
maximumSize(long size) 限制缓存最大条目数,采用 W-TinyLFU 淘汰算法
maximumWeight(long weight) 配合 weigher 设置最大权重(例如不同条目权重不同)
weigher(Weigher<K, V> weigher) 自定义权重计算函数

示例:

java 复制代码
Cache<String, String> cache = Caffeine.newBuilder()
        .maximumWeight(1000)
        .weigher((String key, String value) -> value.length())
        .build();

4.2 过期策略

方法 说明
expireAfterWrite(long, TimeUnit) 写入后经过固定时间过期
expireAfterAccess(long, TimeUnit) 最后一次访问(读或写)后经过固定时间过期
expireAfter(Expiry<K, V>) 自定义过期策略,可针对每个条目单独计算过期时间
refreshAfterWrite(long, TimeUnit) 写入后经过固定时间,下一次访问时异步刷新(不阻塞读取)

注意:

  • expireAfterWrite 和 expireAfterAccess 互斥,不能同时使用。
  • refreshAfterWrite 仅当缓存被访问时触发刷新,且刷新是异步的(需要搭配 buildAsync 或 CacheLoader)。

自定义过期示例:

java 复制代码
Cache<String, String> cache = Caffeine.newBuilder()
        .expireAfter(new Expiry<String, String>() {
            @Override
            public long expireAfterCreate(String key, String value, long currentTime) {
                return TimeUnit.SECONDS.toNanos(30); // 创建后30秒过期
            }
            @Override
            public long expireAfterUpdate(String key, String value, long currentTime, long currentDuration) {
                return currentDuration; // 更新后不重置
            }
            @Override
            public long expireAfterRead(String key, String value, long currentTime, long currentDuration) {
                return currentDuration; // 读取后不重置
            }
        })
        .build();

4.3 刷新策略(异步加载)

java 复制代码
LoadingCache<String, String> cache = Caffeine.newBuilder()
        .refreshAfterWrite(1, TimeUnit.MINUTES) // 写入1分钟后,下次访问触发异步刷新
        .build(key -> loadFromDB(key));         // 同步加载函数

注意:刷新不会抛出异常到调用方,如果刷新失败,会继续返回旧值,并在后台重试。

4.4 统计功能

java 复制代码
Cache<String, String> cache = Caffeine.newBuilder()
        .recordStats()   // 开启统计
        .build();
// 使用一段时间后...
CacheStats stats = cache.stats();
System.out.println(stats.hitRate());          // 命中率
System.out.println(stats.loadSuccessCount()); // 成功加载次数
System.out.println(stats.averageLoadPenalty()); // 平均加载耗时(纳秒)

五、缓存加载方式

Caffeine 提供了三种缓存接口:

5.1 Cache(手动加载)

java 复制代码
Cache<K, V> cache = Caffeine.newBuilder().build();
// 手动 put/get
V value = cache.getIfPresent(key);
cache.put(key, value);
cache.invalidate(key);

5.2 LoadingCache(同步加载)

需要提供 CacheLoader,当 key 不存在时自动调用 load 方法。

java 复制代码
LoadingCache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1000)
        .build(key -> loadFromDB(key));   // 或实现 CacheLoader

String value = cache.get("key");  // 若不存在则调用 load
Map<String, String> all = cache.getAll(keys); // 批量加载

5.3 AsyncLoadingCache(异步加载)

异步加载不会阻塞调用线程,适用于加载耗时较长的场景。

java 复制代码
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1000)
        .buildAsync(key -> CompletableFuture.supplyAsync(() -> loadFromDB(key)));

CompletableFuture<String> future = cache.get("key");
future.thenAccept(System.out::println);

六、驱逐算法:W-TinyLFU

Caffeine 采用 W-TinyLFU(Window Tiny Least Frequently Used)算法,结合了 LRU 和 LFU 的优点,同时解决了传统算法的痛点:

  1. LRU (最近最少使用):只看时间,不看频率。容易被冷数据扫描污染,热点易被冲掉。
  2. LFU (最不经常使用):只看频率,不看时间。新数据冷启动困难(刚加入频率低易被删),历史高频数据长期占内存。

核心思想:新数据靠 Window 保护,老数据靠频率留存,冷热智能分离。

  1. 窗口缓存(Window Cache):

    • 小容量(约总容量 1%),纯 LRU 策略
    • 作用:接纳新数据,解决冷启动问题,避免新数据刚进就被淘汰
  2. Main Cache (主缓存,TinyLFU)

    • 大容量,核心存储区
    • 用 Count-Min Sketch 概率结构极低内存开销记录访问频率
    • 定期频率衰减(如每小时减半),让旧高频数据自然降温
    • 淘汰时:频率最低 + 最久未用 者优先
  3. PD (Probation Denominator) 候选区

    • 临时缓冲区,用于 Window 与 Main 之间的替换权衡

七、高级特性

7.1 异步监听与移除监听

java 复制代码
Cache<String, String> cache = Caffeine.newBuilder()
        .removalListener((String key, String value, RemovalCause cause) -> {
            System.out.println("Removed " + key + " because " + cause);
        })
        .build();

// 手动移除
cache.invalidate("key1");

// 移除原因:EXPLICIT(手动)、REPLACED(替换)、COLLECTED(GC回收)、EXPIRED(过期)、SIZE(容量淘汰)

7.2 弱引用与软引用

java 复制代码
// 键使用弱引用(允许 GC 回收键)
Cache<String, String> cache = Caffeine.newBuilder()
        .weakKeys()   // 键使用 WeakReference
        .weakValues() // 值使用 WeakReference
        .build();

// 值使用软引用(适合内存敏感的缓存)
Cache<String, String> cache2 = Caffeine.newBuilder()
        .softValues()
        .build();

7.3 写入外部存储(Write-Through)

可以通过 CacheWriter 实现同步写入外部存储(如数据库),确保缓存和存储的一致性。

java 复制代码
Cache<String, String> cache = Caffeine.newBuilder()
        .writer(new CacheWriter<String, String>() {
            @Override
            public void write(String key, String value) {
                // 写入数据库
                db.put(key, value);
            }
            @Override
            public void delete(String key, String value, RemovalCause cause) {
                // 从数据库删除
                db.delete(key);
            }
        })
        .build();

7.4 与 Spring Cache 集成

Spring Boot 2.x 默认的 CacheManager 实现为 CaffeineCacheManager。

yaml 复制代码
# application.yml
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=500,expireAfterWrite=60s

或在 Java 配置中:

java 复制代码
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "products");
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(10, TimeUnit.MINUTES));
        return cacheManager;
    }
}

使用时:

java 复制代码
@Service
public class UserService {
    @Cacheable(value = "users", key = "#id")
    public User getUser(Long id) {
        return userRepository.findById(id);
    }
}

八、其他本地缓存实现方案

8.1 Spring Cache

Spring 从 3.1 开始提供了 Cache 和 CacheManager 接口,统一了缓存编程模型。通过注解(@Cacheable、@CacheEvict、@CachePut、@Caching、@CacheConfig)可以透明地使用不同缓存技术,而无需修改业务代码。

核心注解:

注解 作用
@Cacheable 方法执行前先查缓存,有则返回;无则执行方法并将结果存入缓存
@CacheEvict 触发时清除一条或多条缓存(常用于更新/删除操作)
@CachePut 总是执行方法,并将结果存入缓存(用于更新缓存)
@Caching 组合多个缓存操作
@CacheConfig 类级别共享缓存配置(如 cacheNames、keyGenerator)

使用步骤:

  1. 启用缓存:@EnableCaching
  2. 声明 CacheManager Bean(根据具体实现)
  3. 在业务方法上添加缓存注解

8.2 Guava Cache

Guava 是 Google 提供的 Java 核心库,其中的 Cache 实现是 Caffeine 的前身,使用 LRU 淘汰算法。在 Spring Boot 1.x 中是默认本地缓存,2.x 后被 Caffeine 取代。

特点:

  • 成熟稳定,文档丰富
  • 支持过期、大小限制、移除监听器
  • 性能逊于 Caffeine(约 3~5 倍差距)

集成方式:

xml 复制代码
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

配置示例:

java 复制代码
@Bean
public CacheManager cacheManager() {
    GuavaCacheManager cacheManager = new GuavaCacheManager("users");
    cacheManager.setCacheBuilder(CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .recordStats());
    return cacheManager;
}

8.3 EhCache

EhCache 是一个纯 Java 的分布式缓存框架,历史悠久,功能非常全面:支持磁盘溢出、多级缓存、事务、JMX 监控等。它既可以作为本地缓存,也支持分布式(需配合 Terracotta)。

特点:

  • 支持内存+磁盘两级存储(适合超大缓存)
  • 支持缓存事件监听器
  • 支持 XML 或 Java 配置
  • 性能中等,略低于 Caffeine

集成方式:

xml 复制代码
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
<!-- Spring Boot 对 EhCache 的自动配置支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

配置文件(ehcache.xml):

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">
        <resources>
            <heap unit="entries">1000</heap>
            <offheap unit="MB">10</offheap>
        </resources>
        <expiry>
            <ttl unit="minutes">10</ttl>
        </expiry>
    </cache>
</config>

Spring 配置:

yaml 复制代码
spring:
  cache:
    type: ehcache
    ehcache:
      config: classpath:ehcache.xml

8.4 Simple Cache(基于 ConcurrentHashMap)

Spring Boot 自动配置的"简单缓存",当没有添加其他缓存库时,默认使用 ConcurrentHashMap 作为缓存存储。仅适合开发测试,生产环境不推荐,因为没有过期、淘汰策略,内存会无限增长。

特点:

  • 无过期、无淘汰、无统计
  • 基于 ConcurrentHashMap,性能高
  • 无需额外依赖

配置(默认即启用):

java 复制代码
// 自动配置 SimpleCacheManager
@Bean
public CacheManager cacheManager() {
    return new SimpleCacheManager();
}

8.5 自定义缓存实现

如果上述方案均不满足需求(例如需要 TTL 分桶、异步刷新等特殊逻辑),可以自行实现 Cache 和 CacheManager 接口,或继承 AbstractValueAdaptingCache。

示例(基于 Caffeine 定制):

java 复制代码
public class CustomCaffeineCache extends CaffeineCache {
    public CustomCaffeineCache(String name, Caffeine<Object, Object> caffeine) {
        super(name, caffeine.build());
    }
    // 可覆盖 get、put 等方法添加额外逻辑
}

九、选型标准

  1. 90% Spring Boot 项目 → Spring Cache + Caffeine
  2. 轻量小工具 → ConcurrentHashMap
  3. 需要磁盘持久化 → Ehcache
  4. 高并发、精细控制 → Caffeine 原生 LoadingCache
  5. 老项目维护 → Guava

Spring Boot 本地缓存首选:Spring Cache + Caffeine

简单场景用 ConcurrentHashMap,复杂高性能场景用 Caffeine 原生 API。

相关推荐
froginwe112 小时前
JSP 发送邮件
开发语言
沐雪轻挽萤2 小时前
15. C++17新特性-std::string_view
java·开发语言·c++
不考研当牛马2 小时前
python 第21课 基础完结(UDP套接字)
开发语言·python·udp
wearegogog1232 小时前
光伏发电系统最大功率跟踪(MPPT)算法 Matlab 实现指南
开发语言·算法·matlab
小小码农Come on2 小时前
QML怎么使用C++多线程编程
开发语言·c++
devilnumber2 小时前
java的NIO框架Netty、Mina、Grizzly 和 Jetty 四种对比
java·nio·java面试·jetty
努力进修2 小时前
【java-数据结构】Java优先级队列揭秘:堆的力量让数据处理飞起来
java·开发语言·数据结构
亚历克斯神2 小时前
Java 代码质量与静态分析:2026 实战指南
java·spring·微服务
@hhr2 小时前
使用java对接火山方舟doubao-seedance-1.5-pro 模型进行视频生成
java·python·音视频