4. 缓存数据响应
10m

概述

在我们的 验证过程中,我们节省了时间和精力;现在是提升我们 数据获取步骤的性能的时候了!

在这一课中,我们将

  • 比较缓存 字符串和缓存由方法返回的数据
  • 复习 Spring Boot 中的缓存
  • 讨论标记为可缓存的方法的注意事项
  • 实例化 Caffeine 缓存管理器
  • 应用@Cacheable@CacheEvict 注解到我们的 方法

缓存操作响应

在上节课中,我们与 执行过程的生命周期相连;通过提供 PreparsedDocumentProvider 实例,我们可以在解析和验证是否真的必需的情况下进行干预。在缓存我们的服务器解析 数据(用于 )的情况下,我们将使用一种不同的方法

在我们的 datasources/ListingService文件中,我们有一些方法可用于从 REST API 请求列表数据。我们将从了解缓存如何与其中一种方法(即 listingRequest)配合使用开始。

datasources/ListingService
@Component
public class ListingService {
private static final String LISTING_API_URL = "https://rt-airlock-services-listing.herokuapp.com";
private final RestClient client = RestClient.builder().baseUrl(LISTING_API_URL).build();
public ListingModel listingRequest(String id) {
return client
.get()
.uri("/listings/{listing_id}", id)
.retrieve()
.body(ListingModel.class);
}
// ... other methods
}

当我们运行某个特定列表的时,我们的Query.listing数据提取器把所提列表 ID 发送到listingRequest方法。反过来,这个方法使用其id 来访问某个特定列表的 REST 端点,并返回响应作为ListingModel类的实例。

当浏览我们的应用程序中提供的各种列表时,用户可能会在多个列表页面之间来回切换,比较和对比功能。虽然我们很可能在前端有一个缓存解决方案,但我们仍然不想让服务器超负荷处理相同的列表数据请求,一次又一次又一次。 (尤其在我们不期望列表数据经常更改时!)

相反,我们将授予服务器缓存其从某个特定 ID 的 REST 端点获取响应的能力。然后,在再次请求时,它可以直接从缓存中提供结果,直到数据被逐出。

让我们看看如何在 Spring 中启用缓存。

在 Spring 中缓存

我们可以使用基本的spring-boot-starter-cache包以及一些方便的注释,在我们的应用程序中实现数据缓存。

我们现在来负责把这个包放到我们的项目中。打开项目的build.gradle文件,并在dependencies部分中添加以下行。

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
// ... other dependencies
}

如有必要,重新载入依赖项,让新包生效。

以下是我们为配置缓存要遵循的步骤。

  1. 我们要为我们的缓存创建一个新的配置文件。
  2. 我们要应用@Configuration@EnableCaching注释。
  3. 我们将定义一个cacheManager方法,该方法使用 Caffeine 创建新的缓存管理器,并指定我们的缓存应该如何运行。
  4. 最后,我们将对我们的@Cacheable@CacheEvict注释进行测试方法。

让我们开始吧!

步骤 1:CacheJavaConfig

com.example.listings目录中,与我们的ListingsApplicationCachingPreparsedDocumentProvider文件处于同一级别,创建名为CacheJavaConfig.java的新类文件。

📦 com.example.listings
┣ 📂 datafetchers
┣ 📂 dataloaders
┣ 📂 datasources
┣ 📂 models
┣ 📄 CacheJavaConfig
┣ 📄 CachingPreparsedDocumentProvider
┣ 📄 ListingsApplication
┗ 📄 WebConfiguration

马上,我们就引入了某些依赖项。

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;

接下来,让我们使用我们的注释。我们有两个需要直接应用于我们的CacheJavaConfig类的注释:@Configuration@EnableCaching

步骤 2:使用 @Configuration@EnableCaching

@Configuration注释表明特定文件包含 bean 定义(由 Spring 容器管理的对象)。我们将在我们的类定义之上添加它。

CacheJavaConfig
@Configuration // 🫘 This class is a source of bean definitions!
public class CacheJavaConfig {}

@EnableCaching注释应用到配置文件时,它将启用 Spring 中的缓存。让我们也添加这个!

CacheJavaConfig
@Configuration
@EnableCaching // 💡 Caching is switched on!
public class CacheJavaConfig {}

需要注意的是,这些注释并不会给我们提供即用型的实际缓存。相反,它们会让我们可以访问缓存逻辑,然后我们可以将其应用到我们提供的实际缓存实现。要访问所有这些逻辑,我们需要提供一个CacheManager实例。

步骤 3:定义缓存管理器

来自 Spring 缓存包的CacheManager接口被认为是“缓存抽象”的实现。它充当我们插入的缓存的“管理器”,并且它提供逻辑来更新或从缓存中删除条目。其主要目的是咨询缓存以了解 Java 方法实际需要调用,还是其结果可以从缓存中提供。

Spring 文档指出在使用缓存管理器时我们需要执行两个步骤

  • 配置将实际存储数据的缓存
  • 标识输出应被缓存的方法

缓存管理器提供了其余内容!我们创建它作为返回 CacheManager 接口实例的 Bean。我们还将用 @Bean 注解标记它以提高清晰度。

CacheJavaConfig
@Bean
public CacheManager cacheManager() {}

在本例中,我们再次使用 Caffeine 创建缓存。在 cacheManager 方法内部,我们将创建一个类型为 CaffeineCacheManager (它实现了 CacheManager 接口)的新 cacheManager,并且我们将传递我们希望给我们的缓存取的名字:“listings”。

public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("listings");
}

我们有几种方法可以为缓存提供配置。我们准备定义包含配置设置的字符串并使用 setCacheSpecification 方法将其应用于缓存管理器。

注意:如需了解配置缓存的替换方法,请查阅 官方文档

在这个字符串中,我们将允许 initialCapacity100maximumSize500expireAfterAccess2m 表示 2 分钟。我们还将传递 recordStats,以便我们可以看到缓存统计信息,因为它正在工作。

String specAsString = "initialCapacity=100,maximumSize=500,expireAfterAccess=5m,recordStats";

为了将这个字符串应用于我们的缓存,我们可以调用缓存管理器上的 setCacheSpecification 方法。

String specAsString = "initialCapacity=100,maximumSize=500,expireAfterAccess=5m,recordStats";
cacheManager.setCacheSpecification(specAsString);

我们的最后一步是返回我们创建的 cacheManager

return cacheManager;

步骤 4:@Cacheable 注解

我们的最后一步是标识我们希望其输出被缓存的方法。

为此,我们可以应用 @Cacheable 注释,指定缓存储存的名字和任何其他条件(比如 where 查询时结果不为空)考虑在内。我们也可以使用 @CachePut@CacheEvict 注释来控制对缓存的更新或驱逐。

缓存注释
// Store the output from the method unless result is null
@Cacheable(value="cacheName", unless = "#result == null")
// Run the method and update its results in the cache
@CachePut(value="cacheName")
// Remove all items from the cache
@CacheEvict(value="cacheName", allEntries=true)

注意当 @Cacheable 应用于某个方法时,该方法的输出是被缓存 整体的。这意味着如果我们的方法返回一个 列表 的对象,那么每个对象将不会被单独缓存。相反,整个响应将会被存储起来,这样下次调用相同方法(使用相同的 )时就能直接使用。

关于缓存注释的一个重要注意事项

在我们给一个方法添加了缓存注释,例如 @Cacheable 时,如果我们从 同一个类 中调用了这个方法,其结果将不会被缓存。

这是因为缓存中的实际缓存行为是由 Spring 通过一个代理添加的。你可以将这个代理看成是一个包裹着类的缓存功能包装器。当标记为 @Cacheable 的方法从 类外 被调用时,请求在调用实际方法之前会首先通过缓存代理,并启用缓存行为。

A diagram showing how a proxy is added that enables caching when method is called external to the class

但是从 类内 发出的请求会避开缓存代理,因此对缓存中的条目不会产生任何存储或更新的效果。

在同一个类中调用时不缓存
public class ListingService {
public ListingModel anotherListingServiceMethod() {
// ❌ The REST API response will NOT be cached!
return listingRequest("listing-2");
}
/* ⬇️ Calling this method from WITHIN the class avoids the proxy
that provides the caching functionality! */
@Cacheable(value="listings")
public ListingModel listingRequest(String id) {
// ...logic, logic, logic
}
}

我们的应用程序结构是这样的,我们的所有 ListingService 方法都是由我们的 datafetcher 类的中的方法调用的,所以默认情况下,这个设置会使用代理。

添加注释

我们跳到我们的 datasources/ListingService 类中来对其进行测试。首先,我们导入我们的注释。

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;

然后让我们滚动到请求一个特定列表的方法: listingRequest

要缓存此方法的结果,我们将添加 @Cacheable 注释。我们将使用一个名为 value 的属性来指定我们的缓存名称, "listings"

@Cacheable(value="listings")
public ListingModel listingRequest(String id) {
// method body
}

我们的客户端可能会一个实际上不存在的 ID 的清单,因此我们在此处为标注添加一些额外内容。我们将包含一个名为unless的属性。此处,我们可以定义我们的条件:如果清单无效,请不要将其放入缓存。

@Cacheable(value="listings", unless = "#result == null")

让我们试试吧!重新启动服务器,然后返回沙盒

任务!

我们将运行以获取特定清单,以便我们达到我们刚刚标记为“可缓存”的这个底层方法。在操作面板中添加以下查询。

query GetListingAndAmenities($listingId: ID!) {
listing(id: $listingId) {
title
description
numOfBeds
}
}

并在变量面板中:

{
"listingId": "listing-2"
}

运行,并且...是,数据如期而至。但是请注意响应面板顶部的毫秒数。我们可能正在看到 300 毫秒以上的情况。

https://studio.apollographql.com/sandbox/explorer

The results of running the operation in Explorer, with the large number of milliseconds highlighted

这是我们的基准;现在让我们再试一次

在完全相同的上再次点击播放按钮。

https://studio.apollographql.com/sandbox/explorer

The results of running the operation in Explorer, with the smaller number of milliseconds highlighted

您是否看到记录的毫秒数大幅度下降?现在我们应下降到 10-20 毫秒范围,具体取决于您的互联网速度。我们的数据已缓存,并且我们绝对可以看到改进!

使用 @CacheEvict

当我们时,我们各个清单都将保存在缓存中。现在,让我们测试缓存的另一面:强制删除。

我们可以在任何一个方法上应用@CacheEvict标注。使用此标注,我们可以清除全部或特定条目。我们将尝试强制删除所有内容。

返回 ListingService,在 @CacheEvict 注释中添加 featuredListingsRequest 方法。我们将指定缓存的名称 "listings",然后传递一个附加属性: allEntries = true。每当调用该方法时,它都会清除整个缓存中的所有条目。

@CacheEvict(value="listings", allEntries = true)
public List<ListingModel> featuredListingsRequest() throws IOException {
// ... logic
}

重启您的服务器并返回 Sandbox。

任务!

我们的缓存中还没有任何内容,所以让我们在其中放入一些内容,这样我们就可以立即清除它。运行以下 ,在 变量 面板中换入每个以下清单 ID:"listing-1""listing-2""listing-3"

query GetListingAndAmenities($listingId: ID!) {
listing(id: $listingId) {
title
description
numOfBeds
amenities {
category
name
}
}
}

在您运行 几次后,在 Explorer 中打开一个新标签页。使用 featuredListings 构建一个新查询,其数据取用器调用我们添加了清除注释的 ListingServicefeaturedListingsRequest 方法。

将以下 添加到 操作 面板。

query GetFeaturedListings {
featuredListings {
id
title
}
}

运行 ……我们将看到数据,但这并不是我们感兴趣的!返回我们的服务器终端,我们将在缓存中看到日志。

Invalidating entire cache for operation

现在再次尝试第一个 ,针对之前您提供的任意一个清单 ID。

query GetListingAndAmenities($listingId: ID!) {
listing(id: $listingId) {
title
description
numOfBeds
amenities {
category
name
}
}
}

我们会看到现在我们的响应时间再次提高!已从缓存中删除。

尝试仅驱逐缓存中一个单个条目!请看是否可以找到使用@Cacheable@CacheEvict,甚至@CachePut的方法,如果你准备应战。你能说出@Cacheable@CachePut之间的区别吗?(线索在下方可折叠部分!)

实践

缓存数据
通过启用
 
 和提供 
 
,我们可以开始缓存 Java 方法的输出。要将方法标记为其结果应该被缓存的方法,我们使用
 
 注解。如果我们从调用该方法 
 
,其输出将不会被缓存。这是因为 Spring 在该类周围创建一个
 
 ,以提供所有
 
 功能。因此,要缓存我们想要的数据的方法应该从 
 
 调用它们所在的位置。

将该框中的项目拖动至上方的空白处

  • 缓存

  • 在同一类中

  • 缓存管理器

  • @CacheData

  • 数据获取

  • DGS 缓存

  • Spring 缓存

  • 代理

  • @Cacheable

  • 一个 POJO 类

  • 在类外部

要点

  • 与我们缓存 字符串的方式相比,我们可以配置一个单独的缓存来存储调用某个方法的输出
  • 我们可以使用 Spring Boot Starter Cache 和一系列注释在我们的项目中启用缓存
    • @Configuration注解用于表示一个文件包含 bean 定义
    • @EnableCaching注解在 Spring 中启用缓存特性
    • @Cacheable@CachePut@CacheEvict注解可以应用于 Java 方法,以确定如何在缓存中处理其输出
  • 缓存管理器为我们提供了许多开箱即用的缓存,我们可以直接将其应用于我们的缓存数据存储中
  • 我们可以使用setSpecification方法创建一个新的 Caffeine 支持的缓存管理器并提供字符串形式的特提定义

旅程结束

您已完成到最后——并且我们在整个过程中看到我们的处理时间减少了大量庞大的毫秒数!现在,我们的服务器正在缓存我们的 字符串,以更有效地提供服务。与此同时,我们已经发现了如何从我们的 缓存响应并将其重复用于未来的请求。

感谢您参加本课程,我们期待在下一课程中与您相见。

前一页

分享您对本课的疑问和评论

本课程当前处于

beta
.您的反馈有助于我们改进!如果您遇到问题或困惑,请告知我们,我们将竭诚为您提供帮助。所有评论均为公开,且必须遵循 阿波罗行为准则。请注意,已经解决或可解决的评论可能会被删除。

您需要一个 GitHub 帐户才能发布以下内容。没有帐户? 在其 Odyssey 论坛中发布。