找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

268

积分

0

好友

34

主题
发表于 昨天 04:50 | 查看: 26| 回复: 0

Caffeine Cache 简介

Caffeine Cache 是一个基于 Java 的高性能本地缓存库。它站在巨人 Guava Cache 的肩膀上,借鉴其思想并针对缓存淘汰算法进行了显著优化,提供了接近最佳的命中率。

1. Caffeine Cache 的核心算法优势:W-TinyLFU

传统的缓存淘汰算法主要有 FIFO、LRU 和 LFU,它们各有优劣:

  1. FIFO(先进先出):先进入缓存的先被淘汰,命中率通常较低。
  2. LRU(最近最少使用):将最近访问的数据移至队列尾部,淘汰队首数据。但它可能淘汰掉短期内访问频繁但最近未被访问的热点数据。
  3. LFU(最不经常使用):统计数据访问频率,淘汰频率最低的数据。它能避免 LRU 的问题,但需要维护昂贵的频率信息,且难以适应访问模式随时间变化的情况。

Caffeine 采用了W-TinyLFU回收策略,巧妙地结合了 LRU 和 LFU 的优势。

  • TinyLFU:用于应对大多数访问场景,它使用 Count-Min Sketch 数据结构以较小的空间开销维护近期访问频率,作为新数据能否进入缓存的过滤器。
  • Window:一个小的 LRU 窗口,用于处理稀疏的突发访问流量,避免新来的突发访问元素因频率不够而被立即淘汰。

这种组合使得 Caffeine 在各种负载模式下都能获得近乎最佳的命中率

Count-Min Sketch 是布隆过滤器的一种变种,用于高效、节省空间地记录访问频率。如下图所示,它通过多个哈希函数将键映射到一个二维计数数组,获取频率时取所有哈希位置中的最小值,有效降低了哈希冲突带来的误差。

Count-Min Sketch 原理图

2. Caffeine Cache 使用指南

Caffeine 的最新版本可通过 Maven 引入:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.2</version>
</dependency>

2.1 缓存填充策略

Caffeine 提供了三种填充策略:手动加载、同步加载和异步加载。

1. 手动加载

手动控制缓存的存入与获取。

public Object manualOperator(String key) {
    Cache<String, Object> cache = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.SECONDS)
        .maximumSize(10)
        .build();
    // 如果key不存在,则调用函数生成value并加载
    Object value = cache.get(key, t -> setValue(key).apply(key));
    cache.put(\"hello\", value);
    // 获取值,若不存在则返回null
    Object ifPresent = cache.getIfPresent(key);
    // 移除一个key
    cache.invalidate(key);
    return value;
}
public Function<String, Object> setValue(String key){
    return t -> key + \"value\";
}
2. 同步加载

通过 CacheLoader 实现加载逻辑,get 操作会自动加载缓存。

public Object syncOperator(String key){
    LoadingCache<String, Object> cache = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build(k -> setValue(key).apply(key));
    return cache.get(key);
}
3. 异步加载

返回 CompletableFuture,适用于响应式编程模型。

public Object asyncOperator(String key){
    AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .buildAsync(k -> setAsyncValue(key).get());
    return cache.get(key);
}
public CompletableFuture<Object> setAsyncValue(String key){
    return CompletableFuture.supplyAsync(() -> key + \"value\");
}

2.2 回收策略

Caffeine 支持基于大小、时间和引用的回收策略。

1. 基于大小回收
// 基于缓存条目数量
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(10000)
    .build(key -> function(key));

// 基于权重(需实现Weigher接口)
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
    .maximumWeight(10000)
    .weigher((key, value) -> ((String)value).length()) // 示例权重计算
    .build(key -> function(key));
// 注意:maximumSize 与 maximumWeight 不可同时使用
2. 基于时间回收
// 最后一次访问后过期
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .expireAfterAccess(5, TimeUnit.MINUTES)
    .build(key -> function(key));
// 最后一次写入后过期
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> function(key));
// 自定义过期策略
LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
    .expireAfter(new Expiry<String, Object>() {
        @Override
        public long expireAfterCreate(String key, Object value, long currentTime) {
            return TimeUnit.SECONDS.toNanos(60); // 创建60秒后过期
        }
        @Override
        public long expireAfterUpdate(String key, Object value, long currentTime, long currentDuration) {
            return currentDuration; // 更新不改变过期时间
        }
        @Override
        public long expireAfterRead(String key, Object value, long currentTime, long currentDuration) {
            return currentDuration; // 读取不改变过期时间
        }
    }).build(key -> function(key));
3. 基于引用回收

利用 Java 的软引用、弱引用辅助 GC 进行回收。

// 使用弱引用存储Key和Value
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .weakKeys()
    .weakValues()
    .build(key -> function(key));
// 使用软引用存储Value (内存不足时回收)
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
    .softValues()
    .build(key -> function(key));
// 注意:weakValues()和softValues()不可同时使用。AsyncLoadingCache不支持弱引用和软引用。

2.3 移除事件监听

可以监听缓存条目被移除的原因(如过期、被替换、手动删除等)。

Cache<String, Object> cache = Caffeine.newBuilder()
    .removalListener((String key, Object value, RemovalCause cause) ->
        System.out.printf(\"Key %s was removed (%s)%n\", key, cause))
    .build();

2.4 写入外部存储

通过 CacheWriter 可以实现写穿透模式,将缓存的所有写操作同步到外部存储(如数据库),适用于多级缓存场景。

LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
    .writer(new CacheWriter<String, Object>() {
        @Override public void write(String key, Object value) {
            // 写入到外部存储
            externalStorage.save(key, value);
        }
        @Override public void delete(String key, Object value, RemovalCause cause) {
            // 从外部存储删除
            externalStorage.delete(key);
        }
    })
    .build(key -> function(key));
// 注意:CacheWriter不能与弱键或AsyncLoadingCache一起使用。

2.5 统计功能

开启统计后,可以获取缓存命中等多项指标。

Cache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();
CacheStats stats = cache.stats();
System.out.println(\"命中率: \" + stats.hitRate());
System.out.println(\"回收总数: \" + stats.evictionCount());
System.out.println(\"平均加载耗时[纳秒]: \" + stats.averageLoadPenalty());

3. 在 Spring Boot 中集成 Caffeine Cache

Spring Boot 2.x 开始,默认的本地缓存实现已从 Guava Cache 替换为 Caffeine Cache。

3.1 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.2</version>
</dependency>

3.2 启用缓存

在启动类上添加 @EnableCaching 注解。

@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3.3 配置缓存

有两种主要配置方式:配置文件或 Java Config Bean。

方式一:application.yml 配置文件

spring:
  cache:
    type: caffeine
    cache-names: userCache, productCache
    caffeine:
      spec: maximumSize=1024,expireAfterWrite=60s

如果配置了 refreshAfterWrite,则必须定义一个 CacheLoader Bean:

@Bean
public CacheLoader<Object, Object> cacheLoader() {
    return new CacheLoader<Object, Object>() {
        @Override
        public Object load(Object key) { return null; }
        @Override
        public Object reload(Object key, Object oldValue) {
            // 刷新逻辑,这里直接返回旧值,通常应重新加载
            return oldValue;
        }
    };
}

方式二:Java Config 方式(更灵活) 通过 CacheManager Bean 自定义多个具有不同配置的缓存。

@Configuration
public class CacheConfig {
    @Bean
    @Primary
    public CacheManager caffeineCacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<CaffeineCache> caches = new ArrayList<>();
        // 创建用户缓存:最大10000条,60秒过期
        caches.add(new CaffeineCache(\"userCache\",
                Caffeine.newBuilder()
                        .recordStats()
                        .expireAfterWrite(60, TimeUnit.SECONDS)
                        .maximumSize(10000)
                        .build()));
        // 创建部门缓存:最大500条,120秒过期
        caches.add(new CaffeineCache(\"deptCache\",
                Caffeine.newBuilder()
                        .expireAfterWrite(120, TimeUnit.SECONDS)
                        .maximumSize(500)
                        .build()));
        cacheManager.setCaches(caches);
        return cacheManager;
    }
}

3.4 使用缓存注解

Spring 提供了简洁的注解来操作缓存。

常用注解:

  • @Cacheable:方法执行前检查缓存,存在则返回,不存在则执行方法并缓存结果。适合查询方法。
  • @CachePut:总是执行方法,并用结果更新缓存。适合更新/保存方法。
  • @CacheEvict:删除缓存。适合删除方法。
  • @Caching:组合多个缓存操作。
  • @CacheConfig:在类级别共享缓存配置。

使用示例:

@Service
public class UserService {
    // 查询:缓存名为userCache,key为参数id,支持同步(防止缓存击穿)
    @Cacheable(value = \"userCache\", key = \"#id\", sync = true)
    public User getUserById(Long id) {
        // ... 从数据库查询
        return user;
    }
    // 更新:总是执行,并用返回值更新缓存中key为#user.id的条目
    @CachePut(value = \"userCache\", key = \"#user.id\")
    public User saveUser(User user) {
        // ... 保存到数据库
        return user;
    }
    // 删除:删除对应key的缓存
    @CacheEvict(value = \"userCache\", key = \"#id\")
    public void deleteUser(Long id) {
        // ... 从数据库删除
    }
}
SpEL 表达式在缓存注解中的使用: 在注解的 keycondition 等属性中,可以使用 SpEL 表达式引用方法参数、返回值等上下文数据。 表达式 描述 示例
#root.methodName 当前方法名 #root.methodName
#id#p0 方法的第一个参数(参数名为id) key = \"#id\"
#result 方法的返回值(仅限@CachePut, @CacheEvictcondition等) unless = \"#result == null\"
#a0 / #p0 第一个参数 key = \"#p0\"

Caffeine Cache 凭借其先进的 W-TinyLFU 算法,在命中率上表现卓越,同时提供了丰富而灵活的配置选项。通过与 Spring Boot 无缝集成,开发者可以轻松构建高效的本地缓存层,显著提升应用性能,尤其在处理高并发读场景时优势明显。

您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-3 13:46 , Processed in 0.084044 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表