文章

Caffeine 原理及实践

前言

Caffeine 是一个高性能的 Java 缓存库,它提供了近乎最佳的命中率,同时具有非常出色的读写性能。Caffeine 的设计借鉴了 Guava Cache 的 API,但在内部实现上进行了重大改进,使其成为目前 Java 生态系统中最先进的缓存解决方案之一。

缓存

缓存是一种用于存储频繁访问数据的技术,目的是提高数据检索速度,减少对原始数据源的访问,从而提升系统整体性能。在分布式系统和高并发场景中,合理使用缓存可以显著降低系统负载,提高响应速度。

常见的缓存类型包括:

  • 本地缓存:如 Caffeine、Guava Cache
  • 分布式缓存:如 Redis、Memcached
  • 多级缓存:结合本地缓存和分布式缓存

Caffeine 原理

Caffeine 的核心原理围绕着两个主要方面:高效的缓存淘汰算法和优化的并发读写机制。

淘汰算法

Caffeine 使用了一种称为 Window TinyLFU(W-TinyLFU)的淘汰算法。这是一个复合算法,结合了以下几个部分:

  1. Admission Window:新项目首先进入一个小的 admission window。这个窗口使用简单的 FIFO(先进先出)策略。

  2. TinyLFU:一个频率统计器,用于记录项目的访问频率。它使用了一种称为 Count-Min Sketch 的概率数据结构来高效地统计频率。

  3. Main Cache:主缓存区域,使用 SLRU(Segmented Least Recently Used)策略管理。

当缓存需要淘汰项目时,新项目会与 main cache 中最近最少使用的项目进行频率比较。如果新项目的频率更高,它会被允许进入 main cache,否则会被丢弃。

这种算法能够有效地平衡新鲜度和频率,提供接近最优的命中率。

高性能读写

Caffeine 采用了多项技术来确保高性能的并发读写:

  1. 并发 Hash 表:使用优化的并发 Hash 表作为底层数据结构,支持高并发的读写操作。

  2. 写入缓冲:采用了类似 CPU 写缓冲的机制,将写操作暂存,以批量方式异步处理,减少锁竞争。

  3. 异步化:支持异步加载和异步刷新,避免同步操作阻塞线程。

  4. 细粒度锁:使用分段锁和 CAS 操作,最小化锁竞争。

  5. 引用处理:支持 weak 和 soft 引用,允许缓存根据 JVM 的内存压力自动调整大小。

Caffeine 实践

配置说明

Caffeine 提供了灵活的配置选项,主要包括:

  • 缓存大小:可以限制缓存项数量或总权重
  • 过期策略:支持基于时间的过期(访问后或写入后)
  • 引用类型:支持 weak 和 soft 引用
  • 统计功能:可以启用统计以监控缓存性能

缓存加载方式

Caffeine 支持三种主要的缓存加载方式:

1. Cache 手动加载

手动加载方式需要显式地将值放入缓存:

Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000).build();

// 手动加载
Graph graph = cache.get(key, k -> createExpensiveGraph(k));

2. Loading Cache 自动创建

Loading Cache 会在缓存未命中时自动加载值:

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));

// 自动加载
Graph graph = cache.get(key);

3. Async Cache 异步获取

Async Cache 提供了异步加载和检索值的能力:

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES).buildAsync(key -> createExpensiveGraph(key));

// 异步获取
CompletableFuture<Graph> future = cache.get(key);

淘汰策略

Caffeine 提供了多种淘汰策略:

  1. 基于大小:

    .maximumSize(10_000)
    
  2. 基于权重:

    .maximumWeight(100_000).weigher((key, value) -> value.size())
    
  3. 基于时间:

    .expireAfterAccess(5, TimeUnit.MINUTES)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    
  4. 基于引用:

    .weakKeys()
    .weakValues()
    .softValues()
    

刷新策略

Caffeine 支持自动刷新缓存项:

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .refreshAfterWrite(1, TimeUnit.MINUTES).build(key -> createExpensiveGraph(key));

刷新操作是异步执行的,不会阻塞读取操作。

统计

Caffeine 提供了丰富的统计信息:

Cache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000).recordStats()
    .build();

// 获取统计信息
CacheStats stats = cache.stats();
System.out.println(stats.hitRate());
System.out.println(stats.evictionCount());

SpringBoot 整合 Caffeine

1. 相关依赖

pom.xml 中添加以下依赖:

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

2. 常用注解

  • @EnableCaching:启用缓存支持
  • @Cacheable:将方法的返回值存储到缓存中
  • @CachePut:更新缓存
  • @CacheEvict:从缓存中移除特定数据

3. 常用注解属性

  • cacheNames/value:指定缓存名称
  • key:缓存的 key,支持 SpEL 表达式
  • condition:缓存的条件,支持 SpEL 表达式
  • unless:否定缓存的条件,支持 SpEL 表达式
  • sync:是否使用同步模式

4. 缓存同步模式

使用 sync = true 可以防止缓存击穿:

@Cacheable(cacheNames = "user", key = "#id", sync = true)
public User getUser(Long id) {
    // ...
}

5. 示例

配置 Caffeine 缓存:

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

使用缓存:

@Service
public class UserService {
    @Cacheable(cacheNames = "users", key = "#id")
    public User getUser(Long id) {
        // 从数据库获取用户
    }

    @CachePut(cacheNames = "users", key = "#user.id")
    public User updateUser(User user) {
        // 更新用户并返回更新后的用户
    }

    @CacheEvict(cacheNames = "users", key = "#id")
    public void deleteUser(Long id) {
        // 删除用户
    }
}

通过这种方式,我们可以轻松地在 Spring Boot 应用中集成 Caffeine 缓存,利用其高性能特性来提升应用性能。

总结

Caffeine 作为一个高性能的 Java 缓存库,通过其先进的淘汰算法和优化的并发机制,为开发者提供了一个强大而灵活的缓存解决方案。它不仅能够提供接近最优的缓存命中率,还能在高并发场景下保持出色的性能。

在实践中,Caffeine 提供了多种缓存加载方式、灵活的配置选项以及丰富的统计信息,使得开发者可以根据具体需求进行精细化的缓存管理。结合 Spring Boot 使用时,通过注解可以方便地实现缓存操作,进一步简化了开发流程。

通过合理使用 Caffeine,我们可以显著提升应用的性能,减少对后端存储的访问压力,从而构建更高效、更可扩展的系统。在选择和使用缓存时,需要根据具体的业务场景和性能需求,合理配置缓存参数,并持续监控和优化缓存效果,以获得最佳的系统性能。

License:  CC BY 4.0