Java 缓存框架 Caffeine 应用场景解析

2025-09-24 12:50

本文主要是介绍Java 缓存框架 Caffeine 应用场景解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Java缓存框架Caffeine应用场景解析》文章介绍Caffeine作为高性能Java本地缓存框架,基于W-TinyLFU算法,支持异步加载、灵活过期策略、内存安全机制及统计监控,重点解析其...

一、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() 配置缓存规则,常见配置包括:

  • 容量控制
    • maximumSize(long):设置缓存最大容量(条目数),超过后按 W-TinyLFU 算法淘汰。
    • maximumWeight(long) + weigher(Weigher):基于权重控制缓存大小,适合不同条目占用不同内存的场景。
  • 过期策略
    • expireAfterWrite(Duration):写入后过期(如 10 分钟未更新则过期),适合数据变更频繁的场景。
    • expireAfterAccess(Duration):访问后过期(如 5 分钟未访问则过期),适合热点数据缓存。
    • expireAfter(Expiry):自定义过期时间计算逻辑,可实现基于业务规则的过期。
  • 监听器
    • removalListener(RemovalListener):设置缓存移除监听器,可记录日志或触发后续操作。
  • 其他特性
    • 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"); // 单个删除
   www.chinasem.cn     userCache.invalidateAll(List.of("user:1001", "user:1003")); // 批量删除
        userCache.cleanUp(); // 手动触发清理过期条目
        userCache.invalidateAll(); // 清空所有缓存
        // 6. 查看统计信息(需先启用recordStats)
        System.out.println("命中率:" + userCache.stats().hitRate());
    }
}

3.1.2 应用场景示例

  • 简单KV缓存
    • 缓存用户Session信息
    • 缓存系统配置项
    • 临时数据存储(如验证码)
  • 配合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;
    }
}

多级缓存

// 作为本地缓存与Redis组成二级缓存
public class MultiLevelCache {
    private final Cache<String, Object>YjlSE 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)与过php期(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&pythonlt;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 最佳实践

线程池配置

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

异常处理

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

结合Spring使用

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

性能监控

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

功能特性对比

特性CaffeineGuava 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 进程
  • 不同服务器节点间的缓存数据不共享
  • 适用于高频访问、低变化率的数据

二级缓存架构实现

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

  • 第一层:Caffeine 本地缓存(纳秒级响应)
    • 设置合理的过期时间(如30秒)
    • 适合极端热点数据
  • 第二层:Redis 分布式缓存(毫秒级响应)
    • 设置较长的过期时间(如5分钟)
    • 使用Redis集群保证高可用
  • 数据源:数据库/服务(秒级响应)
    • 最终数据一致性保障

实现示例

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永不过期方案

实现模式

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

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);

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

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);
    

这篇关于Java 缓存框架 Caffeine 应用场景解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1156002

相关文章

Java 中的 equals 和 hashCode 方法关系与正确重写实践案例

《Java中的equals和hashCode方法关系与正确重写实践案例》在Java中,equals和hashCode方法是Object类的核心方法,广泛用于对象比较和哈希集合(如HashMa... 目录一、背景与需求分析1.1 equals 和 hashCode 的背景1.2 需求分析1.3 技术挑战1.4

一个Java的main方法在JVM中的执行流程示例详解

《一个Java的main方法在JVM中的执行流程示例详解》main方法是Java程序的入口点,程序从这里开始执行,:本文主要介绍一个Java的main方法在JVM中执行流程的相关资料,文中通过代码... 目录第一阶段:加载 (Loading)第二阶段:链接 (Linking)第三阶段:初始化 (Initia

使用Node.js和PostgreSQL构建数据库应用

《使用Node.js和PostgreSQL构建数据库应用》PostgreSQL是一个功能强大的开源关系型数据库,而Node.js是构建高效网络应用的理想平台,结合这两个技术,我们可以创建出色的数据驱动... 目录初始化项目与安装依赖建立数据库连接执行CRUD操作查询数据插入数据更新数据删除数据完整示例与最佳

java读取excel文件为base64实现方式

《java读取excel文件为base64实现方式》文章介绍使用ApachePOI和EasyExcel处理Excel文件并转换为Base64的方法,强调EasyExcel适合大文件且内存占用低,需注意... 目录使用 Apache POI 读取 Excel 并转换为 Base64使用 EasyExcel 处

java时区时间转为UTC的代码示例和详细解释

《java时区时间转为UTC的代码示例和详细解释》作为一名经验丰富的开发者,我经常被问到如何将Java中的时间转换为UTC时间,:本文主要介绍java时区时间转为UTC的代码示例和详细解释,文中通... 目录前言步骤一:导入必要的Java包步骤二:获取指定时区的时间步骤三:将指定时区的时间转换为UTC时间步

Java 日志中 Marker 的使用示例详解

《Java日志中Marker的使用示例详解》Marker是SLF4J(以及Logback、Log4j2)提供的一个接口,它本质上是一个命名对象,你可以把它想象成一个可以附加到日志语句上的标签或戳... 目录什么是Marker?为什么使用Markejavascriptr?1. 精细化的过滤2. 触发特定操作3

深入浅出Java中的Happens-Before核心规则

《深入浅出Java中的Happens-Before核心规则》本文解析Java内存模型中的Happens-Before原则,解释其定义、核心规则及实际应用,帮助理解多线程可见性与有序性问题,掌握并发编程... 目录前言一、Happens-Before是什么?为什么需要它?1.1 从一个问题说起1.2 Haht

JDK8(Java Development kit)的安装与配置全过程

《JDK8(JavaDevelopmentkit)的安装与配置全过程》文章简要介绍了Java的核心特点(如跨平台、JVM机制)及JDK/JRE的区别,重点讲解了如何通过配置环境变量(PATH和JA... 目录Java特点JDKJREJDK的下载,安装配置环境变量总结Java特点说起 Java,大家肯定都

Spring定时任务之fixedRateString的实现示例

《Spring定时任务之fixedRateString的实现示例》本文主要介绍了Spring定时任务之fixedRateString的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录从毫秒到 Duration:为何要改变?核心:Java.time.Duration.parse

Java 中 Optional 的用法及最佳实践

《Java中Optional的用法及最佳实践》在Java开发中,空指针异常(NullPointerException)是开发者最常遇到的问题之一,本篇文章将详细讲解Optional的用法、常用方... 目录前言1. 什么是 Optional?主要特性:2. Optional 的基本用法2.1 创建 Opti