Cache
在本指南中,您将学习如何在 Quarkus 应用程序任意 CDI 托管 bean 中启用应用程序数据缓存。
This technology is considered 预览. For a full list of possible extension statuses, check our FAQ entry. |
场景
让我们想象下你想要在你的 Quarkus 应用程序中暴露一个 REST API ,让用户检索今后三天的天气预报。 问题在于,你只能依靠一个外部气象服务,它只能每天请求一次并且需要很长时间才回应。 由于天气预报每隔12小时更新一次,把服务响应缓存起来肯定会提高您的 API 性能。
我们将使用一个单独的 Quarkus 注解来完成这项工作。
成果
我们建议您按照下面章节指示,一步一步创建应用程序。 但是你也可以直接跳到已完成的例子。
克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git
, 或下载 压缩包
代码在 cache-quickstart
目录。
创建 Maven 项目
首先,我们需要使用Maven创建一个新的Quarkus项目,其命令如下:
mvn io.quarkus:quarkus-maven-plugin:1.3.1.Final:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=cache-quickstart \
-DclassName="org.acme.cache.WeatherForecastResource" \
-Dpath="/weather" \
-Dextensions="cache,resteasy-jsonb"
此命令生成带有 REST 接口的 Maven 项目,并导入了 cache
和 resteasy-jsonb
extensions。
创建 REST API
让我们先创建一个很慢的模拟外部气象服务。
创建 src/main/java/org/acme/cache/WeatherForecastService.java
,内容如下:
package org.acme.cache;
import java.time.LocalDate;
import javax.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;
}
}
现在,我们需要更新生成的 WeatherForecastResource
类使用服务和响应:
package org.acme.cache;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;
@Path("/weather")
public class WeatherForecastResource {
@Inject
WeatherForecastService service;
@GET
@Produces(MediaType.APPLICATION_JSON)
public WeatherForecast getForecast(@QueryParam String city, @QueryParam 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 天开始。 |
我们都完成了! 让我们看看是否都生效。
首先,在项目目录中的使用 ./mvnw compile quarkus:dev
来运行应用程序。
然后,从浏览器访问 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,总是需要6秒钟才能回应。
启用缓存
既然您的 Quarkus 应用程序已经启动并运行,我们会通过缓存外部气象服务响应来极大改善其响应时间。
修改 WeatherForecastService
class 如下:
package org.acme.cache;
import java.time.LocalDate;
import javax.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 | 我们只是添加了此注解(以及相关的import)。 |
让我们再次调用 http://localhost:8080/weather?city=Raleigh
。
您仍等待了很长时间才得到回复。
这是正常的,因为服务器刚刚重新启动了,缓存为空。
等一秒钟! 服务器在修改 WeatherForecastService
后重新启动了?
是的,这是Quarkus 为开发人员设计的一个惊人功能,叫做 live coding
。
现在缓存已在上次调用中加载了,请尝试重新访问下上个网址。
这一次,你应该很快就有回复了,并且 executionTimeInMs
的值接近0 。
让我们看看如果我们指定从一天开始,访问 http://localhost:8080/weather?city=Ral8&daysInFuture=1
网址会怎样。
两秒后你应该得到回复,因为请求中有两天已经被加载到缓存中。
您也可以尝试再次访问同一个网址,看看缓存生效。 第一次调用将需要六秒钟,接下来的调用将立即得到回复。
恭喜! 您刚刚用一行代码就将应用程序数据缓存添加到了 Quarkus 应用程序!
您想要了解更多 Quarkus 应用程序数据缓存能力的信息吗? 下面的章节将向您展示它所知道的一切。
缓存注解
Quarkus 提供了一组注解,可用于一个 CDI 管理的 bean 以启用缓存能力。
@CacheResult
尽可能从缓存中加载方法结果,而不实际执行方法。
调用被 @CacheResult
注解方法时, Quarkus 将计算一个缓存 key ,并使用它在缓存中检查该方法是否已被调用过。
如果方法有一个或多个参数且没有一个注解为 @CacheKey
, 或者所有参数都注解了 @CacheKey
, 密钥是用所有参数来计算的。
这个注解也可以用于没有参数的方法,在这种情况下从缓存名称中生成默认 key。
如果在缓存中找到一个值它将返回,被注解的方法并未实际执行。
如果找不到值,则会调用注解方法,返回的值会被存储在缓存中,使用计算或生成的 key.
注解了 CacheResult
的方法会被缓存缺失机制锁保护。
如果几个调用并发,试图用同一个 key 从缺失缓存获取,该方法只会被调用一次。
并发的第一次调用将调用方法,而随后并行调用将等待方法调用结束以获取缓存结果。
lockTimeout
参数可以指定延迟多久后中断锁定。
默认情况下禁用锁定超时,这意味着锁永远不会中断。
参数更详细信息请参阅参Javadoc 。
此注解不能用于返回 void
的方法。
@CacheInvalidate
从缓存中删除一个条目。
当注解了 @CacheInvalidate
的方法执行时, Quarkus 将计算一个缓存 key 并使用它来尝试从缓存中删除一个现有的数据。
如果方法有一个或多个参数且没有一个注解为 @CacheKey
, 或者所有参数都注解了 @CacheKey
, 密钥是用所有参数来计算的。
这个注解也可以用于没有参数的方法,在这种情况下从缓存名称中生成默认 key。
如果 key 没有对应任何缓存数据,什么也不会发生。
如果 |
配置底层缓存实现
此扩展使用 Caffeine 作为其内置缓存实现。 Caffeine 性能很高,接近最佳缓存库。
Caffeine 配置属性
每个用 Caffeine 缓存的 Quarkus 程序都可以通过下边的属性在 application.properties
中配置。
您需要将以下所有属性中的 |
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Type |
Default |
|
---|---|---|
Minimum total size for the internal data structures. Providing a large enough estimate at construction time avoids the need for expensive resizing operations later, but setting this value unnecessarily high wastes memory. |
int |
|
Maximum number of entries the cache may contain. Note that the cache may evict an entry before this limit is exceeded or temporarily exceed the threshold while evicting. As the cache size grows close to the maximum, the cache evicts entries that are less likely to be used again. For example, the cache may evict an entry because it hasn’t been used recently or very often. |
long |
|
Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, or the most recent replacement of its value. |
||
Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, the most recent replacement of its value, or its last read. |
About the Duration format
The format for durations uses the standard You can also provide duration values starting with a number.
In this case, if the value consists only of a number, the converter treats the value as seconds.
Otherwise, |
您的缓存配置可能跟下边类似:
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 缓存。 |
注解 bean 例子
隐式简单缓存 key
package org.acme.cache;
import javax.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 | 缓存 key 是隐含的,因为没有 @CacheKey 注解。 |
显式组合缓存 key
package org.acme.cache;
import javax.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 | 明指缓存 key 由两个元素组成。 方法签名还包含与 key 无关的第三个参数。 |
默认缓存 key
package org.acme.cache;
import javax.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 | 从缓存名称中生成并使用的唯一默认缓存 key。 |
单个方法上多个注解
package org.acme.cache;
import javax.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 | 此方法可以用来强制刷新与给定的 key 相对应的缓存数据。 |
2 | 调用一次这个方法会使缓存的 foo 和 bar 所有数据失效。 |