java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java 缓存框架 Caffeine

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

作者:祈祷苍天赐我java之术

文章介绍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在性能方面实现了质的飞跃:

1.1.2 灵活的过期策略

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

  1. 写入后过期:通过expireAfterWrite设置,例如:
    Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
  2. 访问后过期:通过expireAfterAccess设置,适合热点数据场景
  3. 自定义过期:通过expireAfter方法实现基于业务逻辑的复杂过期判断
1.1.3 异步支持

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

1.1.4 丰富的监听器

Caffeine提供完善的监控支持:

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.高级特性支持

4.最佳实践提示:

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

三、Caffeine 核心用法

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

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

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

3.1.1 创建 Cache 实例

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

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 应用场景示例

@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> 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 最佳实践

线程池配置

// 更完善的线程池配置
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):

⚠️ 典型误用场景:

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

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

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

缓存穿透的典型特征:

解决方案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)的线程池选择

默认线程池的问题:

推荐配置:

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 的优势:

功能特性对比

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

兼容性与迁移

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

// 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 作为本地缓存的核心特点:

二级缓存架构实现

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

实现示例

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);
    
您可能感兴趣的文章:
阅读全文