Java最强本地缓存——Caffeine

Yonagi833 2024-10-08 15:35:01 阅读 80

缓存(Cache)在计算机中无处不在。缓存本质上是一种用空间换时间的手段——通过将数据存储在更快的存储媒介上,减少响应的时间,使得下一次访问这些数据的时候能够获得加速的效果。从缓存类型来看,缓存还分为 本地缓存分布式缓存 两种类型。分布式缓存(如 Redis)除了要解决数据读取效率问题外,还要解决集群环境下的数据不一致问题。本文将简单地介绍一种高性能的本地缓存库——Caffeine,以及Caffeine 的使用、驱逐策略、刷新策略,以及如何在 SpringBoot 中使用 Caffeine

Caffeine 简介

Caffeine 是基于Java 1.8的高性能本地缓存库,由 Guava 演变而来,它的性能比Guava也更好,官方声称在基准测试中, Caffeine 的缓存命中率已经接近于最优值,且 Caffeine 的内存占用情况也优于 Guava

实际上,Caffeine 这种本地缓存和 ConcurrentHashMap 很像——都支持并发,都支持 O(1) 时间复杂度的存取。两者的主要区别在于:

ConcurrentHashMap 会存储所有存入的数据,且数据移除需要进行显式的操作Caffeine 将通过给定的配置,自动移除不常用的数据,节约内存空间

因此,我们可以粗略地将Caffeine视为带有淘汰策略的ConcurrentHashMap

根据 Caffeine 官方提供的文档,Caffeine提供的功能如下:

自动加载元素到缓存当中,异步加载的方式也可供选择当达到最大容量的时候可以使用基于就近度和频率的算法进行基于容量的驱逐将根据缓存中的元素上一次访问或者被修改的时间进行基于过期时间的驱逐当向缓存中一个已经过时的元素进行访问的时候将会进行异步刷新key将自动被弱引用所封装value将自动被弱引用或者软引用所封装驱逐(或移除)缓存中的元素时将会进行通知写入传播到一个外部数据源当中持续计算缓存的访问统计指标

在 Java 中使用 Caffeine

为了使用 Caffeine,在 Maven 工程下引入以下依赖:

<code><dependency>

<groupId>com.github.ben-manes.caffeine</groupId>

<artifactId>caffeine</artifactId>

<version>2.9.3</version>

</dependency>

Caffeine 提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载

手动加载

Cache<Key, Graph> cache = Caffeine.newBuilder()

.expireAfterWrite(10, TimeUnit.MINUTES)

.maximumSize(10_000)

.build();

// 查找一个缓存元素, 没有查找到的时候返回null

Graph graph = cache.getIfPresent(key);

// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null

graph = cache.get(key, k -> createExpensiveGraph(key));

// 添加或者更新一个缓存元素

cache.put(key, graph);

// 移除一个缓存元素

cache.invalidate(key);

Cache 接口提供了显式搜索查找、更新和移除缓存元素的能力

cache.put(key, value) 操作将会直接写入或更新缓存中的缓存元素,在缓存中已经存在的该 key 对应的缓存值都会被直接覆盖

cache.get(key, k -> value) 操作来在缓存中不存在该key对应的缓存元素的时候进行计算生成并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。如果缓存的元素无法生成或在生成的过程中抛出了异常导致生成失败,cache.get 可能会返回null

通过调用 cache.invalidate(key) 方法来移除缓存

自动加载

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()

.maximumSize(10_000)

.expireAfterWrite(10, TimeUnit.MINUTES)

.build(key -> createExpensiveGraph(key));

// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null

Graph graph = cache.get(key);

// 批量查找缓存,如果缓存不存在则生成缓存元素

Map<Key, Graph> graphs = cache.getAll(keys);

LoadingCache 是一个 Cache 附加上 CacheLoader 能力后的缓存实现

当缓存不存在时,如果调用了get()方法,则会调用CacheLoader.load()方法加载最新值。通过 getAll() 可以达到批量查找缓存的目的。通常情况下,getAll() 方法会对每个不存在对应缓存的key调用一次 CacheLoader.load() 来生成缓存元素

使用 LoadingCache 时,需要指定 CacheLoader ,并实现其中的 load() 方法供缓存缺失时的自动加载

手动异步加载

AsyncCache<Key, Graph> cache = Caffeine.newBuilder()

.expireAfterWrite(10, TimeUnit.MINUTES)

.maximumSize(10_000)

.buildAsync();

// 查找一个缓存元素, 没有查找到的时候返回null

CompletableFuture<Graph> graph = cache.getIfPresent(key);

// 查找缓存元素,如果不存在,则异步生成

graph = cache.get(key, k -> createExpensiveGraph(key));

// 添加或者更新一个缓存元素

cache.put(key, graph);

// 移除一个缓存元素

cache.synchronous().invalidate(key);

AsyncCacheCache 的一个变体,响应结果均为 CompletableFuture。默认情况下,缓存计算使用 ForkJoinPool.commonPool()作为线程池,如果想要指定线程池,可以覆盖并实现 Caffeine.executor(Executor) 方法

synchronous() 提供了阻塞直到异步缓存生成完毕的能力,它将以 Cache 进行返回

自动异步加载

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()

.maximumSize(10_000)

.expireAfterWrite(10, TimeUnit.MINUTES)

// 你可以选择: 去异步的封装一段同步操作来生成缓存元素

.buildAsync(key -> createExpensiveGraph(key));

// 你也可以选择: 构建一个异步缓存元素操作并返回一个future

.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

// 查找缓存元素,如果其不存在,将会异步进行生成

CompletableFuture<Graph> graph = cache.get(key);

// 批量查找缓存元素,如果其不存在,将会异步进行生成

CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

一个 AsyncLoadingCache 是一个 AsyncCache 加上 AsyncCacheLoader 能力的实现

与自动加载 LoadingCache 类似,AsyncLoadingCache 也需要指定 CacheLoader ,同时需要实现 load() 方法供供缓存缺失时的自动加载。

默认以 ForkJoinPool.commonPool()作为线程池来提交,如果想要指定线程池,可以覆盖并实现 AsyncCacheLoader.aysncLoad() 方法

驱逐策略

驱逐策略在创建缓存的时候进行指定。常用的有基于容量的驱逐和基于时间的驱逐。基于容量的驱逐需要指定缓存容量的最大值。当缓存容量达到最大时,Caffeine将使用LRU策略对缓存进行淘汰;基于时间的驱逐策略如字面意思,可以设置在最后访问/写入一个缓存经过指定时间后,自动进行淘汰。

驱逐策略可以自由组合,在任意驱逐策略生效后,该缓存将会被清除

// 创建最大容量为1000的缓存

Caffeine.newBuilder().maximumSize(1000).build();

// 创建一个写入10h后过期的缓存

Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.HOURS).build();

// 创建一个访问1h后过期的缓存

Caffeine.newBuilder().expireAfterAccess(1, TimeUnit.HOURS).build();

刷新机制

刷新机制与驱逐完全不同,它可以通过 LoadingCache.refresh(K) 方法,异步地为 key 对应的缓存元素刷新一个新的值。与驱逐不同的是,在刷新时候如果查询缓存元素,其旧值将会被返回,知道元素刷新完毕后才能返回刷新后的新值

使用 refreshAfterWrite() 机制,Caffeine将在 key 允许刷新后的首次访问时,立即返回旧值,同时异步地对缓存值进行刷新,使得调用方不至于因为缓存驱逐而被阻塞。刷新机制只能适用于自动加载和自动异步加载

通过覆写 Cache.reload() 方法,将在刷新时使得旧缓存值参与其中

刷新操作将会异步执行在一个 Executor 上,默认的线程池实现是 ForkJoinPool.commonPool() ,也可以通过覆盖 Caffeine.executor(Executor) 方法自定义线程池的实现

Caffeine.newBuilder().refreshAfterWrite(10, TimeUnit.MINUTES).build(k -> create(k));

统计

Caffeine 内置了数据收集功能,通过 Caffeine.recordStats() 方法可以打开数据收集功能。Cache.stats() 方法将会返回一个 CacheStats 对象,将会包含一些统计指标,例如

hitRate() 缓存命中率evictionCount() 被驱逐的缓存数量averageLoadPenalty() 新值被载入的平均耗时

在 SpringBoot 中使用 Caffeine

除了上面提及的方法之外,在 SpringBoot框架中,我们有一些更加方便的配置方法和管理功能

SpringBoot 缓存管理器

Spring从3.1版本开始就引入了对 Cache 的支持。定义了 org.springframework.cache.Cacheorg.springframework.cache.CacheManager 接口,来统一不同的缓存技术,支持使用 JCache(JSR-107) 注解来简化开发

Cache 接口包括了对缓存的各种操作集合,实际操作缓存时,是通过这些接口进行操作Cache 接口下提供了各种 xxxCache 的实现,由于 Spring 从2.0版本后把默认的缓存组件由 Guava 替换为 Caffeine,因此这里需要用到的就是 CaffeineCache 类。CacheManager 定义了创建、配置、获取、管理和控制多个唯一命名的的 Cache。这些 Cache 存在于 CacheManager 的上下文中

创建缓存管理器

@Bean

public CacheManager cacheManager() {

SimpleCacheManager cacheManager = new SimpleCacheManager();

ArrayList<CaffeineCache> caches = new ArrayList<>();

caches.add(new CaffeineCache(cacheName(), generateCache()));

cacheManager.setCaches(caches);

return cacheManager;

}

需要注意的是,Spring 只支持手动加载和自动加载缓存,无法支持异步缓存

使用 @Cacheable 注解

为了使用 @Cacheable 注解,需要引入 Maven 依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-cache</artifactId>

</dependency>

与 @Cacheable 相关的常用注解包括:

@Cachable:表示该方法支持缓存。当调用被注解的方法时,如果对应的键已经存在缓存,则不再执行方法体,而是从缓存中直接返回。@CachePut:表示执行该方法后,其值将作为最新结果更新到缓存中@CacheEvict:表示执行该方法后,将执行缓存清除操作@Caching:组合前三个注解

常用注解属性

@Cacheable 常用的注解属性如下:

cacheNames/value:缓存组件的名字,即 cacheManager 中缓存的名称key:缓存数据时使用的 keykeyGenerator:key 和 keyGenerator 二选一cacheManager:指定使用的缓存管理器condition:在方法执行开始前检查,在符合 condition 的情况下,进行缓存unless:在方法执行完成后检查,在符合 unless 的情况下,进行缓存sync:是否进行同步模式,若使用同步模式,在多个线程同时对一个 key 进行 load 时,其他线程将被阻塞

缓存同步模式

@Cacheable 注解支持配置同步模式,在不同的 Caffeine 配置下,对是否开启同步模式进行观察

Caffeine缓存类型 是否开启同步 多线程读取不存在/被驱逐的 key 多线程读取待刷新的 key
Cache 各自独立执行被注解方法 -
Cache 线程1执行被注解方法,线程2阻塞,直到缓存更新完成 -
LoadingCache 线程1执行 load(),线程2被阻塞,直到缓存更新完成 线程1使用旧值立即返回,并异步更新缓存值;线程2立即返回,不进行更新
LoadingCache 线程1执行被注解方法,线程2阻塞,直到缓存更新完成 线程1使用旧值立即返回,并异步更新缓存值;线程2立即返回,不进行更新

在 Cache 中,sync 表示是否需要所有线程同步等待在 LoadingCache 中,sync 表示在读取不存在/被驱逐的 key 时,是否执行被注解方法



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。