Application Data Caching

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

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

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Scenario

假设您要在 Quarkus 应用程序中公开一个 REST API,允许用户检索未来三天的天气预报。问题是,您必须依赖于外部气象服务,该服务一次只接受一天的请求,而且响应非常慢。由于天气预报每 12 小时更新一次,因此缓存服务响应肯定能提高 API 性能。

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

在本指南中,我们使用默认的 Quarkus 缓存后端 (Caffeine)。您可以改用 Infinispan 或 Redis。有关配置 Infinispan 后端的详情,请参阅 Infinispan cache backend reference。有关配置 Redis 后端的详情,请参阅 Redis cache backend reference

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

解决方案位于 cache-quickstart directory

Creating the Maven project

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

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

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

适用于 Windows 用户:

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

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

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

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

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

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

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

Creating the 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

然后,在浏览器中调用 http://localhost: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,它总是需要六秒钟来响应。

Enabling the cache

现在,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 我们只添加了此注释(当然还有关联的导入)。

让我们再次尝试调用 http://localhost:8080/weather?city=Raleigh。收到答复之前仍需要等待很长的时间。这是因为服务器刚刚重启并清空了缓存。

等等!服务器在 WeatherForecastService`更新后自行重启?是的,这是 Quarkus 开发人员非常棒的功能之一,称为 `live coding

由于上一次调用中加载了缓存,请尝试再次调用相同的 URL。这次,您应该得到超快速的答复,其中 `executionTimeInMs`值接近 0。

我们使用 `http://localhost:8080/weather?city=Raleigh&daysInFuture=1`URL 从未来的一天开始尝试下会发生什么。您应该在两秒后收到答复,因为请求的两天数据已加载到缓存中。

您还可以尝试用一个不同的城市来调用相同的 URL 并再次看到缓存如何运行。第一次调用将持续 6 秒钟,而接下来的调用会立即得到答复。

恭喜!您刚刚通过一行代码向 Quarkus 应用程序添加了应用程序数据缓存!

您想进一步了解 Quarkus 应用程序数据缓存功能吗?以下章节将为您展示相关的所有知识。

Caching using annotations

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

在私有方法中不允许使用缓存注解。它们适用于任何其他访问修饰符,包括包私有(无显式修饰符)。

@CacheResult

尽可能在不执行方法体的情况下从缓存中加载方法结果。

当调用用 @CacheResult 注释的方法时,Quarkus 会计算一个缓存键并使用它来检查缓存中该方法是否已经调用。请参阅本指南的 Cache keys building logic 部分以了解如何计算缓存键。如果在缓存中找到值,则返回该值,并且不会实际执行带注释的方法。如果找不到值,则调用带注释的方法,并将返回值使用计算出的键存储在缓存中。

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

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

与底层的 Caffeine 提供程序不同,Quarkus 还可以缓存 null 值。请参阅 more on this topic below

@CacheInvalidate

从缓存中删除一个条目。

当调用用 @CacheInvalidate 注释的方法时,Quarkus 将计算一个缓存键并使用它尝试从缓存中删除一个现有条目。请参阅本指南的 Cache keys building logic 部分以了解如何计算缓存键。如果密钥未识别出任何缓存条目,则不会发生任何事情。

@CacheInvalidateAll

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

@CacheKey

当一个方法参数用 @CacheKey 注释时,它在带注释的方法 @CacheResult@CacheInvalidate 的调用期间被标记为缓存键的一部分。

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

Cache keys building logic

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

  • 如果 io.quarkus.cache.CacheKeyGenerator@CacheResult@CacheInvalidate 注释中声明,则使用它生成缓存键。可能会出现在某些方法参数上的 @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`方法缓存的值失效,因为键元素顺序不同。

Generating a cache key with CacheKeyGenerator

您可能希望在缓存键中包含多于方法的参数。这可以通过实现`io.quarkus.cache.CacheKeyGenerator`接口并声明`keyGenerator`和`@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 注解将被忽略,但`param1` 在 bar 缓存数据失效时将是缓存密钥。

Caching using the programmatic API

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

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

Injecting a Cache with the @CacheName annotation

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 This is optional.
2 此方法返回非阻塞的 Uni&lt;String&gt; 类型。
3 k 参数包含缓存键值。
4 如果你不需要调用非阻塞的方法,这里提供了一种以阻塞方式获取缓存值的方法。

Retrieving a Cache from the CacheManager

另一种检索 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();
        }
    }
}

Building a programmatic cache key

在构建编程缓存键之前,你需要了解在调用注释方法时,注释 API 如何构建缓存键。此方法在本文档的 Cache keys building logic 部分进行了说明。

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

Retrieving all keys from a 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();
    }
}

Populating a 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"));
    }
}

Retrieving a value if a key is present from a 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);
    }
}

Changing the expiration policy or the maximum size of a CaffeineCache in real time

如果最初在 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 异常。

CaffeineCachesetExpireAfterAccesssetExpireAfterWritesetMaximumSize 方法绝不能在缓存操作的原子范围内调用。

Configuring the underlying caching provider

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

Caffeine configuration properties

可以通过 application.properties 文件中的以下属性来配置作为 Quarkus 应用程序数据缓存扩展程序基础的每个 Caffeine 缓存。默认情况下,缓存不会执行任何类型的驱逐(如果未配置的话)。

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

Unresolved include directive in modules/ROOT/pages/cache.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-cache.adoc[]

以下是缓存配置的外观:

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 缓存。

Enabling Micrometer metrics

使用 annotations caching API 声明的每个缓存都可以使用 Micrometer 指标来监控。

只有当您的应用程序依赖于 quarkus-micrometer-registry-* 扩展时,缓存指标收集才有效。请参见 Micrometer metrics guide 以了解如何在 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

Annotated beans examples

Implicit simple cache key

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 注释。

Explicit composite cache key

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 缓存键明确地由两个元素组成。方法签名还包含第三个参数,该参数不是键的一部分。

Default cache key

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 方法没有参数,因此使用了从缓存名称派生的唯一默认缓存键。

Multiple annotations on a single method

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 缓存的所有条目失效。

Clear all application caches

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();
        }
    }
}

Negative caching and nulls

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

一种简单的方法是捕获异常并返回 null,以便调用者可以根据情况采取措施:

Sample code
    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,并且不会执行远程调用。在某些情况下这可能是需要的,但通常人们希望重试远程调用,直到它返回一个结果。

Let exceptions bubble up

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

With Exception bubbling up
   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 Exceptions may bubble up
3 这会引发各种远程异常

当对远程调用的调用引发异常时,缓存不会存储结果,因此使用相同参数值对 callRemote() 的后续调用将不会从缓存中得到解答。它将导致另一次调用远程的尝试。

Going native

缓存扩展支持构建本机可执行文件。

但是,为了优化运行时内存,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 注解的更多详细信息可以在 native application tips 页面上找到。