你还在用Guava Cache做本地缓存吗?Spring Boot 2.x已经默认使用Caffeine作为缓存组件!作为号称**“本地缓存之王”**的高性能Java缓存库,Caffeine在性能上比Guava Cache提升数倍。本文将从基础入门到高级特性,手把手教你掌握Caffeine的使用,包括同步/异步加载、多种淘汰策略、引用驱逐、状态统计等核心功能,附带完整实战代码!


📋 文章目录


一、Caffeine简介

1.1 什么是Caffeine?

Caffeine Cache 是一个高性能的Java本地缓存库,以其卓越的性能和可扩展性赢得了 “本地缓存之王” 的称号。

1.2 与Spring Boot的关系

  • Spring Boot 1.x:默认本地缓存是 Guava Cache
  • Spring Boot 2.x:官方放弃Guava Cache,改用性能更优秀的 Caffeine 作为默认缓存组件

1.3 性能对比

官方测试报告:https://github.com/ben-manes/caffeine/wiki/Benchmarks-zh-CN

Caffeine性能对比图


二、Caffeine八大核心特点

  1. 自动加载:自动将数据加载到缓存中,支持异步加载
  2. 灵活淘汰策略:基于频次、最近访问、最大容量
  3. 智能过期:根据上次访问/写入决定过期设置
  4. 异步清理:缓存过期后自动异步清理
  5. 内存友好:考虑JVM内存管理,支持弱引用、软引用
  6. 清除通知:缓存被清理后收到相关通知
  7. 外部存储:缓存写入可传播到外部存储
  8. 统计功能:访问次数、命中率、清理个数等统计数据

三、快速入门

3.1 Maven依赖

<!-- Spring Boot Cache Starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- Caffeine Cache -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.7.0</version>
</dependency>

3.2 基础使用

Cache是核心接口,通过Caffeine类来构建实现:

public static void basicCache() throws Exception {
    // 构建Caffeine实例
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(100)                    // 设置最大缓存数量
            .expireAfterAccess(3L, TimeUnit.SECONDS)  // 3秒无访问失效
            .build();                            // 构建Cache实例

    // 写入缓存
    cache.put("mca", "www.mashibing.com");
    cache.put("baidu", "www.baidu.com");
    cache.put("spring", "www.spring.io");

    // 获取缓存
    log.info("获取缓存[getIfPresent]: mca={}", cache.getIfPresent("mca"));
    
    TimeUnit.SECONDS.sleep(5);  // 休眠5秒
    
    // 再次获取(已过期)
    log.info("获取缓存[getIfPresent]: mca={}", cache.getIfPresent("mca"));
}

运行效果

获取缓存[getIfPresent]: mca=www.mashibing.com
获取缓存[getIfPresent]: mca=null

注意getIfPresent()对于不存在的key立即返回null,不会阻塞。


四、缓存加载机制

4.1 同步加载(CacheLoader)

当缓存失效时,自动同步加载数据:

public static void loadingCache() throws Exception {
    LoadingCache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterAccess(3L, TimeUnit.SECONDS)
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    log.info("正在重新加载数据... key={}", key);
                    TimeUnit.SECONDS.sleep(1);
                    return key.toUpperCase();
                }
            });

    // 写入缓存
    cache.put("mca", "www.mashibing.com");
    cache.put("baidu", "www.baidu.com");
    
    TimeUnit.SECONDS.sleep(5);  // 等待缓存过期
    
    // 获取所有key对应的值(触发自动加载)
    List<String> keys = Arrays.asList("mca", "baidu");
    Map<String, String> map = cache.getAll(keys);
    
    map.forEach((k, v) -> 
        log.info("缓存的键: {}, 值: {}", k, v));
}

特点

  • 使用CacheLoader接口标准化加载流程
  • 支持getAll()批量加载
  • 加载过程是同步阻塞

4.2 异步加载(AsyncLoadingCache)

当多个缓存同时失效时,使用异步加载提高效率:

public static void asyncLoadingCache() throws Exception {
    AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterAccess(3L, TimeUnit.SECONDS)
            .buildAsync(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    log.info("正在重新加载数据... key={}", key);
                    TimeUnit.SECONDS.sleep(1);
                    return key.toUpperCase();
                }
            });

    // 异步缓存的值被CompletableFuture包裹
    cache.put("mca", CompletableFuture.completedFuture("www.mashibing.com"));
    cache.put("baidu", CompletableFuture.completedFuture("www.baidu.com"));
    
    TimeUnit.SECONDS.sleep(5);  // 等待缓存过期
    
    // 批量获取(异步执行,并行加载)
    List<String> keys = Arrays.asList("mca", "baidu");
    Map<String, String> map = cache.getAll(keys).get();
    
    map.forEach((k, v) -> 
        log.info("缓存的键: {}, 值: {}", k, v));
}

特点

  • 返回CompletableFuture包裹的结果
  • 多个key并行加载,提高效率
  • 接口层级:AsyncCacheCache同级

五、缓存淘汰策略

Caffeine提供三类驱逐策略:基于大小基于时间基于引用

5.1 基于大小淘汰

最大容量(maximumSize)
public static void expireMaxType() throws Exception {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(1)  // 最大容量为1
            .expireAfterAccess(3L, TimeUnit.SECONDS)
            .build();
    
    cache.put("name", "张三");
    cache.put("age", "18");  // 触发淘汰,"name"被清除
    
    TimeUnit.MILLISECONDS.sleep(100);
    
    System.out.println(cache.getIfPresent("name"));  // null
    System.out.println(cache.getIfPresent("age"));   // 18
}
最大权重(maximumWeight)
public static void expireWeigherType() throws Exception {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumWeight(100)  // 最大权重100
            .weigher((key, value) -> {
                // 自定义权重计算
                if (key.equals("age")) {
                    return 30;
                }
                return 50;
            })
            .build();
    
    cache.put("name", "张三");   // 权重50
    cache.put("age", "18");      // 权重30
    cache.put("sex", "男");      // 权重50,总权重130>100,触发淘汰
    
    TimeUnit.MILLISECONDS.sleep(100);
    
    System.out.println(cache.getIfPresent("name"));  // null(被清除)
    System.out.println(cache.getIfPresent("age"));   // 18
    System.out.println(cache.getIfPresent("sex"));   // 男
}

注意maximumSizemaximumWeight只能二选一。

5.2 基于时间淘汰

最后一次访问后过期
public static void expireAfterAccess() throws Exception {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterAccess(1L, TimeUnit.SECONDS)  // 1秒无访问过期
            .build();
    
    cache.put("name", "张三");
    
    for (int i = 0; i < 10; i++) {
        System.out.println("第" + i + "次读:" + cache.getIfPresent("name"));
        TimeUnit.SECONDS.sleep(2);  // 每次间隔2秒,会过期
    }
}
最后一次写入后过期
public static void expireAfterWrite() throws InterruptedException {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(1L, TimeUnit.SECONDS)  // 写入后1秒过期
            .build();
    
    cache.put("name", "张三");
    
    for (int i = 0; i < 10; i++) {
        System.out.println("第" + i + "次读:" + cache.getIfPresent("name"));
        TimeUnit.SECONDS.sleep(1);
    }
}
自定义过期策略
public static void customExpire() throws InterruptedException {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfter(new Expiry<String, String>() {
                @Override
                public long expireAfterCreate(String key, String value, long currentTime) {
                    // 创建后2秒失效
                    return TimeUnit.NANOSECONDS.convert(2, TimeUnit.SECONDS);
                }
                
                @Override
                public long expireAfterUpdate(String key, String value, 
                        long currentTime, long currentDuration) {
                    // 更新后5秒失效
                    return TimeUnit.NANOSECONDS.convert(5, TimeUnit.SECONDS);
                }
                
                @Override
                public long expireAfterRead(String key, String value, 
                        long currentTime, long currentDuration) {
                    // 读取后100秒失效
                    return TimeUnit.NANOSECONDS.convert(100, TimeUnit.SECONDS);
                }
            })
            .build();
    
    cache.put("name", "张三");
    // ...
}

5.3 基于引用淘汰

软引用(Soft Reference)
public static void expireSoft() throws InterruptedException {
    Cache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .softValues()  // 使用软引用
            .build();
    
    cache.put("name", new byte[1024 * 1024 * 5]);  // 5MB数据
    
    System.out.println("第1次读:" + cache.getIfPresent("name"));
    
    // 模拟OOM
    List<byte[]> list = new LinkedList<>();
    try {
        for (int i = 0; i < 100; i++) {
            list.add(new byte[1024 * 1024 * 1]);  // 1M对象
        }
    } catch (Throwable e) {
        TimeUnit.SECONDS.sleep(1);
        System.out.println("OOM时读:" + cache.getIfPresent("name"));  // null
    }
}

JVM参数-Xms20m -Xmx20m

弱引用(Weak Reference)
public static void expireWeak() throws InterruptedException {
    Cache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .weakValues()  // 使用弱引用
            .build();
    
    cache.put("name", new WeakReference<>("张三"));
    
    System.out.println("第1次读:" + cache.getIfPresent("name"));
    
    System.gc();  // 触发GC
    
    System.out.println("GC后读:" + cache.getIfPresent("name"));  // null
}

六、状态统计与监控

6.1 开启统计功能

public static void cacheStats() throws Exception {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(2)
            .recordStats()  // 开启统计功能
            .expireAfterAccess(200L, TimeUnit.SECONDS)
            .build();
    
    cache.put("name", "张三");
    cache.put("sex", "男");
    cache.put("age", "18");
    
    // 模拟查询
    String[] keys = new String[]{"name", "age", "sex", "phone", "school"};
    for (int i = 0; i < 1000; i++) {
        cache.getIfPresent(keys[new Random().nextInt(keys.length)]);
    }
    
    // 获取统计数据
    CacheStats stats = cache.stats();
    System.out.println("用户请求查询总次数:" + stats.requestCount());
    System.out.println("命中个数:" + stats.hitCount());
    System.out.println("命中率:" + stats.hitRate());
    System.out.println("未命中次数:" + stats.missCount());
    System.out.println("未命中率:" + stats.missRate());
    System.out.println("加载次数:" + stats.loadCount());
    System.out.println("总共加载时间:" + stats.totalLoadTime());
    System.out.println("平均加载时间(纳秒):" + stats.averageLoadPenalty());
    System.out.println("加载失败率:" + stats.loadFailureRate());
    System.out.println("加载失败次数:" + stats.loadFailureCount());
    System.out.println("加载成功次数:" + stats.loadSuccessCount());
    System.out.println("被淘汰数据总个数:" + stats.evictionCount());
    System.out.println("被淘汰数据总权重:" + stats.evictionWeight());
}

6.2 自定义状态收集器

public class MyStatsCounter implements StatsCounter {
    @Override
    public void recordHits(int count) {
        System.out.println("命中次数:" + count);
    }
    
    @Override
    public void recordMisses(int count) {
        System.out.println("未命中次数:" + count);
    }
    
    @Override
    public void recordLoadSuccess(long loadTime) {
        System.out.println("加载成功,耗时:" + loadTime);
    }
    
    @Override
    public void recordLoadFailure(long loadTime) {
        System.out.println("加载失败,耗时:" + loadTime);
    }
    
    @Override
    public void recordEviction() {
        System.out.println("缓存被淘汰");
    }
    
    @Override
    public CacheStats snapshot() {
        return null;
    }
}

七、清除与更新监听

缓存数据更新或被清除时触发监听器(异步执行):

public static void removalListener() throws InterruptedException {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(2)
            .removalListener((key, value, cause) -> {
                System.out.println("键:" + key + 
                                 " 值:" + value + 
                                 " 清除原因:" + cause);
            })
            .expireAfterAccess(1, TimeUnit.SECONDS)
            .build();
    
    cache.put("name", "张三");
    cache.put("sex", "男");
    cache.put("age", "18");  // 触发淘汰,"name"被清除
    
    TimeUnit.SECONDS.sleep(2);
    
    cache.put("name2", "张三");
    cache.put("age2", "18");
    cache.invalidate("age2");  // 手动清除
    
    TimeUnit.SECONDS.sleep(10);
}

清除原因(RemovalCause)

  • EXPLICIT:手动清除(调用invalidate()
  • REPLACED:被替换(put相同key)
  • COLLECTED:被垃圾收集器回收
  • EXPIRED:过期
  • SIZE:超过容量限制

八、完整配置速查表

配置项 说明 示例
initialCapacity 初始缓存空间大小 .initialCapacity(100)
maximumSize 最大缓存条数 .maximumSize(1000)
maximumWeight 最大权重 .maximumWeight(10000)
expireAfterAccess 最后一次访问后过期 .expireAfterAccess(5, TimeUnit.MINUTES)
expireAfterWrite 最后一次写入后过期 .expireAfterWrite(10, TimeUnit.MINUTES)
refreshAfterWrite 定时刷新 .refreshAfterWrite(1, TimeUnit.MINUTES)
weakKeys Key使用弱引用 .weakKeys()
weakValues Value使用弱引用 .weakValues()
softValues Value使用软引用 .softValues()
recordStats 开启统计 .recordStats()
removalListener 清除监听器 .removalListener((k, v, c) -> {...})

关键词:Caffeine Cache, Java本地缓存, Spring Boot缓存, 缓存淘汰策略, 软引用, 弱引用, 缓存统计, 异步加载, 同步加载, CacheLoader

如果本文对你有帮助,欢迎点赞、收藏、关注!有任何Caffeine使用问题,欢迎在评论区留言讨论。

更多推荐