概述
在我们的 GraphQL 查询 验证过程中,我们节省了时间和精力;现在是提升我们 数据获取步骤的性能的时候了!
在这一课中,我们将
- 比较缓存 操作字符串和缓存由方法返回的数据
- 复习 Spring Boot 中的缓存
- 讨论标记为可缓存的方法的注意事项
- 实例化 Caffeine 缓存管理器
- 应用
@Cacheable
和@CacheEvict
注解到我们的 数据源方法
缓存操作响应
在上节课中,我们与 GraphQL 查询执行过程的生命周期相连;通过提供 PreparsedDocumentProvider
实例,我们可以在解析和验证是否真的必需的情况下进行干预。在缓存我们的服务器解析 数据(用于 操作)的情况下,我们将使用一种不同的方法
在我们的 datasources/ListingService
文件中,我们有一些方法可用于从 REST API 请求列表数据。我们将从了解缓存如何与其中一种方法(即 listingRequest
)配合使用开始。
@Componentpublic 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}
如有必要,重新载入依赖项,让新包生效。
以下是我们为配置缓存要遵循的步骤。
- 我们要为我们的缓存创建一个新的配置文件。
- 我们要应用
@Configuration
和@EnableCaching
注释。 - 我们将定义一个
cacheManager
方法,该方法使用 Caffeine 创建新的缓存管理器,并指定我们的缓存应该如何运行。 - 最后,我们将对我们的
@Cacheable
和@CacheEvict
注释进行测试数据源方法。
让我们开始吧!
步骤 1:CacheJavaConfig
类
在com.example.listings
目录中,与我们的ListingsApplication
和CachingPreparsedDocumentProvider
文件处于同一级别,创建名为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 容器管理的对象)。我们将在我们的类定义之上添加它。
@Configuration // 🫘 This class is a source of bean definitions!public class CacheJavaConfig {}
在@EnableCaching
注释应用到配置文件时,它将启用 Spring 中的缓存。让我们也添加这个!
@Configuration@EnableCaching // 💡 Caching is switched on!public class CacheJavaConfig {}
需要注意的是,这些注释并不会给我们提供即用型的实际缓存。相反,它们会让我们可以访问缓存逻辑,然后我们可以将其应用到我们提供的实际缓存实现。要访问所有这些逻辑,我们需要提供一个CacheManager
实例。
步骤 3:定义缓存管理器
来自 Spring 缓存包的CacheManager
接口被认为是“缓存抽象”的实现。它充当我们插入的缓存的“管理器”,并且它提供逻辑来更新或从缓存中删除条目。其主要目的是咨询缓存以了解 Java 方法实际需要调用,还是其结果可以从缓存中提供。
Spring 文档指出在使用缓存管理器时我们需要执行两个步骤
- 配置将实际存储数据的缓存
- 标识输出应被缓存的方法
缓存管理器提供了其余内容!我们创建它作为返回 CacheManager
接口实例的 Bean。我们还将用 @Bean
注解标记它以提高清晰度。
@Beanpublic CacheManager cacheManager() {}
在本例中,我们再次使用 Caffeine 创建缓存。在 cacheManager
方法内部,我们将创建一个类型为 CaffeineCacheManager
(它实现了 CacheManager
接口)的新 cacheManager
,并且我们将传递我们希望给我们的缓存取的名字:“listings”。
public CacheManager cacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager("listings");}
我们有几种方法可以为缓存提供配置。我们准备定义包含配置设置的字符串并使用 setCacheSpecification
方法将其应用于缓存管理器。
注意:如需了解配置缓存的替换方法,请查阅 官方文档。
在这个字符串中,我们将允许 initialCapacity
为 100
, maximumSize
为 500
, expireAfterAccess
为 2m
表示 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
的方法从 类外 被调用时,请求在调用实际方法之前会首先通过缓存代理,并启用缓存行为。
但是从 类内 发出的请求会避开缓存代理,因此对缓存中的条目不会产生任何存储或更新的效果。
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 proxythat 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")
让我们试试吧!重新启动服务器,然后返回沙盒。
我们将运行查询以获取特定清单,以便我们达到我们刚刚标记为“可缓存”的这个底层方法。在操作面板中添加以下GraphQL查询。
query GetListingAndAmenities($listingId: ID!) {listing(id: $listingId) {titledescriptionnumOfBeds}}
并在变量面板中:
{"listingId": "listing-2"}
运行查询,并且...是,数据如期而至。但是请注意响应面板顶部的毫秒数。我们可能正在看到 300 毫秒以上的情况。
这是我们的基准;现在让我们再试一次查询!
在完全相同的查询上再次点击播放按钮。
您是否看到记录的毫秒数大幅度下降?现在我们应下降到 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) {titledescriptionnumOfBedsamenities {categoryname}}}
在您运行 查询 几次后,在 Explorer 中打开一个新标签页。使用 featuredListings
字段 构建一个新查询,其数据取用器调用我们添加了清除注释的 ListingService
的 featuredListingsRequest
方法。
将以下 GraphQL 查询 添加到 操作 面板。
query GetFeaturedListings {featuredListings {idtitle}}
运行 查询……我们将看到数据,但这并不是我们感兴趣的!返回我们的服务器终端,我们将在缓存中看到日志。
Invalidating entire cache for operation
现在再次尝试第一个 查询,针对之前您提供的任意一个清单 ID。
query GetListingAndAmenities($listingId: ID!) {listing(id: $listingId) {titledescriptionnumOfBedsamenities {categoryname}}}
我们会看到现在我们的响应时间再次提高!操作已从缓存中删除。
尝试仅驱逐缓存中一个单个条目!请看是否可以找到使用@Cacheable
、@CacheEvict
,甚至@CachePut
的方法,如果你准备应战。你能说出@Cacheable
和@CachePut
之间的区别吗?(线索在下方可折叠部分!)
实践
将该框中的项目拖动至上方的空白处
缓存
在同一类中
缓存管理器
@CacheData
数据获取
DGS 缓存
Spring 缓存
代理
@Cacheable
一个 POJO 类
在类外部
要点
- 与我们缓存操作 字符串的方式相比,我们可以配置一个单独的缓存来存储调用某个方法的输出
- 我们可以使用 Spring Boot Starter Cache 和一系列注释在我们的项目中启用缓存
-
@Configuration
注解用于表示一个文件包含 bean 定义 -
@EnableCaching
注解在 Spring 中启用缓存特性 -
@Cacheable
、@CachePut
和@CacheEvict
注解可以应用于 Java 方法,以确定如何在缓存中处理其输出
-
- 缓存管理器为我们提供了许多开箱即用的缓存操作,我们可以直接将其应用于我们的缓存数据存储中
- 我们可以使用
setSpecification
方法创建一个新的 Caffeine 支持的缓存管理器并提供字符串形式的特提定义
旅程结束
您已完成到最后——并且我们在整个过程中看到我们的处理时间减少了大量庞大的毫秒数!现在,我们的服务器正在缓存我们的 操作字符串,以更有效地提供服务。与此同时,我们已经发现了如何从我们的 数据源缓存响应并将其重复用于未来的请求。
感谢您参加本课程,我们期待在下一课程中与您相见。
分享您对本课的疑问和评论
本课程当前处于
您需要一个 GitHub 帐户才能发布以下内容。没有帐户? 在其 Odyssey 论坛中发布。