概述
使用中运算使用 PreparsedDocumentProvider
进行缓存时,我们的 DGS 服务器(构建在 Spring Boot 之上)将准备好处理很多细节。但是,我们需要提供一些东西。第一个是缓存实现。
在本课中,我们将:
- 介绍缓存库 Caffeine
- 创建新的缓存实例并配置其大小和过期时间
- 实现
PreparsedDocumentProvider
的所需方法 - 测试我们的缓存并观察缓存命中和缓存未命中
Caffeine
要创建新的缓存,我们将使用 Caffeine,这是一个通常用于 Java 应用程序的高性能缓存库。Caffeine 是一个内存中缓存。这意味着每个服务器实例都有自己的缓存,并且单独填充;这与分布式缓存(例如 Redis,其中每个服务器共享同一个实例)形成对比。
我们的 PreparsedDocumentProvider
实现将使用此缓存来存储已解析并已验证的 GraphQL运算,我们的服务器接收到的。
打开你的项目的 build.gradle
文件。在依赖项下,让我们引入 Caffeine 库。
dependencies {implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")// ... other dependencies}
请务必重新加载 Gradle 依赖项,以便将新软件包纳入我们的项目。
实现缓存
使用 Caffeine 实现缓存的方法有很多。为了遵循我们代码库的响应式风格,我们将使用一个名为 AsyncCache
的接口。通过使用异步缓存,我们可以在数据就绪时将其存储并加载,而不是通过多个阻塞请求在代码中创建瓶颈。
注意:请查看 Caffeine 官方 Wiki,了解有关创建新缓存的其他方法的详细信息。
我们回到 CachingPreparsedDocumentProvider
类文件并导入一些依赖项。
import com.github.benmanes.caffeine.cache.AsyncCache;import com.github.benmanes.caffeine.cache.Caffeine;import java.util.concurrent.CompletableFuture;import graphql.ExecutionInput;import java.time.Duration;import java.util.function.Function;
在我们类内部,我们将创建一个 private final
属性,称为 cache
。我们的缓存将是 AsyncCache
类型,它接受两个 类型变量;我们将回到此步骤,因此现在可将其留空。
private final AsyncCache<> cache
接下来,要创建缓存,我们将调用 Caffeine.newBuilder
。在此基础上,我们可以将配置链接到我们的缓存,包括允许它容纳的条目数,以及何时应从缓存中逐出记录。
private final AsyncCache<> cache = Caffeine.newBuilder().maximumSize() // How many entries can the cache contain?.expireAfterAccess() // When should the record be removed?.buildAsync();
让我们为缓存赋予 250 条记录的宽裕容量,以及 2 分钟的过期时间以方便测试。
Caffeine.newBuilder().maximumSize(250).expireAfterAccess(Duration.ofMinutes(2)).buildAsync();
现在我们回到缓存的类型定义。 AsyncCache
接受两个类型 变量:一个用于密钥类型,另一个用于它存储的对象类型。
AsyncCache<KeyType, ValueType>
在我们示例中,我们的缓存将包含类型为 PreparsedDocumentEntry
的对象。 PreparsedDocumentEntry
,之前已从与我们的 PreparsedDocumentProvider
接口相同的 graphql
包中导入,表示已解析并验证的 查询 的结果。对于我们的密钥数据类型,我们将提供 String
。
private final AsyncCache<String, PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(250).expireAfterAccess(Duration.ofMinutes(2)).buildAsync();
现在让我们启动此缓存!
构建我们的类
请回想一下 PreparsedDocumentProvider
接口要求在我们的类上定义一个特定方法: getDocumentAsync
。此方法是使用每个传入的 操作 调用的,我们的 GraphQL 服务器 收到。此方法的职责是返回缓存操作(如果在缓存中找到)或者解析、验证,然后将其缓存(如果在缓存中不存在)。
getDocumentAsync
方法
在 CachingPreparsedDocumentProvider
中腾出一些空间,让我们添加 getDocumentAsync
方法的结构。
@Overridepublic void getDocumentAsync() {}
注意:我们需要在此应用 @Override
来覆盖 getDocumentAsync
类型在该类的超类,即 PreparsedDocumentProvider
接口中的行为。
还记得 getDocumentAsync
接收的参数吗?第一个是我们的执行输入,一个包含当前 操作 详情的对象,由我们的 GraphQL 服务器 处理。我们指定一个名为 executionInput
,类型为 ExecutionInput
的参数。
public void getDocumentAsync(ExecutionInput executionInput) {}
第二个参数是我们如果发现请求的 操作 不存在于缓存中就会调用的函数。它负责解析、验证和缓存步骤。我们将其称为 parseAndValidateFunction
,它是一个 Function
类型,具有两个类型 变量: ExecutionInput
和 PreparsedDocumentEntry
。我们现在更新函数签名——稍后我们会详细说明这些参数是如何发挥作用的。
public void getDocumentAsync(ExecutionInput executionInput,Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {}
在填入 our 方法逻辑之前,我们先更新 our 返回类型。此函数是异步函数,因此我们将返回 CompletableFuture
类型。 our CompletableFuture
将接受 PreparsedDocumentEntry
类型变量。这个变量负责存储和检索预解析且经过验证的 文档的实际解析和验证的 文档,因此 our 的缓存对此变量负责。
public CompletableFuture<PreparsedDocumentEntry> getDocumentAsync(ExecutionInput executionInput,Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {}
很好!在完成 our 方法签名后,让我们回顾一下它的任务。
- 检查缓存是否包含预解析、预验证的 查询。如果是,则返回!
- 如果不是,则改为调用
parseAndValidateFunction
因此, getDocumentAsync
需要调用 our 缓存的 get
方法。它将传入服务器尝试解析的 查询,从而向缓存提供预解析、预验证 文档的引用以便返回。但由于缓存可能不包含有问题的 操作,因此我们需要向它传递第二个 参数:发生“缓存未命中”时应当运行的函数。
cache.get(operationString, // The operation string we can use to look up the right parsed and validated docfallbackFunction // The function that will run if the operation is not found (a "cache miss"))
访问操作
我们可以通过方法的 operation 字符串(尚未分析或验证)访问实际 executionInput
。一个实用的方法 getQuery
,可让我们准确获取所需信息。
public CompletableFuture<PreparsedDocumentEntry> getDocumentAsync(ExecutionInput executionInput,Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {cache.get(executionInput.getQuery());}
备用函数
现在,我们将指定当缓存中 未找到 operation 时要运行的函数。
在此,我们要获取 executionInput
并将其传递以进行分析和验证。为此,我们将使用 parseAndValidateFunction
, GraphQL 进程将该函数传递给我们的 getDocumentAsync 方法。
但是我们不会直接调用 parseAndValidateFunction
;相反,我们将传递一个函数,该函数使用 apply
来用我们的 executionInput
调用 parseAndValidate
函数。为了遵守我们的 cache.get
调用针对其第二个 参数预期的签名,我们需要给我们的函数一些任意输入——出于简单起见,我们称之为 s
。
最后,我们一定要确保返回结果。
return cache.get(executionInput.getQuery(),s -> parseAndValidateFunction.apply(executionInput));
注意: parseAndValidateFunction
遵守 Function
接口,它为我们提供了四种方法以供选择。此处我们使用 apply
来专门将该函数“应用”于我们的执行输入。进一步了解 Java 文档中的 Function
接口。
放大来看,以下是我们整个方法的样貌。
@Overridepublic CompletableFuture<PreparsedDocumentEntry> getDocumentAsync(ExecutionInput executionInput,Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {return cache.get(executionInput.getQuery(),s -> parseAndValidateFunction.apply(executionInput));}
在执行特定 操作的时候,GraphQL 进程将调用我们的 CachingPreparsedDocumentProvider
的 getDocumentAsync
方法。
它将传入当前执行输入,以及作为我们的 parseAndValidateFunction
值的特殊 GraphQL Java 提供的函数。
通常,它会自动使用此函数根据执行输入的内容进行解析、验证和执行——但我们刚刚添加了连接,以确保它 首先 检查我们的缓存以获取已解析且经过验证的 文档 !
检查缓存未命中
我们还可以修改getDocumentAsync
方法来打印是否有缓存未命中,以及如果存在该未命中时的情况。
让我们为我们的类添加下面的方法, callIfCacheMiss
,它将负责调用 parseAndValidateFunction.apply(executionInput)
,它将记录在缓存中找不到的 操作。
public PreparsedDocumentEntry callIfCacheMiss(ExecutionInput executionInput,Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction){System.out.println("Pre-parsed operation wasn't found in cache: " + executionInput.getQuery());return parseAndValidateFunction.apply(executionInput);}
接下来,我们将更新 getDocumentAsync
以将此责任委托给我们的新函数 callIfCacheMiss
。
@Overridepublic CompletableFuture<PreparsedDocumentEntry> getDocumentAsync(ExecutionInput executionInput,Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {return cache.get(executionInput.getQuery(),s -> callIfCacheMiss(executionInput, parseAndValidateFunction));}
让我们重新启动服务器,然后回到 沙盒。
我们将设置一个新的 查询来要求提供特定列表的详细信息,以及一些关于其便利设施的信息。
query GetListingAndAmenities($listingId: ID!) {listing(id: $listingId) {titledescriptionnumOfBedsamenities {namecategory}}}
在 变量面板中,提供以下内容:
{"listingId": "listing-1"}
当我们运行 查询时,我们应该在 响应面板中看到数据,以及请求以毫秒为单位花费的时间。(记下这个数字!)回到我们的服务器终端,我们还应该能看到一些附加输出。
Pre-parsed operation wasn't found in cache: query GetListingAndAmenities($listingId: ID!) {listing(id: $listingId) {titledescriptionnumOfBedsamenities {namecategory}}}
有道理。这是我们第一次运行 查询,所以很自然,我们在缓存中找不到它预先解析和预先验证的自己。让我们尝试再次运行它,交换不同的列表 ID:
{"listingId": "listing-2"}
这次,我们的服务器没有输出!我们的 操作已成功缓存,即使使用不同的列表 ID,我们也可以重复使用它。这是一个“缓存命中”,因为我们找到了我们正在寻找的东西。
此外,我们应该看到我们的时间 操作花费大幅减少。我们节省了几毫秒的时间——否则我们将浪费在 重新解析和 重新验证熟悉的 操作上。
让我们微调一下我们的 操作。移除 字段 for 设施
,再次运行 操作。我们将在服务器的终端中看到一些新的输出;我们运行了一个 新的 操作,所以它在缓存中创建了一个新条目!
Pre-parsed operation wasn't found in cache: query GetListingAndAmenities($listingId: ID!) {listing(id: $listingId) {titledescriptionnumOfBeds}}
每次我们运行一个 新的 操作,我们应该看到我们的终端输出标记我们的“缓存未命中”。
如果我们等待两分钟并运行一个 操作已经被添加到我们的缓存中,我们应该看到相同的“缓存未命中”消息——两分钟后,该条目已被逐出!
我们的 操作正在被缓存,我们已经看到了我们的 查询 变量 最佳实践:我们可以缓存一个单一操作,但将其应用于多个不同的值,而无需重新解析和重新验证。
不要犹豫,删除 callIfCacheMiss
方法,并恢复对 getDocumentAsync
方法的更改。请查看下面的可折叠部分以获取指导。
实践
PreparsedDocumentProvider
实现的一部分进行配置时,我们可以使用缓存来存储 将此框中的项目拖动到上面的空白处
已解析并已验证
GraphQL 操作
Java 类
逐出
类型变量
内存缓存
方法
分布式缓存
GraphQL 变量
缓存库
要点
- Caffeine 是一款高性能缓存库,我们可以用它在 Spring Boot 中实例化新的缓存
AsyncCache
是一个缓存实现,可避免阻塞请求,并保持我们应用程序的响应性- 我们可以针对容量和过期时间配置缓存中某些限制
- 在实现
PreparsedDocumentProvider
接口时,我们需要提供一个getDocumentAsync
函数来遵循特定签名 - 当我们缓存中找不到一个特定的 操作时,我们需要调用
getDocumentAsync
的第二个 参数,即一个解析和验证函数,才能完成这两个步骤并继续执行
接下来
我们刚刚完成了缓存之旅的上半程。现在,由于我们不必对服务器已处理的 操作重新解析和重新验证,因此我们可以继续下一个主题:缓存我们的 数据源 响应。
分享你对本课程的问题和评论
本课程目前处于
您需要一个 GitHub 帐户才能在下面发帖。还没有吗? 转而在我们的 Odyssey 论坛发帖。