Application Data Caching

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

In this guide, you will learn how to enable application data caching in any CDI managed bean of your Quarkus application.

Prerequisites

Unresolved directive in cache.adoc - include::{includes}/prerequisites.adoc[]

Scenario

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

Let’s imagine you want to expose in your Quarkus application a REST API that allows users to retrieve the weather forecast for the next three days. The problem is that you have to rely on an external meteorological service which only accepts requests for one day at a time and takes forever to answer. Since the weather forecast is updated once every twelve hours, caching the service responses would definitely improve your API performances.

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

We’ll do that using a single Quarkus annotation.

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

In this guide, we use the default Quarkus Cache backend (Caffeine). You can use Infinispan or Redis instead. Refer to the Infinispan cache backend reference to configure the Infinispan backend. Refer to the Redis cache backend reference to configure the Redis backend.

Solution

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

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

解决方案位于 cache-quickstart directory

The solution is located in the cache-quickstart directory.

Creating the Maven project

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

First, we need to create a new Quarkus project with the following command:

Unresolved directive in cache.adoc - include::{includes}/devtools/create-app.adoc[]

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

This command generates the project and imports the cache and rest-jackson extensions.

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

If you already have your Quarkus project configured, you can add the cache extension to your project by running the following command in your project base directory:

Unresolved directive in cache.adoc - include::{includes}/devtools/extension-add.adoc[]

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

This will add the following to your build file:

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,内容如下:

Let’s start by creating a service that will simulate an extremely slow call to the external meteorological service. Create src/main/java/org/acme/cache/WeatherForecastService.java with the following content:

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 This is where the slowness comes from.

还需要一个类,其中包含在用户要求未来三天内天气预报时发送给他们的响应。以这种方式创建 src/main/java/org/acme/cache/WeatherForecast.java

We also need a class that will contain the response sent to the users when they ask for the next three days weather forecast. Create src/main/java/org/acme/cache/WeatherForecast.java this way:

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 文件,内容如下:

Now, we just need to create the REST resource. Create the src/main/java/org/acme/cache/WeatherForecastResource.java file with this content:

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 If the daysInFuture query parameter is omitted, the three days weather forecast will start from the current day. Otherwise, it will start from the current day plus the daysInFuture value.

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

We’re all done! Let’s check if everything’s working.

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

First, run the application using dev mode from the project directory:

Unresolved directive in cache.adoc - include::{includes}/devtools/dev.adoc[]

然后,在浏览器中调用 http://localhost:8080/weather?city=Raleigh。六秒钟之后,应用程序将响应类似如下内容:

Then, call http://localhost:8080/weather?city=Raleigh from a browser. After six long seconds, the application will answer something like this:

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

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

The response content may vary depending on the day you run the code.

您可以尝试再次调用相同的 URL,它总是需要六秒钟来响应。

You can try calling the same URL again and again, it will always take six seconds to answer.

Enabling the cache

现在,Quarkus 应用程序已启动并运行,让我们通过缓存外部气象服务响应大幅缩短响应时间。像这样更新 WeatherForecastService 类:

Now that your Quarkus application is up and running, let’s tremendously improve its response time by caching the external meteorological service responses. Update the WeatherForecastService class like this:

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 We only added this annotation (and the associated import of course).

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

Let’s try to call http://localhost:8080/weather?city=Raleigh again. You’re still waiting a long time before receiving an answer. This is normal since the server just restarted and the cache was empty.

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

Wait a second! The server restarted by itself after the WeatherForecastService update? Yes, this is one of Quarkus amazing features for developers called live coding.

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

Now that the cache was loaded during the previous call, try calling the same URL. This time, you should get a super fast answer with an executionTimeInMs value close to 0.

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

Let’s see what happens if we start from one day in the future using the http://localhost:8080/weather?city=Raleigh&daysInFuture=1 URL. You should get an answer two seconds later since two of the requested days were already loaded in the cache.

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

You can also try calling the same URL with a different city and see the cache in action again. The first call will take six seconds and the following ones will be answered immediately.

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

Congratulations! You just added application data caching to your Quarkus application with a single line of code!

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

Do you want to learn more about the Quarkus application data caching abilities? The following sections will show you everything there is to know about it.

Caching using annotations

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

Quarkus offers a set of annotations that can be used in a CDI managed bean to enable caching abilities.

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

Caching annotations are not allowed on private methods. They will work fine with any other access modifier including package-private (no explicit modifier).

@CacheResult

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

Loads a method result from the cache without executing the method body whenever possible.

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

When a method annotated with @CacheResult is invoked, Quarkus will compute a cache key and use it to check in the cache whether the method has been already invoked. See the Cache keys building logic section of this guide to learn how the cache key is computed. If a value is found in the cache, it is returned and the annotated method is never actually executed. If no value is found, the annotated method is invoked and the returned value is stored in the cache using the computed key.

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

A method annotated with CacheResult is protected by a lock on cache miss mechanism. If several concurrent invocations try to retrieve a cache value from the same missing key, the method will only be invoked once. The first concurrent invocation will trigger the method invocation while the subsequent concurrent invocations will wait for the end of the method invocation to get the cached result. The lockTimeout parameter can be used to interrupt the lock after a given delay. The lock timeout is disabled by default, meaning the lock is never interrupted. See the parameter Javadoc for more details.

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

This annotation cannot be used on a method returning void.

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

Quarkus is able to also cache null values unlike the underlying Caffeine provider. See negative-cache.

@CacheInvalidate

从缓存中删除一个条目。

Removes an entry from the cache.

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

When a method annotated with @CacheInvalidate is invoked, Quarkus will compute a cache key and use it to try to remove an existing entry from the cache. See the Cache keys building logic section of this guide to learn how the cache key is computed. If the key does not identify any cache entry, nothing will happen.

@CacheInvalidateAll

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

When a method annotated with @CacheInvalidateAll is invoked, Quarkus will remove all entries from the cache.

@CacheKey

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

When a method argument is annotated with @CacheKey, it is identified as a part of the cache key during an invocation of a method annotated with @CacheResult or @CacheInvalidate.

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

This annotation is optional and should only be used when some method arguments are NOT part of the cache key.

Cache keys building logic

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

Cache keys are built by the annotations API using the following logic:

  • If an io.quarkus.cache.CacheKeyGenerator is declared in a @CacheResult or a @CacheInvalidate annotation, then it is used to generate the cache key. The @CacheKey annotations that might be present on some method arguments are ignored.

  • Otherwise, if the method has no arguments, then the cache key is an instance of io.quarkus.cache.DefaultCacheKey built from the cache name.

  • Otherwise, if the method has exactly one argument, then that argument is the cache key.

  • Otherwise, if the method has multiple arguments but only one annotated with @CacheKey, then that annotated argument is the cache key.

  • Otherwise, if the method has multiple arguments annotated with @CacheKey, then the cache key is an instance of io.quarkus.cache.CompositeCacheKey built from these annotated arguments.

  • Otherwise, if the method has multiple arguments and none of them are annotated with @CacheKey, the cache key is an instance of io.quarkus.cache.CompositeCacheKey built from all the method arguments.

每个非基元方法参数都是密钥的一部分,它必须正确实现 equals()hashCode() 才能使缓存按预期工作。

Each non-primitive method argument that is part of the key must implement equals() and hashCode() correctly for the cache to work as expected.

当缓存键从几个方法参数(无论是通过 @CacheKey 显式标识的,还是没有标识的)构建时,构建逻辑取决于这些参数在方法签名中的顺序。另一方面,参数名称根本不会被使用,并且不会对缓存键产生任何影响。

When a cache key is built from several method arguments, whether they are explicitly identified with @CacheKey or not, the building logic depends on the order of these arguments in the method signature. On the other hand, the arguments names are not used at all and do not have any effect on the cache key.

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 Calling this method WILL invalidate values cached by the load method even if the key elements names have been swapped.
2 Calling this method WILL NOT invalidate values cached by the load method because the key elements order is different.
3 Calling this method WILL invalidate values cached by the load method because the key elements order is the same.
4 Calling this method WILL NOT invalidate values cached by the load method because the key elements order is different.

Generating a cache key with CacheKeyGenerator

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

You may want to include more than the arguments of a method into a cache key. This can be done by implementing the io.quarkus.cache.CacheKeyGenerator interface and declaring that implementation in the keyGenerator field of a @CacheResult or @CacheInvalidate annotation.

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

The class must either represent a CDI bean or declare a public no-args constructor. If it represents a CDI bean, then the key generator will be injected during the cache key computation. Otherwise, a new instance of the key generator will be created using its default constructor for each cache key computation.

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

In case of CDI, there must be exactly one bean that has the class in its set of bean types, otherwise the build fails. The context associated with the scope of the bean must be active when the CacheKeyGenerator#generate() method is invoked. If the scope is @Dependent then the bean instance is destroyed when the CacheKeyGenerator#generate() method completes.

以下密钥生成器将注入为 CDI Bean:

The following key generator will be injected as a 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 External data can be included into the cache key by injecting a CDI bean in the key generator.
2 Be careful while using Method, some of its methods can be expensive.
3 Make sure the method has enough arguments before accessing them from their index. Otherwise, an IndexOutOfBoundsException may be thrown during the cache key computation.

以下密钥生成器将使用其默认构造函数进行实例化:

The following key generator will be instantiated using its default constructor:

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 Including the method name into the cache key is not expensive, unlike other methods from Method.

两种密钥生成器都可以以类似的方式使用:

Both kinds of key generators can be used in a similar way:

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 This key generator is a CDI bean.
2 The @CacheKey annotation will be ignored because a key generator is declared in the @CacheResult annotation.
3 This key generator is not a CDI bean.
4 The @CacheKey annotation will be ignored when the foo cache data is invalidated, but param1 will be the cache key when the bar cache data is invalidated.

Caching using the programmatic API

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

Quarkus also offers a programmatic API which can be used to store, retrieve or delete values from any cache declared using the annotations API. All operations from the programmatic API are non-blocking and rely on Mutiny under the hood.

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

Before accessing programmatically the cached data, you need to retrieve an io.quarkus.cache.Cache instance. The following sections will show you how to do that.

Injecting a Cache with the @CacheName annotation

io.quarkus.cache.CacheName 可以用于字段、构造函数参数或方法参数来注入 Cache:

io.quarkus.cache.CacheName can be used on a field, a constructor parameter or a method parameter to inject a 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 This method returns the Uni<String> type which is non-blocking.
3 The k argument contains the cache key value.
4 If you don’t need the call to be non-blocking, this is how you can retrieve the cache value in a blocking way.

Retrieving a Cache from the CacheManager

另一种检索 Cache 实例的方法是在先注入 io.quarkus.cache.CacheManager 之后,再从其名称中检索想要的 Cache:

Another way to retrieve a Cache instance consists in injecting the io.quarkus.cache.CacheManager first and then retrieving the desired Cache from its name:

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 部分进行了说明。

Before building a programmatic cache key, you need to know how cache keys are built by the annotations API when an annotated method is invoked. This is explained in the Cache keys building logic section of this guide.

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

Now, if you want to retrieve or delete, using the programmatic API, a cache value that was stored using the annotations API, you just need to make sure the same key is used with both APIs.

Retrieving all keys from a CaffeineCache

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

The cache keys from a specific CaffeineCache can be retrieved as an unmodifiable Set as shown below. If the cache entries are modified while an iteration over the set is in progress, the set will remain unchanged.

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 将替换旧值。如果异步计算失败,则该条目将自动删除。

You can populate a CaffeineCache using the CaffeineCache#put(Object, CompletableFuture) method. This method associates the CompletableFuture with the given key in the cache. If the cache previously contained a value associated with the key, the old value is replaced by this CompletableFuture. If the asynchronous computation fails, the entry will be automatically removed.

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

The cache value from a specific CaffeineCache can be retrieved if present as shown below. If the given key is contained in the cache, the method will return the CompletableFuture the specified key is mapped to. That CompletableFuture may be computing or may already be completed. Otherwise, the method will return 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 的最大值。

The expiration policy of a CaffeineCache can be changed while a Quarkus app is running if that policy was initially specified in the Quarkus configuration. Similarly, the maximum size of a CaffeineCache can be changed in real time if the cache was built with an initial maximum size defined in the configuration.

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 This line will only work if the cache was constructed with an expire-after-access configuration value. Otherwise, an IllegalStateException will be thrown.
2 This line will only work if the cache was constructed with an expire-after-write configuration value. Otherwise, an IllegalStateException will be thrown.
3 This line will only work if the cache was constructed with a maximum-size configuration value. Otherwise, an IllegalStateException will be thrown.

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

The setExpireAfterAccess, setExpireAfterWrite and setMaximumSize methods from CaffeineCache must never be invoked from within an atomic scope of a cache operation.

Configuring the underlying caching provider

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

This extension uses Caffeine as its underlying caching provider. Caffeine is a high performance, near optimal caching library.

Caffeine configuration properties

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

Each of the Caffeine caches backing up the Quarkus application data caching extension can be configured using the following properties in the application.properties file. By default, caches do not perform any type of eviction if not configured.

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

You need to replace cache-name in all the following properties with the real name of the cache you want to configure.

Unresolved directive in cache.adoc - include::{generated-dir}/config/quarkus-cache.adoc[]

以下是缓存配置的外观:

Here’s what your cache configuration could look like:

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 The foo cache is being configured.
2 The bar cache is being configured.

Enabling Micrometer metrics

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

Each cache declared using the annotations-api can be monitored using Micrometer metrics.

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

The cache metrics collection will only work if your application depends on a quarkus-micrometer-registry-* extension. See the Micrometer metrics guide to learn how to use Micrometer in Quarkus.

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

The cache metrics collection is disabled by default. It can be enabled from the application.properties file:

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

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

Like all instrumentation methods, collecting metrics comes with a small overhead that can impact the application performances.

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

The collected metrics contain cache statistics such as:

  • the approximate current number of entries in the cache

  • the number of entries that were added to the cache

  • the number of times a cache lookup has been performed, including information about hits and misses

  • the number of evictions and the weight of the evicted entries

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

Here is an example of cache metrics available for an application that depends on the quarkus-micrometer-registry-prometheus extension:

# 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 The cache key is implicit since there’s no @CacheKey annotation.

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 The cache key is explicitly composed of two elements. The method signature also contains a third argument which is not part of the key.

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 A unique default cache key derived from the cache name is used because the method has no arguments.

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 This method can be used to force a refresh of the cache entry corresponding to the given key.
2 This method will invalidate all entries from the foo and bar caches with a single call.

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

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

Sometimes one wants to cache the result of an (expensive) remote call. If the remote call fails, one may not want to cache the result or exception, but rather re-try the remote call on the next invocation.

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

A simple approach could be to catch the exception and return null, so that the caller can act accordingly:

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 Call the method to call the remote
2 Do the remote call and return its result
3 Return in case of exception

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

This approach has an unfortunate side effect: as we said before, Quarkus can also cache null values. Which means that the next call to callRemote() with the same parameter value will be answered out of the cache, returning null and no remote call will be done. This may be desired in some scenarios, but usually one wants to retry the remote call until it returns a result.

Let exceptions bubble up

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

To prevent the cache from caching (marker) results from a remote call, we need to let the exception bubble out of the called method and catch it at the caller side:

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 Call the method to call the remote
2 Exceptions may bubble up
3 This can throw all kinds of remote exceptions

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

When the call to the remote throws an exception, the cache does not store the result, so that a subsequent call to callRemote() with the same parameter value will not be answered out of the cache. It will instead result in another attempt to call the remote.

Going native

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

The Cache extension supports building native executables.

但是,为了优化运行时内存,Caffeine 搭载许多缓存实现类,这些类会根据缓存配置进行选择。我们并没有全部将其注册以进行反射(并且未注册的那些未包含在本机可执行文件中),因为全部注册它们将非常昂贵。

However, to optimize runtime memory, Caffeine embarks many cache implementation classes that are selected depending on the cache configuration. We are not registering all of them for reflection (and the ones not registered are not included into the native executables) as registering all of them would be very costly.

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

We are registering the most common implementations but, depending on your cache configuration, you might encounter errors like:

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 is one of the many cache implementation classes of Caffeine so this part may vary.

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

When you encounter this error, you can easily fix it by adding the following annotation to any of your application classes (or you can create a new class such as Reflections just to host this annotation if you prefer):

@RegisterForReflection(classNames = { "com.github.benmanes.caffeine.cache.PSAMS" }) 1
1 It is an array, so you can register several cache implementations in one go if your configuration requires several of them.

此注解将为反射注册缓存实现类,并将这些类包含在本地可执行文件中。有关 @RegisterForReflection 注解的更多详细信息可以在 native application tips 页面上找到。

This annotation will register the cache implementation classes for reflection and this will include the classes into the native executable. More details about the @RegisterForReflection annotation can be found on the native application tips page.