编辑此页面

应用程序数据缓存

在本指南中,您将学习如何在 Quarkus 应用程序的任何 CDI 管理 bean 中启用应用程序数据缓存。

先决条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

  • 已安装 JDK 17+ 并正确配置了 JAVA_HOME

  • Apache Maven 3.9.9

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

场景

假设您想在 Quarkus 应用程序中公开一个 REST API,允许用户检索未来三天的天气预报。问题在于您必须依赖一个外部气象服务,该服务一次只接受一天的请求,并且需要很长时间才能回复。由于天气预报每十二小时更新一次,因此缓存服务响应肯定会提高 API 的性能。

我们将使用单个 Quarkus 注解来实现这一点。

在本指南中,我们使用默认的 Quarkus Cache 后端(Caffeine)。您可以使用 Infinispan 或 Redis 代替。请参阅 Infinispan 缓存后端参考以配置 Infinispan 后端。请参阅 Redis 缓存后端参考以配置 Redis 后端。

解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载 存档

解决方案位于 cache-quickstart 目录中。

创建 Maven 项目

首先,我们需要使用以下命令创建一个新的 Quarkus 项目

CLI
quarkus create app org.acme:cache-quickstart \
    --extension='cache,rest-jackson' \
    --no-code
cd cache-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=cache-quickstart \
    -Dextensions='cache,rest-jackson' \
    -DnoCode
cd cache-quickstart

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=cache-quickstart"

此命令生成项目并导入 cacherest-jackson 扩展。

如果您已经配置了 Quarkus 项目,则可以通过在项目基本目录中运行以下命令将 cache 扩展添加到您的项目中

CLI
quarkus extension add cache
Maven
./mvnw quarkus:add-extension -Dextensions='cache'
Gradle
./gradlew addExtension --extensions='cache'

这会将以下内容添加到您的构建文件中

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-cache</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-cache")

创建 REST API

让我们首先创建一个服务,该服务将模拟对外部气象服务的一次极其缓慢的调用。创建 src/main/java/org/acme/cache/WeatherForecastService.java,内容如下

package org.acme.cache;

import java.time.LocalDate;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class WeatherForecastService {

    public String getDailyForecast(LocalDate date, String city) {
        try {
            Thread.sleep(2000L); (1)
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return date.getDayOfWeek() + " will be " + getDailyResult(date.getDayOfMonth() % 4) + " in " + city;
    }

    private String getDailyResult(int dayOfMonthModuloFour) {
        switch (dayOfMonthModuloFour) {
            case 0:
                return "sunny";
            case 1:
                return "cloudy";
            case 2:
                return "chilly";
            case 3:
                return "rainy";
            default:
                throw new IllegalArgumentException();
        }
    }
}
1 这就是速度慢的原因。

我们还需要一个类来包含在用户询问未来三天的天气预报时发送给用户的响应。创建 src/main/java/org/acme/cache/WeatherForecast.java,如下所示

package org.acme.cache;

import java.util.List;

public class WeatherForecast {

    private List<String> dailyForecasts;

    private long executionTimeInMs;

    public WeatherForecast(List<String> dailyForecasts, long executionTimeInMs) {
        this.dailyForecasts = dailyForecasts;
        this.executionTimeInMs = executionTimeInMs;
    }

    public List<String> getDailyForecasts() {
        return dailyForecasts;
    }

    public long getExecutionTimeInMs() {
        return executionTimeInMs;
    }
}

现在,我们只需要创建 REST 资源。创建 src/main/java/org/acme/cache/WeatherForecastResource.java 文件,内容如下

package org.acme.cache;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.RestQuery;

@Path("/weather")
public class WeatherForecastResource {

    @Inject
    WeatherForecastService service;

    @GET
    public WeatherForecast getForecast(@RestQuery String city, @RestQuery long daysInFuture) { (1)
        long executionStart = System.currentTimeMillis();
        List<String> dailyForecasts = Arrays.asList(
                service.getDailyForecast(LocalDate.now().plusDays(daysInFuture), city),
                service.getDailyForecast(LocalDate.now().plusDays(daysInFuture + 1L), city),
                service.getDailyForecast(LocalDate.now().plusDays(daysInFuture + 2L), city));
        long executionEnd = System.currentTimeMillis();
        return new WeatherForecast(dailyForecasts, executionEnd - executionStart);
    }
}
1 如果省略了 daysInFuture 查询参数,则未来三天的天气预报将从当天开始。否则,它将从当前日期加上 daysInFuture 值开始。

我们都完成了!让我们检查一下一切是否正常。

首先,使用项目目录中的开发模式运行应用程序

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

然后,从浏览器调用 https://:8080/weather?city=Raleigh。经过漫长的六秒钟后,应用程序将回答类似以下内容

{"dailyForecasts":["MONDAY will be cloudy in Raleigh","TUESDAY will be chilly in Raleigh","WEDNESDAY will be rainy in Raleigh"],"executionTimeInMs":6001}

响应内容可能会因您运行代码的日期而异。

您可以尝试一次又一次地调用相同的 URL,它总是需要六秒钟才能回答。

启用缓存

现在您的 Quarkus 应用程序已启动并运行,让我们通过缓存外部气象服务响应来极大地提高其响应时间。更新 WeatherForecastService 类,如下所示

package org.acme.cache;

import java.time.LocalDate;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class WeatherForecastService {

    @CacheResult(cacheName = "weather-cache") (1)
    public String getDailyForecast(LocalDate date, String city) {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return date.getDayOfWeek() + " will be " + getDailyResult(date.getDayOfMonth() % 4) + " in " + city;
    }

    private String getDailyResult(int dayOfMonthModuloFour) {
        switch (dayOfMonthModuloFour) {
            case 0:
                return "sunny";
            case 1:
                return "cloudy";
            case 2:
                return "chilly";
            case 3:
                return "rainy";
            default:
                throw new IllegalArgumentException();
        }
    }
}
1 我们只添加了这个注解(以及相关的导入,当然)。

让我们尝试再次调用 https://:8080/weather?city=Raleigh。您仍然需要等待很长时间才能收到答案。这是正常的,因为服务器刚刚重新启动并且缓存为空。

等一下!在 WeatherForecastService 更新后,服务器自行重新启动了吗?是的,这是 Quarkus 为开发者提供的惊人功能之一,称为 live coding

现在缓存已在上一次调用期间加载,请尝试调用相同的 URL。这一次,您应该获得一个超快速的答案,其 executionTimeInMs 值接近 0。

让我们看看如果我们使用 https://:8080/weather?city=Raleigh&daysInFuture=1 URL 从未来的一天开始会发生什么。您应该在两秒钟后得到答案,因为请求的天数中的两天已经加载到缓存中。

您还可以尝试使用不同的城市调用相同的 URL,并再次查看缓存的实际效果。第一次调用将花费六秒钟,而随后的调用将立即得到响应。

恭喜!您只需一行代码就将应用程序数据缓存添加到 Quarkus 应用程序!

您想了解更多关于 Quarkus 应用程序数据缓存能力的信息吗?以下各节将向您展示您需要了解的一切。

使用注解进行缓存

Quarkus 提供了一组注解,可以在 CDI 管理的 bean 中使用以启用缓存功能。

不允许在私有方法上使用缓存注解。它们可以与任何其他访问修饰符一起正常工作,包括包私有(没有显式修饰符)。

@CacheResult

尽可能从缓存加载方法结果,而无需执行方法体。

当调用使用 @CacheResult 注解的方法时,Quarkus 将计算缓存键,并使用它来检查缓存中是否已经调用了该方法。请参阅本指南的 缓存键构建逻辑部分,了解如何计算缓存键。如果在缓存中找到一个值,则返回该值并且永远不会实际执行注解的方法。如果未找到任何值,则调用注解的方法,并将返回的值存储在缓存中使用计算的键。

使用 CacheResult 注解的方法受到缓存未命中锁机制的保护。如果多个并发调用尝试从同一缺失键检索缓存值,则该方法只会调用一次。第一个并发调用将触发方法调用,而随后的并发调用将等待方法调用结束以获取缓存结果。lockTimeout 参数可用于在给定的延迟后中断锁定。默认情况下禁用锁定超时,这意味着永远不会中断锁定。有关更多详细信息,请参阅参数 Javadoc。

此注解不能用于返回 void 的方法。

与底层 Caffeine 提供程序不同,Quarkus 还能缓存 null 值。请参阅 下面有关此主题的更多信息

@CacheInvalidate

从缓存中删除一个条目。

当调用使用 @CacheInvalidate 注解的方法时,Quarkus 将计算缓存键,并使用它来尝试从缓存中删除现有条目。请参阅本指南的 缓存键构建逻辑部分,了解如何计算缓存键。如果该键未标识任何缓存条目,则不会发生任何事情。

@CacheInvalidateAll

当调用使用 @CacheInvalidateAll 注解的方法时,Quarkus 将从缓存中删除所有条目。

@CacheKey

当使用 @CacheKey 注解方法参数时,在调用使用 @CacheResult@CacheInvalidate 注解的方法期间,该参数将被标识为缓存键的一部分。

此注解是可选的,仅应在某些方法参数不是缓存键的一部分时使用。

缓存键构建逻辑

缓存键由注解 API 使用以下逻辑构建

  • 如果在 @CacheResult@CacheInvalidate 注解中声明了 io.quarkus.cache.CacheKeyGenerator,则使用它来生成缓存键。可能存在于某些方法参数上的 @CacheKey 注解将被忽略。

  • 否则,如果该方法没有参数,则缓存键是根据缓存名称构建的 io.quarkus.cache.DefaultCacheKey 的实例。

  • 否则,如果该方法只有一个参数,则该参数是缓存键。

  • 否则,如果该方法有多个参数但只有一个使用 @CacheKey 注解,则该注解的参数是缓存键。

  • 否则,如果该方法有多个使用 @CacheKey 注解的参数,则缓存键是根据这些注解的参数构建的 io.quarkus.cache.CompositeCacheKey 的实例。

  • 否则,如果该方法有多个参数并且没有一个参数使用 @CacheKey 注解,则缓存键是根据所有方法参数构建的 io.quarkus.cache.CompositeCacheKey 的实例。

每个作为键的一部分的非原始方法参数必须正确实现 equals()hashCode(),以使缓存按预期工作。

当缓存键从多个方法参数构建时,无论它们是否使用 @CacheKey 显式标识,构建逻辑都取决于这些参数在方法签名中的顺序。另一方面,参数名称根本不使用,并且对缓存键没有任何影响。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class CachedService {

    @CacheResult(cacheName = "foo")
    public Object load(String keyElement1, Integer keyElement2) {
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate1(String keyElement2, Integer keyElement1) { (1)
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate2(Integer keyElement2, String keyElement1) { (2)
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate3(Object notPartOfTheKey, @CacheKey String keyElement1, @CacheKey Integer keyElement2) { (3)
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate4(Object notPartOfTheKey, @CacheKey Integer keyElement2, @CacheKey String keyElement1) { (4)
    }
}
1 即使键元素的名称已交换,调用此方法也将使 load 方法缓存的值失效。
2 调用此方法不会使 load 方法缓存的值失效,因为键元素的顺序不同。
3 调用此方法将使 load 方法缓存的值失效,因为键元素的顺序相同。
4 调用此方法不会使 load 方法缓存的值失效,因为键元素的顺序不同。

使用 CacheKeyGenerator 生成缓存键

您可能希望在缓存键中包含超过方法参数的内容。这可以通过实现 io.quarkus.cache.CacheKeyGenerator 接口并在 @CacheResult@CacheInvalidate 注解的 keyGenerator 字段中声明该实现来完成。

该类必须表示 CDI bean 或声明一个公共的无参数构造函数。如果它表示 CDI bean,则将在缓存键计算期间注入键生成器。否则,将使用其默认构造函数为每个缓存键计算创建一个键生成器的新实例。

在 CDI 的情况下,必须只有一个 bean 在其 bean 类型集中具有该类,否则构建将失败。当调用 CacheKeyGenerator#generate() 方法时,与 bean 作用域关联的上下文必须处于活动状态。如果作用域是 @Dependent,则当 CacheKeyGenerator#generate() 方法完成时,bean 实例将被销毁。

以下键生成器将作为 CDI bean 注入

package org.acme.cache;

import java.lang.reflect.Method;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import io.quarkus.cache.CacheKeyGenerator;
import io.quarkus.cache.CompositeCacheKey;

@ApplicationScoped
public class ApplicationScopedKeyGen implements CacheKeyGenerator {

    @Inject
    AnythingYouNeedHere anythingYouNeedHere; (1)

    @Override
    public Object generate(Method method, Object... methodParams) { (2)
        return new CompositeCacheKey(anythingYouNeedHere.getData(), methodParams[1]); (3)
    }
}
1 可以通过在键生成器中注入 CDI bean 将外部数据包含到缓存键中。
2 使用 Method 时请小心,它的某些方法可能很昂贵。
3 在从索引访问方法参数之前,请确保该方法有足够的参数。否则,在缓存键计算期间可能会抛出 IndexOutOfBoundsException

以下键生成器将使用其默认构造函数实例化

package org.acme.cache;

import java.lang.reflect.Method;

import io.quarkus.cache.CacheKeyGenerator;
import io.quarkus.cache.CompositeCacheKey;

public class NotABeanKeyGen implements CacheKeyGenerator {

    // CDI injections won't work here because it's not a CDI bean.

    @Override
    public Object generate(Method method, Object... methodParams) {
        return new CompositeCacheKey(method.getName(), methodParams[0]); (1)
    }
}
1 Method 中的其他方法不同,将方法名称包含到缓存键中并不昂贵。

两种键生成器都可以以类似的方式使用

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import org.acme.cache.ApplicationScopedKeyGen;
import org.acme.cache.NotABeanKeyGen;

import io.quarkus.cache.CacheKey;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class CachedService {

    @CacheResult(cacheName = "foo", keyGenerator = ApplicationScopedKeyGen.class) (1)
    public Object load(@CacheKey Object notUsedInKey, String keyElement) { (2)
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo", keyGenerator = NotABeanKeyGen.class) (3)
    public void invalidate(Object keyElement) {
    }

    @CacheInvalidate(cacheName = "foo", keyGenerator = NotABeanKeyGen.class)
    @CacheInvalidate(cacheName = "bar")
    public void invalidate(Integer param0, @CacheKey BigDecimal param1) { (4)
    }
}
1 此键生成器是一个 CDI bean。
2 @CacheKey 注解将被忽略,因为在 @CacheResult 注解中声明了键生成器。
3 此键生成器不是 CDI bean。
4 foo 缓存数据失效时,@CacheKey 注解将被忽略,但当 bar 缓存数据失效时,param1 将是缓存键。

使用编程式 API 进行缓存

Quarkus 还提供了一个编程式 API,可用于存储、检索或删除使用注解 API 声明的任何缓存中的值。编程式 API 中的所有操作都是非阻塞的,并且在底层依赖于 Mutiny

在以编程方式访问缓存数据之前,您需要检索一个 io.quarkus.cache.Cache 实例。以下各节将向您展示如何执行此操作。

使用 @CacheName 注解注入 Cache

io.quarkus.cache.CacheName 可以在字段、构造函数参数或方法参数上使用,以注入 Cache

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class CachedExpensiveService {

    @Inject (1)
    @CacheName("my-cache")
    Cache cache;

    public Uni<String> getNonBlockingExpensiveValue(Object key) { (2)
        return cache.get(key, k -> { (3)
            /*
             * Put an expensive call here.
             * It will be executed only if the key is not already associated with a value in the cache.
             */
        });
    }

    public String getBlockingExpensiveValue(Object key) {
        return cache.get(key, k -> {
            // Put an expensive call here.
        }).await().indefinitely(); (4)
    }
}
1 这是可选的。
2 此方法返回 Uni<String> 类型,该类型是非阻塞的。
3 k 参数包含缓存键值。
4 如果您不需要调用是非阻塞的,那么这就是您以阻塞方式检索缓存值的方式。

CacheManager 检索 Cache

检索 Cache 实例的另一种方法是首先注入 io.quarkus.cache.CacheManager,然后从其名称检索所需的 Cache

package org.acme.cache;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheManager;

import java.util.Optional;

@Singleton
public class CacheClearer {

    private final CacheManager cacheManager;

    public CacheClearer(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public void clearCache(String cacheName) {
        Optional<Cache> cache = cacheManager.getCache(cacheName);
        if (cache.isPresent()) {
            cache.get().invalidateAll().await().indefinitely();
        }
    }
}

构建编程式缓存键

在构建编程式缓存键之前,您需要了解当调用注解方法时,注解 API 如何构建缓存键。这在本指南的 缓存键构建逻辑部分中进行了解释。

现在,如果您想使用编程式 API 检索或删除使用注解 API 存储的缓存值,您只需确保将相同的键与两个 API 一起使用即可。

CaffeineCache 检索所有键

来自特定 CaffeineCache 的缓存键可以作为不可修改的 Set 检索,如下所示。如果在正在进行的集合迭代期间修改了缓存条目,则该集合将保持不变。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;

import java.util.Set;

@ApplicationScoped
public class CacheKeysService {

    @CacheName("my-cache")
    Cache cache;

    public Set<Object> getAllCacheKeys() {
        return cache.as(CaffeineCache.class).keySet();
    }
}

填充 CaffeineCache

您可以使用 CaffeineCache#put(Object, CompletableFuture) 方法填充 CaffeineCache。此方法将 CompletableFuture 与缓存中的给定键关联。如果缓存先前包含与该键关联的值,则旧值将被此 CompletableFuture 替换。如果异步计算失败,则将自动删除该条目。

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;

import java.util.concurrent.CompletableFuture;

@ApplicationScoped
public class CacheService {

    @CacheName("my-cache")
    Cache cache;

    @PostConstruct
    public void initialize() {
        cache.as(CaffeineCache.class).put("foo", CompletableFuture.completedFuture("bar"));
    }
}

如果键存在,则从 CaffeineCache 检索值

如果存在,则可以从特定 CaffeineCache 检索缓存值,如下所示。如果缓存中包含给定键,则该方法将返回指定键映射到的 CompletableFuture。该 CompletableFuture 可能正在计算或可能已完成。否则,该方法将返回 null

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;

import java.util.concurrent.CompletableFuture;

@ApplicationScoped
public class CacheKeysService {

    @CacheName("my-cache")
    Cache cache;

    public CompletableFuture<Object> getIfPresent(Object key) {
        return cache.as(CaffeineCache.class).getIfPresent(key);
    }
}

实时更改 CaffeineCache 的过期策略或最大大小

如果最初在 Quarkus 配置中指定了过期策略,则可以在 Quarkus 应用程序运行时更改 CaffeineCache 的过期策略。同样,如果使用配置中定义的初始最大大小构建缓存,则可以实时更改 CaffeineCache 的最大大小。

package org.acme.cache;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheManager;
import io.quarkus.cache.CaffeineCache;

import java.time.Duration;
import java.util.Optional;import javax.inject.Singleton;

@Singleton
public class CacheConfigManager {

    private final CacheManager cacheManager;

    public CacheConfigManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public void setExpireAfterAccess(String cacheName, Duration duration) {
        Optional<Cache> cache = cacheManager.getCache(cacheName);
        if (cache.isPresent()) {
            cache.get().as(CaffeineCache.class).setExpireAfterAccess(duration); (1)
        }
    }

    public void setExpireAfterWrite(String cacheName, Duration duration) {
        Optional<Cache> cache = cacheManager.getCache(cacheName);
        if (cache.isPresent()) {
            cache.get().as(CaffeineCache.class).setExpireAfterWrite(duration); (2)
        }
    }

    public void setMaximumSize(String cacheName, long maximumSize) {
        Optional<Cache> cache = cacheManager.getCache(cacheName);
        if (cache.isPresent()) {
            cache.get().as(CaffeineCache.class).setMaximumSize(maximumSize); (3)
        }
    }
}
1 仅当缓存使用 expire-after-access 配置值构造时,此行才有效。否则,将抛出 IllegalStateException
2 仅当缓存使用 expire-after-write 配置值构造时,此行才有效。否则,将抛出 IllegalStateException
3 仅当缓存使用 maximum-size 配置值构造时,此行才有效。否则,将抛出 IllegalStateException

CaffeineCache 中的 setExpireAfterAccesssetExpireAfterWritesetMaximumSize 方法绝不能从缓存操作的原子范围内调用。

配置底层缓存提供程序

此扩展使用 Caffeine 作为其底层缓存提供程序。Caffeine 是一种高性能、接近最佳的缓存库。

Caffeine 配置属性

支持 Quarkus 应用程序数据缓存扩展的每个 Caffeine 缓存都可以使用 application.properties 文件中的以下属性进行配置。默认情况下,如果未配置,缓存不执行任何类型的驱逐。

您需要在以下所有属性中将 cache-name 替换为您要配置的缓存的真实名称。

构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖

配置属性

类型

默认

缓存类型。

环境变量:QUARKUS_CACHE_TYPE

显示更多

字符串

caffeine

是否启用缓存扩展。

环境变量:QUARKUS_CACHE_ENABLED

显示更多

布尔值

true

应用于所有 Caffeine 缓存的默认配置(最低优先级)

类型

默认

内部数据结构的最小总大小。在构造时提供足够大的估计值可以避免以后进行昂贵的调整大小操作的需要,但是将此值设置得不必要地高会浪费内存。

环境变量:QUARKUS_CACHE_CAFFEINE_INITIAL_CAPACITY

显示更多

整数

缓存可能包含的最大条目数。请注意,缓存在超出此限制之前可能会驱逐条目,或者在驱逐时暂时超过阈值。随着缓存大小接近最大值,缓存会驱逐不太可能再次使用的条目。例如,缓存可能会驱逐一个条目,因为它最近没有使用过或者不经常使用。

环境变量:QUARKUS_CACHE_CAFFEINE_MAXIMUM_SIZE

显示更多

long

指定一旦条目的创建或最近替换其值经过固定持续时间后,应自动从缓存中删除每个条目。

环境变量:QUARKUS_CACHE_CAFFEINE_EXPIRE_AFTER_WRITE

显示更多

Duration 

指定在条目的创建、最近的值替换或最后一次读取后经过固定持续时间后,应自动从缓存中删除每个条目。

环境变量:QUARKUS_CACHE_CAFFEINE_EXPIRE_AFTER_ACCESS

显示更多

Duration 

如果应用程序依赖 Micrometer 扩展,是否记录指标。将此值设置为 true 将启用 Caffeine 中缓存统计信息的累积。

环境变量:QUARKUS_CACHE_CAFFEINE_METRICS_ENABLED

显示更多

布尔值

内部数据结构的最小总大小。在构造时提供足够大的估计值可以避免以后进行昂贵的调整大小操作的需要,但是将此值设置得不必要地高会浪费内存。

环境变量:QUARKUS_CACHE_CAFFEINE__CACHE_NAME__INITIAL_CAPACITY

显示更多

整数

缓存可能包含的最大条目数。请注意,缓存在超出此限制之前可能会驱逐条目,或者在驱逐时暂时超过阈值。随着缓存大小接近最大值,缓存会驱逐不太可能再次使用的条目。例如,缓存可能会驱逐一个条目,因为它最近没有使用过或者不经常使用。

环境变量:QUARKUS_CACHE_CAFFEINE__CACHE_NAME__MAXIMUM_SIZE

显示更多

long

指定一旦条目的创建或最近替换其值经过固定持续时间后,应自动从缓存中删除每个条目。

环境变量:QUARKUS_CACHE_CAFFEINE__CACHE_NAME__EXPIRE_AFTER_WRITE

显示更多

Duration 

指定在条目的创建、最近的值替换或最后一次读取后经过固定持续时间后,应自动从缓存中删除每个条目。

环境变量:QUARKUS_CACHE_CAFFEINE__CACHE_NAME__EXPIRE_AFTER_ACCESS

显示更多

Duration 

如果应用程序依赖 Micrometer 扩展,是否记录指标。将此值设置为 true 将启用 Caffeine 中缓存统计信息的累积。

环境变量:QUARKUS_CACHE_CAFFEINE__CACHE_NAME__METRICS_ENABLED

显示更多

布尔值

关于 Duration 格式

要写入持续时间值,请使用标准 java.time.Duration 格式。有关更多信息,请参阅 Duration#parse() Java API 文档

您还可以使用简化的格式,以数字开头

  • 如果该值仅为一个数字,则表示以秒为单位的时间。

  • 如果该值是一个数字后跟 ms,则表示以毫秒为单位的时间。

在其他情况下,简化格式将被转换为 java.time.Duration 格式以进行解析

  • 如果该值是一个数字后跟 hms,则在其前面加上 PT

  • 如果该值是一个数字后跟 d,则在其前面加上 P

以下是您的缓存配置可能的样子

quarkus.cache.caffeine."foo".initial-capacity=10 (1)
quarkus.cache.caffeine."foo".maximum-size=20
quarkus.cache.caffeine."foo".expire-after-write=60S
quarkus.cache.caffeine."bar".maximum-size=1000 (2)
1 正在配置 foo 缓存。
2 正在配置 bar 缓存。

启用 Micrometer 指标

可以使用 Micrometer 指标监视使用 注解缓存 API 声明的每个缓存。

仅当您的应用程序依赖于 quarkus-micrometer-registry-* 扩展时,缓存指标收集才有效。请参阅 Micrometer 指标指南以了解如何在 Quarkus 中使用 Micrometer。

默认情况下禁用缓存指标收集。可以从 application.properties 文件中启用它

quarkus.cache.caffeine."foo".metrics-enabled=true

与所有检测方法一样,收集指标会带来很小的开销,这可能会影响应用程序性能。

收集的指标包含缓存统计信息,例如

  • 缓存中条目的近似当前数量

  • 添加到缓存中的条目数

  • 执行缓存查找的次数,包括有关命中和未命中的信息

  • 驱逐的数量和驱逐条目的权重

以下是依赖于 quarkus-micrometer-registry-prometheus 扩展的应用程序可用的缓存指标示例

# HELP cache_size The number of entries in this cache. This may be an approximation, depending on the type of cache.
# TYPE cache_size gauge
cache_size{cache="foo",} 8.0
# HELP cache_puts_total The number of entries added to the cache
# TYPE cache_puts_total counter
cache_puts_total{cache="foo",} 12.0
# HELP cache_gets_total The number of times cache lookup methods have returned a cached value.
# TYPE cache_gets_total counter
cache_gets_total{cache="foo",result="hit",} 53.0
cache_gets_total{cache="foo",result="miss",} 12.0
# HELP cache_evictions_total cache evictions
# TYPE cache_evictions_total counter
cache_evictions_total{cache="foo",} 4.0
# HELP cache_eviction_weight_total The sum of weights of evicted entries. This total does not include manual invalidations.
# TYPE cache_eviction_weight_total counter
cache_eviction_weight_total{cache="foo",} 540.0

注解 bean 示例

隐式简单缓存键

package org.acme.cache;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class CachedService {

    @CacheResult(cacheName = "foo")
    public Object load(Object key) { (1)
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate(Object key) { (1)
    }

    @CacheInvalidateAll(cacheName = "foo")
    public void invalidateAll() {
    }
}
1 缓存键是隐式的,因为没有 @CacheKey 注解。

显式复合缓存键

package org.acme.cache;

import jakarta.enterprise.context.Dependent;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheKey;
import io.quarkus.cache.CacheResult;

@Dependent
public class CachedService {

    @CacheResult(cacheName = "foo")
    public String load(@CacheKey Object keyElement1, @CacheKey Object keyElement2, Object notPartOfTheKey) { (1)
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate(@CacheKey Object keyElement1, @CacheKey Object keyElement2, Object notPartOfTheKey) { (1)
    }

    @CacheInvalidateAll(cacheName = "foo")
    public void invalidateAll() {
    }
}
1 缓存键由两个元素显式组成。方法签名还包含第三个参数,该参数不是键的一部分。

默认缓存键

package org.acme.cache;

import jakarta.enterprise.context.Dependent;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;

@Dependent
public class CachedService {

    @CacheResult(cacheName = "foo")
    public String load() { (1)
        // Call expensive service here.
    }

    @CacheInvalidate(cacheName = "foo")
    public void invalidate() { (1)
    }

    @CacheInvalidateAll(cacheName = "foo")
    public void invalidateAll() {
    }
}
1 使用从缓存名称派生的唯一默认缓存键,因为该方法没有参数。

单个方法上的多个注解

package org.acme.cache;

import jakarta.inject.Singleton;

import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;

@Singleton
public class CachedService {

    @CacheInvalidate(cacheName = "foo")
    @CacheResult(cacheName = "foo")
    public String forceCacheEntryRefresh(Object key) { (1)
        // Call expensive service here.
    }

    @CacheInvalidateAll(cacheName = "foo")
    @CacheInvalidateAll(cacheName = "bar")
    public void multipleInvalidateAll(Object key) { (2)
    }
}
1 此方法可用于强制刷新与给定键对应的缓存条目。
2 此方法将通过一次调用使来自 foobar 缓存的所有条目失效。

清除所有应用程序缓存

package org.acme.cache;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import io.quarkus.cache.CacheManager;

@Singleton
public class CacheClearer {

    private final CacheManager cacheManager;

    public CacheClearer(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public void clearAllCaches() {
        for (String cacheName : cacheManager.getCacheNames()) {
            cacheManager.getCache(cacheName).get().invalidateAll().await().indefinitely();
        }
    }
}

负缓存和 null

有时人们希望缓存(昂贵的)远程调用的结果。如果远程调用失败,人们可能不希望缓存结果或异常,而是在下次调用时重试远程调用。

一种简单的方法可能是捕获异常并返回 null,以便调用者可以采取相应的行动

示例代码
    public void caller(int val) {

        Integer result = callRemote(val); (1)
        if (result != null) {
            System.out.println("Result is " + result);
        else {
            System.out.println("Got an exception");
        }
    }

    @CacheResult(cacheName = "foo")
    public Integer callRemote(int val)  {

        try {
            Integer val = remoteWebServer.getResult(val); (2)
            return val;
        } catch (Exception e) {
            return null; (3)
        }
    }
1 调用该方法以调用远程
2 执行远程调用并返回其结果
3 在发生异常时返回

这种方法有一个不幸的副作用:正如我们之前所说,Quarkus 也可以缓存 null 值。这意味着对具有相同参数值的 callRemote() 的下一次调用将从缓存中得到响应,返回 null 并且不会执行远程调用。这在某些情况下可能是期望的,但通常人们希望重试远程调用直到它返回结果。

让异常冒泡

为了防止缓存缓存来自远程调用的(标记)结果,我们需要让异常冒泡出被调用的方法并在调用方捕获它

随着异常冒泡
   public void caller(int val) {
       try {
           Integer result = callRemote(val);  (1)
           System.out.println("Result is " + result);
       } catch (Exception e) {
           System.out.println("Got an exception");
   }

   @CacheResult(cacheName = "foo")
   public Integer callRemote(int val) throws Exception { (2)

      Integer val = remoteWebServer.getResult(val);  (3)
      return val;

   }
1 调用该方法以调用远程
2 异常可能会冒泡
3 这可能会抛出各种远程异常

当对远程的调用抛出异常时,缓存不会存储结果,因此随后对具有相同参数值的 callRemote() 的调用将不会从缓存中得到响应。相反,它会导致另一次尝试调用远程。

构建原生可执行文件

Cache 扩展支持构建原生可执行文件。

但是,为了优化运行时内存,Caffeine 嵌入了许多缓存实现类,这些类是根据缓存配置选择的。我们没有为所有这些类注册反射(并且未注册的类未包含在原生可执行文件中),因为注册所有这些类将非常昂贵。

我们正在注册最常见的实现,但是,根据您的缓存配置,您可能会遇到如下错误

2021-12-08 02:32:02,108 ERROR [io.qua.run.Application] (main) Failed to start application (with profile prod): java.lang.ClassNotFoundException: com.github.benmanes.caffeine.cache.PSAMS (1)
        at java.lang.Class.forName(DynamicHub.java:1433)
        at java.lang.Class.forName(DynamicHub.java:1408)
        at com.github.benmanes.caffeine.cache.NodeFactory.newFactory(NodeFactory.java:111)
        at com.github.benmanes.caffeine.cache.BoundedLocalCache.<init>(BoundedLocalCache.java:240)
        at com.github.benmanes.caffeine.cache.SS.<init>(SS.java:31)
        at com.github.benmanes.caffeine.cache.SSMS.<init>(SSMS.java:64)
        at com.github.benmanes.caffeine.cache.SSMSA.<init>(SSMSA.java:43)
1 PSAMS 是 Caffeine 的众多缓存实现类之一,因此这部分可能会有所不同。

当您遇到此错误时,您可以通过将以下注解添加到您的任何应用程序类来轻松修复它(或者如果您愿意,您可以创建一个新类,例如 Reflections 来托管此注解)

@RegisterForReflection(classNames = { "com.github.benmanes.caffeine.cache.PSAMS" }) (1)
1 它是一个数组,因此如果您的配置需要多个,您可以一次注册多个缓存实现。

此注解将为反射注册缓存实现类,这将把这些类包含到原生可执行文件中。有关 @RegisterForReflection 注解的更多详细信息,请参阅 原生应用程序技巧页面。

相关内容