一、Caffeine 简介

1. 框架概述

Caffeine是由Google工程师Ben Manes开发的一款Java本地缓存框架,其初始版本发布于2014年。该框架的设计灵感来源于Guava Cache,但在性能和功能方面进行了革命性的优化。Caffeine基于"W-TinyLFU"(Window-Tiny Least Frequently Used)算法实现,这是一种改进的LFU缓存淘汰算法,结合了LFU的高命中率优势和LRU的时效性特点。

1.1 Caffeine的核心优势

1.1.1 超高性能

Caffeine在性能方面实现了质的飞跃:

  • 基准测试显示,相比Guava Cache,Caffeine的读性能提升约8-12倍,写性能提升约5-10倍
  • 支持每秒数百万次(典型值300-500万QPS)的缓存操作
  • 采用无锁并发设计,大幅减少线程竞争(如使用并发哈希表和非阻塞队列)
  • 对JVM的内存模型进行了深度优化,减少缓存行伪共享问题
1.1.2 灵活的过期策略

Caffeine提供三种核心过期策略:

  1. 写入后过期:通过expireAfterWrite设置,例如:
    Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
    

  2. 访问后过期:通过expireAfterAccess设置,适合热点数据场景
  3. 自定义过期:通过expireAfter方法实现基于业务逻辑的复杂过期判断
1.1.3 异步支持

Caffeine提供完整的异步缓存(AsyncCache)支持:

  • 异步加载机制:通过AsyncLoadingCache实现非阻塞式数据加载
  • 支持CompletableFuture:可与Java8的异步编程模型完美结合
  • 典型应用场景:高并发环境下的微服务接口缓存
1.1.4 丰富的监听器

Caffeine提供完善的监控支持:

  • 移除监听器(RemovalListener):可监听缓存项的驱逐、失效或手动移除
  • 统计功能:通过recordStats()开启命中率统计
  • 典型配置:
    cache.recordStats();
    CacheStats stats = cache.stats();
    double hitRate = stats.hitRate();
    

1.1.5 内存安全

Caffeine提供多种内存保护机制:

  1. 基于容量:通过maximumSize限制缓存项数量
  2. 基于时间:通过上述过期策略控制
  3. 基于引用:支持弱引用键/值(weakKeys/weakValues)和软引用值(softValues)
  4. 权重控制:通过weighermaximumWeight实现基于对象大小的精确控制

典型内存安全配置示例:

Caffeine.newBuilder()
    .maximumSize(10_000)
    .weigher((String key, String value) -> value.length())
    .maximumWeight(50_000_000) // ~50MB
    .build();

二、Caffeine 基础

在使用 Caffeine 前,需先引入依赖,并了解其核心组件的作用。

2.1 依赖引入(Maven/Gradle)

Caffeine 的最新版本可在 Maven 中央仓库查询(https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine)

Maven 配置示例(含注释说明)

<!-- Caffeine核心依赖(必选) -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.2.7</version>  <!-- 截至2024年1月最新稳定版 -->
    <!-- 建议通过dependencyManagement统一管理版本 -->
</dependency>

<!-- 异步支持依赖(可选) -->
<!-- 当需要配合Java11+的HttpClient实现异步缓存加载时添加 -->
<dependency>
    <groupId>java.net.http</groupId>
    <artifactId>http-client</artifactId>
    <version>11.0.1</version>  <!-- 最低要求JDK11 -->
    <scope>runtime</scope>    <!-- 通常只需运行时依赖 -->
</dependency>

Gradle 配置示例(Kotlin DSL)

dependencies {
    // 核心实现(必选)
    implementation("com.github.ben-manes.caffeine:caffeine:3.2.7")
    
    // 异步支持(可选)
    runtimeOnly("java.net.http:http-client:11.0.1") {
        because("For async cache loading with HTTP requests")
    }
}

2.2 核心组件解析

Caffeine 的核心组件采用分层设计,主要分为基础缓存接口和功能扩展接口两大类:

1.基础缓存接口层次结构

Cache (基本功能)
├── LoadingCache (同步加载)
└── AsyncCache (异步基础)
    └── AsyncLoadingCache (异步加载)

2.关键组件详细说明(含典型应用场景)

组件 作用说明 典型使用场景 示例代码片段
Cache<K,V> 手动管理缓存,需显式处理缓存未命中 简单缓存场景,数据源访问成本较低 cache.get(key, k -> fetchFromDB(k))
LoadingCache<K,V> 自动加载缓存,内置CacheLoader 高频访问且加载逻辑固定的场景 LoadingCache.from(this::loadFromAPI)
AsyncCache<K,V> 返回CompletableFuture的异步接口 配合非阻塞IO或远程调用 cache.get(key).thenAccept(value -> ...)
AsyncLoadingCache<K,V> 异步自动加载缓存 微服务间数据缓存 AsyncLoadingCache.from(this::asyncLoad)
CacheLoader<K,V> 定义加载逻辑的函数式接口 统一数据加载策略 new CacheLoader<>() { @Override public V load(K key)... }
RemovalListener<K,V> 移除事件监听器 缓存一致性维护、监控统计 listener((key,value,reason) -> logRemoval())
Expiry<K,V> 细粒度过期控制 动态TTL场景(如会话缓存) expireAfter((key,value,currentTime) -> customTTL)

3.高级特性支持

  • 权重计算:通过weigher接口实现基于缓存对象大小的淘汰策略
  • 刷新机制refreshAfterWrite配合CacheLoader.reload实现后台刷新
  • 统计监控recordStats()启用命中率等统计指标
  • 线程模型:默认使用ForkJoinPool.commonPool(),可通过executor自定义

4.最佳实践提示:

  1. 对于长时间加载操作,优先选择AsyncLoadingCache避免阻塞
  2. 移除监听器不要执行耗时操作,否则会影响缓存性能
  3. 在Spring环境中建议通过@Bean配置全局缓存管理器
  4. 生产环境务必启用统计功能(recordStats)进行监控

三、Caffeine 核心用法

Caffeine 的使用流程遵循 "构建器模式配置 → 创建缓存实例 → 读写缓存" 的逻辑,下面分场景讲解具体用法。

3.1 基础缓存(Cache):手动控制读写

Cache 是最基础的缓存类型,需手动处理缓存未命中(未命中时返回 null),适合缓存逻辑简单的场景。

3.1.1 创建 Cache 实例

通过 Caffeine.newBuilder() 配置缓存规则,常见配置包括:

  1. 容量控制

    • maximumSize(long):设置缓存最大容量(条目数),超过后按 W-TinyLFU 算法淘汰。
    • maximumWeight(long) + weigher(Weigher):基于权重控制缓存大小,适合不同条目占用不同内存的场景。
  2. 过期策略

    • expireAfterWrite(Duration):写入后过期(如 10 分钟未更新则过期),适合数据变更频繁的场景。
    • expireAfterAccess(Duration):访问后过期(如 5 分钟未访问则过期),适合热点数据缓存。
    • expireAfter(Expiry):自定义过期时间计算逻辑,可实现基于业务规则的过期。
  3. 监听器

    • removalListener(RemovalListener):设置缓存移除监听器,可记录日志或触发后续操作。
  4. 其他特性

    • weakKeys()/weakValues():使用弱引用,允许被垃圾回收。
    • softValues():使用软引用,在内存不足时被回收。
    • recordStats():启用统计信息收集。
import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;

public class CaffeineBasicDemo {
    public static void main(String[] args) {
        // 1. 配置并创建Cache实例(带详细注释)
        Cache<String, String> userCache = Caffeine.newBuilder()
            .maximumSize(1000) // 最大容量1000条
            .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
            .expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期(优先级低于expireAfterWrite)
            .removalListener((key, value, cause) -> { // 缓存移除监听器
                System.out.printf("缓存移除:key=%s, value=%s, 原因=%s%n",
                    key, value, cause.toString());
                // 原因可能是:EXPLICIT(手动删除)、REPLACED(值被替换)、
                // COLLECTED(垃圾回收)、EXPIRED(过期)、SIZE(超过容量限制)
            })
            .recordStats() // 启用统计
            .build(); // 构建Cache实例

        // 2. 写入缓存(多种方式)
        userCache.put("user:1001", "张三"); // 常规put
        userCache.asMap().putIfAbsent("user:1002", "李四"); // 线程安全写入

        // 3. 读取缓存(未命中返回null)
        String user1 = userCache.getIfPresent("user:1001");
        System.out.println("读取user:1001:" + user1); // 输出:张三

        // 4. 读取并计算(未命中时执行函数逻辑,但不自动存入缓存)
        String user3 = userCache.get("user:1003", key -> {
            // 模拟从数据库查询数据(仅当缓存未命中时执行)
            System.out.println("缓存未命中,查询DB:" + key);
            return "王五"; // 此结果不会自动存入缓存
        });
        System.out.println("读取user:1003:" + user3); // 输出:王五

        // 5. 缓存维护操作
        userCache.invalidate("user:1002"); // 单个删除
        userCache.invalidateAll(List.of("user:1001", "user:1003")); // 批量删除
        userCache.cleanUp(); // 手动触发清理过期条目
        userCache.invalidateAll(); // 清空所有缓存

        // 6. 查看统计信息(需先启用recordStats)
        System.out.println("命中率:" + userCache.stats().hitRate());
    }
}

3.1.2 应用场景示例

  1. 简单KV缓存

    • 缓存用户Session信息
    • 缓存系统配置项
    • 临时数据存储(如验证码)
  2. 配合Spring Cache

    @Configuration
    @EnableCaching
    public class CacheConfig {
        @Bean
        public CacheManager cacheManager() {
            CaffeineCacheManager manager = new CaffeineCacheManager();
            manager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(10, TimeUnit.MINUTES));
            return manager;
        }
    }
    

  3. 多级缓存

    // 作为本地缓存与Redis组成二级缓存
    public class MultiLevelCache {
        private final Cache<String, Object> localCache;
        private final RedisTemplate<String, Object> redisTemplate;
    
        public Object get(String key) {
            Object value = localCache.getIfPresent(key);
            if (value == null) {
                value = redisTemplate.opsForValue().get(key);
                if (value != null) {
                    localCache.put(key, value);
                }
            }
            return value;
        }
    }
    

3.2 加载缓存(LoadingCache):自动加载未命中数据

LoadingCache 是 Cache 的子类,通过实现 CacheLoader 接口,实现 "缓存未命中时自动加载数据并存入缓存",适合缓存数据需从数据源(如 DB、Redis)加载的场景。

3.2.1 创建 LoadingCache 实例

import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.LoadingCache;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.List;

public class CaffeineLoadingDemo {
    public static void main(String[] args) throws ExecutionException {
        // 1. 实现CacheLoader:定义缓存未命中时的加载逻辑
        LoadingCache<String, String> productCache = Caffeine.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .refreshAfterWrite(10, TimeUnit.MINUTES) // 10分钟后刷新(不阻塞读取)
            .recordStats()
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    // 模拟从数据库加载数据(缓存未命中时自动执行)
                    System.out.println("缓存未命中,从DB加载:" + key);
                    if (key.startsWith("prod:")) {
                        return "商品-" + key.substring(5); // 如key=prod:101 → 商品-101
                    }
                    throw new IllegalArgumentException("Invalid key format");
                }

                // 可选:实现批量加载(提升getAll性能)
                @Override
                public Map<String, String> loadAll(Iterable<? extends String> keys) {
                    System.out.println("批量加载keys:" + keys);
                    // 实际应从DB批量查询
                    Map<String, String> result = new HashMap<>();
                    for (String key : keys) {
                        result.put(key, "商品-" + key.substring(5));
                    }
                    return result;
                }
            });

        // 2. 读取缓存(未命中时自动调用load()加载并存入缓存)
        String product1 = productCache.get("prod:101"); // 首次:加载并返回
        System.out.println("读取prod:101:" + product1); // 输出:商品-101

        // 3. 批量读取(getAll())
        Map<String, String> products = productCache.getAll(List.of("prod:102", "prod:103"));
        System.out.println("批量读取结果:" + products);

        // 4. 主动刷新(异步)
        productCache.refresh("prod:101"); // 后台刷新,旧值仍可用

        // 5. 统计信息
        System.out.println("加载次数:" + productCache.stats().loadCount());
    }
}

3.2.2 关键特性:刷新(Refresh)与过期(Expire)的区别

特性 刷新(Refresh) 过期(Expire)
触发时机 刷新时间到后 过期时间到后
读取行为 异步刷新,立即返回旧值 同步重新加载,可能阻塞请求
适用场景 数据允许短暂不一致(如商品详情) 数据强一致要求(如订单状态)
实现方式 需配置refreshAfterWrite 配置expireAfterWrite/AfterAccess

典型使用模式

// 商品详情缓存:10分钟强制过期,5分钟自动刷新
LoadingCache<String, Product> productCache = Caffeine.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(10, TimeUnit.MINUTES) // 强制过期时间
    .refreshAfterWrite(5, TimeUnit.MINUTES) // 自动刷新时间
    .build(this::loadProductFromDB);

3.3 异步缓存(AsyncCache/AsyncLoadingCache):非阻塞读写

在高并发场景下,同步缓存的 load() 可能会阻塞线程,而 AsyncCache 通过返回 CompletableFuture 实现非阻塞操作,所有 IO 操作均在异步线程池中执行。

3.3.1 创建 AsyncLoadingCache 实例

import com.github.ben-manes.caffeine.cache.AsyncLoadingCache;
import com.github.ben-manes.caffeine.cache.Caffeine;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CaffeineAsyncDemo {
    public static void main(String[] args) throws Exception {
        // 1. 自定义线程池(生产环境建议使用有界队列和拒绝策略)
        Executor executor = Executors.newFixedThreadPool(5, r -> {
            Thread thread = new Thread(r);
            thread.setName("caffeine-async-" + thread.getId());
            return thread;
        });

        // 2. 创建AsyncLoadingCache实例
        AsyncLoadingCache<String, String> orderCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(15, TimeUnit.MINUTES)
            .executor(executor) // 指定异步线程池
            .buildAsync(key -> {
                // 模拟耗时操作(如RPC调用,耗时200ms)
                TimeUnit.MILLISECONDS.sleep(200);
                System.out.println(Thread.currentThread().getName() + " 加载订单:" + key);
                return "订单-" + key.substring(6); // 如key=order:2024 → 订单-2024
            });

        // 3. 异步读取(推荐方式)
        CompletableFuture<String> future = orderCache.get("order:2024");
        future.thenApplyAsync(order -> {
            System.out.println("处理订单数据:" + order);
            return order.toUpperCase();
        }, executor); // 使用相同线程池处理结果

        // 4. 批量读取(返回Map<Key, CompletableFuture>)
        Map<String, CompletableFuture<String>> futures = 
            orderCache.getAll(List.of("order:2025", "order:2026"));
        
        // 5. 同步获取(仅测试用,实际应避免)
        String order = orderCache.get("order:2027").get();
        System.out.println("同步获取结果:" + order);
    }
}

3.3.2 最佳实践

  1. 线程池配置

    // 更完善的线程池配置
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        5, // 核心线程数
        10, // 最大线程数
        60, TimeUnit.SECONDS, // 空闲线程存活时间
        new LinkedBlockingQueue<>(1000), // 有界队列
        new ThreadFactoryBuilder().setNameFormat("cache-loader-%d").build(),
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
    );
    

  2. 异常处理

    orderCache.get("badKey").exceptionally(ex -> {
        System.err.println("加载失败: " + ex.getMessage());
        return "defaultValue";
    });
    

  3. 结合Spring使用

    @Cacheable(value = "orders", cacheManager = "asyncCacheManager")
    public CompletableFuture<Order> getOrderAsync(String orderId) {
        return CompletableFuture.supplyAsync(() -> orderService.loadOrder(orderId));
    }
    

  4. 性能监控

    CacheStats stats = orderCache.synchronous().stats();
    System.out.println("平均加载时间:" + stats.averageLoadPenalty() + "ns");
    

四、Caffeine 高级特性

4.1 缓存统计(Cache Statistics)

缓存统计功能是优化缓存性能的重要工具。通过开启缓存统计,可以实时监控以下关键指标:

  1. 命中率(Hit Rate):反映缓存有效性,计算公式为:命中次数/(命中次数+未命中次数)
  2. 加载耗时(Load Penalty):统计从数据源加载数据的平均耗时
  3. 移除次数(Eviction Count):因容量或过期策略导致的缓存移除次数
  4. 加载失败率(Load Failure Rate):数据源加载失败的比例

典型应用场景:

  • 评估缓存配置是否合理
  • 识别热点数据
  • 监控缓存性能瓶颈
import com.github.ben-manes.caffeine.cache.CacheStats;

public class CaffeineStatsDemo {

    public static void main(String[] args) {
        LoadingCache<String, String> statsCache = Caffeine.newBuilder()
            .maximumSize(100)
            .recordStats() // 必须显式开启统计功能
            .build(key -> {
                // 模拟耗时加载
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                return "统计测试:" + key;
            });

        // 模拟读写操作
        statsCache.get("key1");  // 第一次加载(未命中)
        statsCache.get("key1");  // 命中已有缓存
        statsCache.get("key2");  // 新键加载
        statsCache.invalidate("key1"); // 手动失效

        // 获取统计结果
        CacheStats stats = statsCache.stats();
        System.out.println("缓存命中率:" + stats.hitRate());  // 50%(1次命中/2次查询)
        System.out.println("加载成功次数:" + stats.loadSuccessCount()); // 2次加载
        System.out.println("移除次数:" + stats.evictionCount()); // 0(未达到容量上限)
        System.out.println("平均加载耗时(ns):" + stats.averageLoadPenalty()); // 约100ms
        System.out.println("加载失败率:" + stats.loadFailureRate()); // 0.0
    }
}

4.2 自定义过期策略(Expiry)

标准的TTL(Time-To-Live)过期策略对所有缓存条目采用统一设置,而自定义过期策略允许基于业务特性实现精细化控制。

常见应用场景:

  • 不同优先级数据设置不同有效期(如热点数据短时效,冷数据长时效)
  • 读写操作影响过期时间(如读操作续期)
  • 动态调整过期时间(如根据数据价值计算)
import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.Expiry;
import com.github.ben-manes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;

class CustomExpiry implements Expiry<String, String> {
    @Override
    public long expireAfterCreate(String key, String value, long currentTime) {
        // 创建时过期策略
        if (key.startsWith("flash:")) {    // 闪存数据:30秒过期
            return TimeUnit.SECONDS.toNanos(30);
        } else if (key.startsWith("hot:")) { // 热门数据:5分钟
            return TimeUnit.MINUTES.toNanos(5);
        } else {                            // 普通数据:30分钟
            return TimeUnit.MINUTES.toNanos(30);
        }
    }

    @Override
    public long expireAfterUpdate(String key, String value, 
            long currentTime, long currentDuration) {
        // 更新策略:保持原有过期时间(默认)
        return currentDuration;
        // 或者重置为创建时间:return expireAfterCreate(key, value, currentTime);
    }

    @Override
    public long expireAfterRead(String key, String value, 
            long currentTime, long currentDuration) {
        // 读取时策略:热门数据读取后续期5分钟
        if (key.startsWith("hot:")) {
            return TimeUnit.MINUTES.toNanos(5);
        }
        return currentDuration;
    }
}

public class CaffeineCustomExpiryDemo {
    public static void main(String[] args) {
        LoadingCache<String, String> customExpiryCache = Caffeine.newBuilder()
            .expireAfter(new CustomExpiry())
            .build(key -> "自定义过期:" + key);

        customExpiryCache.get("flash:news:2023");   // 30秒过期
        customExpiryCache.get("hot:product:101");   // 5分钟且读取续期
        customExpiryCache.get("normal:user:201");   // 30分钟过期
    }
}

4.3 弱引用与软引用:避免内存溢出

Java引用类型与缓存回收策略:

引用类型 GC行为 适用场景 Caffeine配置
强引用 永不回收 默认方式 -
软引用 内存不足时回收 缓存大对象 .softValues()
弱引用 下次GC时回收 临时性缓存 .weakKeys()/.weakValues()

注意事项:

  1. 使用weakKeys()时,key比较基于==而非equals()
  2. softValues()可能导致GC压力增大
  3. 引用回收与显式失效策略共同作用
// 弱引用Key+Value的缓存(适合临时性数据)
Cache<String, byte[]> weakCache = Caffeine.newBuilder()
    .weakKeys()    // Key无强引用时回收
    .weakValues()  // Value无强引用时回收
    .maximumSize(10_000)  // 仍保持容量限制
    .build();

// 软引用Value的缓存(适合大对象)
Cache<String, byte[]> softCache = Caffeine.newBuilder()
    .softValues()  // 内存不足时回收Value
    .expireAfterWrite(1, TimeUnit.HOURS)  // 配合显式过期
    .build();

// 典型使用场景
void processLargeData(String dataId) {
    byte[] data = softCache.get(dataId, id -> {
        // 从数据库加载大对象(如图片、文件等)
        return loadLargeDataFromDB(id); 
    });
    // 使用数据...
}

五、Caffeine 注意事项

在实际开发中,若使用不当,Caffeine 可能出现缓存穿透、内存溢出、线程阻塞等问题,以下是核心注意事项:

5.1 区分 "刷新(Refresh)" 与 "过期(Expire)"

刷新(refreshAfterWrite):

  • 工作机制:当缓存条目超过指定时间未被写入时,下次读取会触发异步刷新,但在此期间仍会返回旧值
  • 适用场景:对数据一致性要求不高,可接受短暂延迟的场景
    • 商品详情页的评论数统计
    • 新闻资讯的阅读量统计
    • 排行榜数据的更新

过期(expireAfterWrite/expireAfterAccess):

  • expireAfterWrite:从写入开始计时
  • expireAfterAccess:从最后一次访问开始计时
  • 工作机制:过期后缓存条目立即失效,读取时会同步阻塞直到重新加载完成
  • 适用场景:对数据一致性要求高的核心业务
    • 用户账户余额
    • 订单支付状态
    • 库存数量

⚠️ 典型误用场景:

将用户余额这类强一致性数据配置为refreshAfterWrite(5s).可能导致:

  1. 用户A看到余额100元
  2. 用户B完成扣款50元
  3. 5秒内用户A仍看到100元(旧值)
  4. 直到下次读取才刷新为50元

5.2 避免缓存穿透:空值缓存与布隆过滤器

缓存穿透的典型特征:

  • 查询一个必然不存在的数据(如不存在的用户ID)
  • 每次请求都穿透到数据库
  • 可能被恶意攻击者利用,造成数据库压力

解决方案1:空值缓存

LoadingCache<String, String> cache = Caffeine.newBuilder()
    .expireAfterWrite(1, TimeUnit.MINUTES)  // 空值缓存1分钟
    .build(key -> {
        String value = queryFromDB(key);
        // 特殊空值标记,避免与真实空值混淆
        return value != null ? value : "NULL_VALUE";  
    });

解决方案2:布隆过滤器(适合千万级key场景)

// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
    Funnels.stringFunnel(Charset.defaultCharset()), 
    1000000,  // 预期元素数量
    0.01      // 误判率
);

// 查询流程
if (!bloomFilter.mightContain(key)) {
    return null;  // 肯定不存在
} else {
    return cache.get(key);  // 可能存在
}

5.3 缓存键(Key)必须重写 hashCode() 和 equals()

常见错误案例:

class CompositeKey {
    private Long id;
    private String category;
    // 缺少hashCode/equals实现
}

// 实际使用中
CompositeKey key1 = new CompositeKey(1L, "A");
CompositeKey key2 = new CompositeKey(1L, "A");
cache.put(key1, "value");

// 将返回null,因为key2被视为不同key
cache.getIfPresent(key2); 

正确实现要点:

  1. 使用Objects工具类自动生成
  2. 保证不可变(final字段)
  3. 实现Serializable接口(分布式缓存需要)
class CompositeKey implements Serializable {
    private final Long id;
    private final String category;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CompositeKey)) return false;
        CompositeKey that = (CompositeKey) o;
        return Objects.equals(id, that.id) && 
               Objects.equals(category, that.category);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, category);
    }
}

5.4 异步缓存(AsyncCache)的线程池选择

默认线程池的问题:

  • ForkJoinPool.commonPool()是JVM全局共享的
  • 可能被CompletableFuture等其他组件占用
  • 在容器环境中可能线程数不足

推荐配置:

ExecutorService executor = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors() * 2,
    new ThreadFactoryBuilder()
        .setNameFormat("caffeine-loader-%d")
        .setDaemon(true)
        .build()
);

AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
    .executor(executor)  // 指定专属线程池
    .buildAsync(key -> loadExpensiveValue(key));

5.5 避免内存溢出:合理配置容量与过期时间

典型配置示例:

Caffeine.newBuilder()
    .maximumSize(10_000)  // 基于条目数限制
    .expireAfterWrite(30, TimeUnit.MINUTES)  // 写入后30分钟过期
    .expireAfterAccess(10, TimeUnit.MINUTES) // 10分钟无访问过期
    .weigher((String key, String value) -> value.length()) // 按value大小计算权重
    .maximumWeight(50_000_000)  // 约50MB内存限制

监控建议:

  1. 通过cache.stats()获取命中率
  2. 使用JMX监控缓存大小
  3. 设置告警阈值(如内存使用>80%)

5.6 CacheLoader 的异常处理

完整异常处理方案:

LoadingCache<String, String> cache = Caffeine.newBuilder()
    .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            try {
                return queryDB(key);
            } catch (SQLException e) {
                // 记录详细日志
                log.error("DB查询失败, key: {}", key, e);
                // 返回降级值
                return "DEFAULT_VALUE";
                // 或者抛出特定异常
                // throw new CacheLoadException(e);
            }
        }
    });

// 使用时的异常处理
try {
    return cache.get(key);
} catch (CacheLoaderException e) {
    // 处理加载失败
    return processFallback(key);
} catch (Exception e) {
    // 兜底处理
    return "SYSTEM_ERROR";
}

六、常见问题

Q1:Caffeine 与 Guava Cache 的详细区别

性能比较

Caffeine 采用了创新的 W-TinyLFU 缓存淘汰算法,该算法结合了 TinyLFU 和 LRU 的优势:

  • 使用 Count-Min Sketch 数据结构高效统计访问频率
  • 适应不同工作负载模式(突发性和长期性访问)
  • 在基准测试中,Caffeine 的读写性能比 Guava Cache 高出 10-20 倍

功能特性对比

特性 Caffeine Guava Cache
异步加载 支持 AsyncLoadingCache 仅同步加载
过期策略 支持基于大小、时间、引用等多种策略 仅基本过期策略
自动刷新 支持 refreshAfterWrite 不支持
权重计算 支持自定义权重 支持但性能较差
监听器 支持移除监听器 支持移除监听器
统计 提供命中率等详细统计 提供基本统计

兼容性与迁移

Caffeine 在设计时特别考虑了与 Guava Cache 的兼容性:

  • 90%以上的 API 可以直接替换
  • 主要差异在于构建方式(Caffeine.newBuilder() vs CacheBuilder.newBuilder())
  • 迁移示例:
// Guava Cache
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .build(new CacheLoader<Key, Value>() {
        public Value load(Key key) {
            return createValue(key);
        }
    });

// 迁移到 Caffeine
LoadingCache<Key, Value> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .build(key -> createValue(key));

Q2:Caffeine 的分布式缓存支持与多级缓存架构

本地缓存特性

Caffeine 作为本地缓存的核心特点:

  • 仅作用于单个 JVM 进程内
  • 不同服务器节点间的缓存数据不共享
  • 适用于高频访问、低变化率的数据

二级缓存架构实现

典型的生产级缓存架构组合:

  1. 第一层:Caffeine 本地缓存(纳秒级响应)

    • 设置合理的过期时间(如30秒)
    • 适合极端热点数据
  2. 第二层:Redis 分布式缓存(毫秒级响应)

    • 设置较长的过期时间(如5分钟)
    • 使用Redis集群保证高可用
  3. 数据源:数据库/服务(秒级响应)

    • 最终数据一致性保障

实现示例

public class TwoLevelCacheService {
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(30, TimeUnit.SECONDS)
        .build();
    
    private final RedisTemplate<String, Object> redisTemplate;
    
    public Object getData(String key) {
        // 1. 尝试从本地缓存获取
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 2. 尝试从Redis获取
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            localCache.put(key, value);
            return value;
        }
        
        // 3. 回源查询
        value = queryDatabase(key);
        redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
        localCache.put(key, value);
        return value;
    }
}

Q3:缓存击穿解决方案的深入分析

互斥锁方案详解

实现要点

  1. 使用 key.intern() 获取字符串规范表示,确保相同key锁定同一对象
  2. 采用双重检查锁定模式减少锁竞争
  3. 设置合理的锁等待超时时间

增强版实现

public Object getDataWithLock(String key) {
    Object value = cache.getIfPresent(key);
    if (value != null) {
        return value;
    }
    
    synchronized (key.intern()) {
        // 双重检查
        value = cache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        try {
            value = queryDataSource(key);
            cache.put(key, value);
        } finally {
            // 释放资源
        }
    }
    return value;
}

热点Key永不过期方案

实现模式

  1. 主动更新:后台线程定期(如每分钟)刷新热点数据

    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    scheduler.scheduleAtFixedRate(() -> {
        List<String> hotKeys = getHotKeyList();
        hotKeys.forEach(key -> {
            Object value = queryDataSource(key);
            cache.put(key, value);
        });
    }, 0, 1, TimeUnit.MINUTES);
    

  2. 被动更新:获取数据时异步刷新

    LoadingCache<String, Object> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .refreshAfterWrite(1, TimeUnit.MINUTES)
        .build(key -> queryDataSource(key));
    

其他防护策略

  1. 布隆过滤器:前置过滤不存在的key请求
  2. 缓存预热:系统启动时加载热点数据
  3. 随机过期时间:对相同类型key设置不同的过期时间偏移量
    int baseExpire = 3600; // 基础1小时
    int randomOffset = ThreadLocalRandom.current().nextInt(600); // 0-10分钟随机
    cache.put(key, value, baseExpire + randomOffset, TimeUnit.SECONDS);
    

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐