JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

Spring Cache高性能缓存库 - Caffeine简介

wys521 2025-06-10 02:30:14 精选教程 3 ℃ 0 评论

概述

本地缓存Cache和Map之间相比,一个最基本区别是缓存Cache具备驱逐策略,用于自动删除存储对象的功能。

缓存Cache驱逐策略决定了在任何给定的时间应该删除哪些对象,此策略直接影响缓存的命中率,这是缓存库的一个关键特性。

Caffeine使用Window TinyLfu驱逐算法策略,提供了接近最佳的命中率。

依赖关系

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

填充缓存

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

比如我们先创建一个将存储在缓存中的数据类:

class DataObject {
    private final String data;

    private static int objectCounter = 0;
    // standard constructors/getters
    
    public static DataObject get(String data) {
        objectCounter++;
        return new DataObject(data);
    }
}

手动填充策略

按这种策略,我们手动先将值放入缓存中,然后再进行检索。

先初始化缓存:

Cache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();

现在可以使用getIfPresent方法从缓存中获取一些值。如果缓存中不存在该值,则此方法将返回null:

String key = "A";
DataObject dataObject = cache.getIfPresent(key);

assertNull(dataObject);

我们可以使用put方法手动填充缓存:

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);

assertNotNull(dataObject);

我们也可以使用get方法获取值,该方法将一个Function和一个键值作为参数。如果缓存中不存在该键值,则此函数将用于提供返回值,该值将在计算后插入到缓存中:

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));

assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());

get方法以原子方式执行计算。这意味着计算将只进行一次,即使几个线程同时请求该值。

有时我们需要手动使一些缓存的值无效:

cache.invalidate(key);
dataObject = cache.getIfPresent(key);

assertNull(dataObject);

同步加载策略

这种加载缓存的方法采用一个Function,用于初始化值,类似于手动策略的get方法。

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

现在我们可以使用get方法检索这些值:

DataObject dataObject = cache.get(key);

assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());

我们还可以使用getAll方法获得一组值:

Map<String, DataObject> dataObjectMap 
  = cache.getAll(Arrays.asList("A", "B", "C"));

assertEquals(3, dataObjectMap.size());

异步加载策略

异步执行操作返回一个CompletableFuture,其中包含实际值:

AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .buildAsync(k -> DataObject.get("Data for " + k));
String key = "A";

cache.get(key).thenAccept(dataObject -> {
    assertNotNull(dataObject);
    assertEquals("Data for " + key, dataObject.getData());
});

cache.getAll(Arrays.asList("A", "B", "C"))
  .thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));

基于缓存大小的驱逐策略

当超过配置的缓存大小限制时逐出对象。

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(1)
  .build(k -> DataObject.get("Data for " + k));

assertEquals(0, cache.estimatedSize());

cache.get("A");
assertEquals(1, cache.estimatedSize());

我们可以将第二个值添加到缓存中,从而删除第一个值:

cache.get("B");
cache.cleanUp();

assertEquals(1, cache.estimatedSize());

我们在获取缓存大小之前调用cleanUp方法。这是因为缓存驱逐是异步执行的,此方法可以等待驱逐完成。

基于时间的驱逐策略

  • 访问后过期(Expire after access):自上次读取或写入发生后经过一段时间后,条目过期
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterAccess(5, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));
  • 写入后过期(Expire after write):自上次写入发生后经过一段时间后,条目即过期
cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));

自动刷新策略

可以将缓存配置为在定义的时间段后自动刷新条目。

Caffeine.newBuilder()
  .refreshAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

expireAfter和refreshAfter之间的区别:当请求过期的条目时,执行会阻塞,直到构建函数计算出新值为止。

但是,如果条目符合刷新条件,那么缓存将返回一个旧值并异步重新加载该值。

统计

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .recordStats()
  .build(k -> DataObject.get("Data for " + k));
cache.get("A");
cache.get("A");

assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());

结论

Caffeine作为Spring默认缓存库,常与Spring Boot Cache注解结合,用于需要驱逐缓存数据的场景。与Guava Cache相比,后者常用于本地数据缓存的场景。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表