服务器端缓存
按字段配置缓存行为
一旦启用, Apollo Server 允许您为模式中的每个 字段 定义缓存控制设置(maxAge
和 scope
):
type Post {id: ID!title: Stringauthor: Authorvotes: Int @cacheControl(maxAge: 30)comments: [Comment]readByCurrentUser: Boolean! @cacheControl(maxAge: 10, scope: PRIVATE)}
当Apollo Server处理一个操作时,它会根据结果字段中的最严格设置,计算出结果正确缓存的策略。然后,您可以使用此计算来支持您想要的任何形式的缓存实现,例如通过通过一个Cache-Control
头来提供您的CDN。
设置缓存提示
您可以在您的模式定义中静态地定义字段级别的缓存提示或在您的解析器中动态地(或两者都可以)
注意,在设置缓存提示时,了解以下内容是很重要的:
- 哪些字段可以安全地缓存
- 缓存的值应该保持有效的时间长度
- 缓存的值是全球性的还是针对用户特定的
这些细节可能因字段而异,甚至是单一的字段类型之间。
在您的模式中(静态)
Apollo Server 识别@cacheControl
指令,您可以在您的模式中使用此指令来定义单独字段或返回特定类型的所有字段的缓存行为。
要使用@cacheControl
指令,您必须在服务器模式中添加以下定义:
enum CacheControlScope {PUBLICPRIVATE}directive @cacheControl(maxAge: Intscope: CacheControlScopeinheritMaxAge: Boolean) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
如果不添加这些定义,Apollo Server 在启动时将抛出Unknown directive "@cacheControl"
错误。
@cacheControl指令接受以下参数:
名称 | 描述 |
---|---|
maxAge | 字段缓存的值有效的最大时间,以秒为单位。默认值是0 ,但您可以设置一个不同的默认值。 |
scope | 如果为PRIVATE ,该字段的值仅对单个用户特定。默认值是PUBLIC 。另请参阅识别为PRIVATE 响应的用户。 |
inheritMaxAge | 如果 true ,则此字段将继承父字段的 maxAge ,而不是使用 默认的 maxAge 。如果您提供此参数,则不要提供 maxAge 。 |
对于应该以相同设置缓存的字段,请使用 @cacheControl
fields 。如果缓存设置可能随时改变,可以使用 动态方法。
重要: Apollo Server 为每个 GraphQL 响应分配一个 maxAge
等于包含的字段中 最低 的 maxAge
。如果任何字段具有 maxAge
值为 0
,则完全不会缓存响应。
类似地,如果包含的任何字段是 Apollo Server 设置响应的 scope
为 PRIVATE
。
字段级定义
此示例为 Post
类型中的两个字段定义了缓存控制设置: votes
和 readByCurrentUser
:
type Post {id: ID!title: Stringauthor: Authorvotes: Int @cacheControl(maxAge: 30)comments: [Comment]readByCurrentUser: Boolean! @cacheControl(maxAge: 10, scope: PRIVATE)}
在此示例中
votes
字段的值将缓存最多 30 秒。readByCurrentUser
字段的值将缓存最多 10 秒,并且其可见性仅限于单个用户。
类型级定义
此示例为返回 Post
对象的所有 schema 字段定义了缓存控制设置:
type Post @cacheControl(maxAge: 240) {id: Int!title: Stringauthor: Authorvotes: Intcomments: [Comment]readByCurrentUser: Boolean!}
如果此 schema 中的另一个对象类型包含一个 Post
类型(或 Post
列表),则该字段的值将缓存最多 240 秒:
type Comment {post: Post! # Cached for up to 240 secondsbody: String!}
注意,字段级设置 会覆盖类型级设置。 在以下情况下,Comment.post
缓存最多为 120 秒,而不是 240 秒:
type Comment {post: Post! @cacheControl(maxAge: 120)body: String!}
在您的解析器中(动态)
您可以在解析特定的字段值时决定如何缓存它。为了支持此功能,Apollo Server 的 缓存控制插件 在传递给每个 resolver 的 info
参数 中提供了一个 cacheControl
对象。
如果您在其解析器中设置了字段的缓存提示,则它 覆盖 您在其 schema 中提供的任何缓存提示。
cacheControl.setCacheHint
The cacheControl
对象包含一个 setCacheHint
方法,您可以通过以下方式调用它:
import { cacheControlFromInfo } from '@apollo/cache-control-types';const resolvers = {Query: {post: (_, { id }, _, info) => {// Access ApolloServerPluginCacheControl's extension of the GraphQLResolveInfo objectconst cacheControl = cacheControlFromInfo(info)cacheControl.setCacheHint({ maxAge: 60, scope: 'PRIVATE' });return find(posts, { id });},},};
The setCacheHint
方法接受一个具有 maxAge
和 scope
字段的对象。
cacheControl.cacheHint
此对象代表当前 cache hint 的字段。其字段包括以下内容:
-
字段的当前
maxAge
和scope
(可能已静态设置) -
A
restrict
方法,它类似于setCacheHint
但不能 relax 现有的提示设置:import { cacheControlFromInfo } from '@apollo/cache-control-types';// Access ApolloServerPluginCacheControl's extension of the GraphQLResolveInfo objectconst cacheControl = cacheControlFromInfo(info)// If we call this first...cacheControl.setCacheHint({ maxAge: 60, scope: 'PRIVATE' });// ...then this changes maxAge (more restrictive) but NOT scope (less restrictive)cacheControl.cacheHint.restrict({ maxAge: 30, scope: 'PUBLIC' });
cacheControl.cacheHintFromType
此方法允许您获取特定 object 类型的默认缓存提示。这在解决可能返回多种对象类型的联合或接口字段时非常有用。
计算缓存行为
为了安全起见,每个 操作响应的缓存行为是根据结果的字段中 最严格的 缓存提示来计算的:
- 响应的
maxAge
等于所有字段中最小的maxAge
。如果该值为0
,则整个结果将不会被缓存。 - 响应的
scope
如果任何字段的scope
是PRIVATE
,则为PRIVATE
。
默认 maxAge
默认情况下,以下模式字段除非您指定一个,否则具有 maxAge
为 0
:
- 根字段(即 Query、Mutation 和 Subscription 类型的字段)
- 因为每个 GraphQL 操作都包含一个根字段,这意味着默认情况下,除非您设置缓存提示,否则不会缓存任何操作结果!
- 返回非标量类型的字段(对象、接口或联合)或非标量类型的列表。
您可以自定义此默认值。
所有其他模式字段(即非根字段返回标量类型)则从其父字段继承其默认maxAge
为什么这些是默认的maxAge
值?
我们关于Apollo服务器缓存的哲学是,只有当响应的每一部分都同意可缓存时,才应该将响应视为可缓存的。同时,我们认为开发者不应该必须指定模式中每个字段的缓存提示。
因此,我们遵循以下启发式方法
- 根字段解析器非常有可能检索数据(因为这些字段没有父字段),因此我们将它们的默认
maxAge
设置为0
以避免自动缓存不应缓存的数据。 - 解析器对于其他非标量字段(对象、接口和联合)也普遍检索数据,因为它们包含任意数量的字段。因此,我们也将它们的默认
maxAge
设置为0
。 - 标量,非根字段解析器很少检索数据,而是通常通过
parent
参数填充数据。因此,这些字段从其父字段继承默认的maxAge以减少模式混乱。
当然,这些启发式方法并不总是正确的!例如,在非标根标量字段的解析器可能实际上检索外部数据。您可以为任何具有不希望的行为的默认值的字段设置自己的缓存提示。
理想情况下,您可以为每个实际上从数据源(如数据库或REST API)检索数据的解析器字段提供maxAge
。大多数其他字段可以从中继承其缓存提示的父级(具有不经常检索数据的解析器的字段)。有关更多信息,请参阅推荐入门使用方法。
设置不同的默认maxAge
您可以设置一个默认的maxAge
,它适用于其它否则接收默认maxAge为0
的字段。
在生产环境中启用它之前,您应识别并解决所有默认maxAge
的异常,但这是一种很好的开始缓存控制的方式。
通过将缓存控制插件传递给 ApolloServer
构造函数来设置您的默认 maxAge
,如下所示:
import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';const server = new ApolloServer({// ...other options...plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 5 })], // 5 seconds});
推荐初始使用方法
您通常不需要为您的模式中的每个 field 指定缓存提示。相反,我们建议从以下几个方面开始:
-
对于应该 永不 缓存的 fields,明确地将
maxAge
设置为0
。 -
为每个具有从数据源(如数据库或REST API)实际获取数据的解析器的 fields 设置
maxAge
。你可以基于相关数据的更新频率来设置maxAge
的值。 -
为每个返回非-标量类型的其他非根 field 设置
inheritMaxAge: true
。- 请注意,您只能以
inheritMaxAge
静态 的方式设置它。
- 请注意,您只能以
示例 maxAge
计算
考虑以下模式
type Query {book: BookcachedBook: Book @cacheControl(maxAge: 60)reader: Reader @cacheControl(maxAge: 40)}type Book {title: StringcachedTitle: String @cacheControl(maxAge: 30)}type Reader {book: Book @cacheControl(inheritMaxAge: true)}
让我们看看一些查询及其相应的 maxAge
值:
# maxAge: 0# Query.book doesn't set a maxAge and it's a root field (default 0).query GetBookTitle {book { # 0cachedTitle # 30}}# maxAge: 60# Query.cachedBook has a maxAge of 60, and Book.title is a scalar, so it# inherits maxAge from its parent by default.query GetCachedBookTitle {cachedBook { # 60title # inherits}}# maxAge: 30# Query.cachedBook has a maxAge of 60, but Book.cachedTitle has# a maxAge of 30.query GetCachedBookCachedTitle {cachedBook { # 60cachedTitle # 30}}# maxAge: 40# Query.reader has a maxAge of 40. Reader.Book is set to# inheritMaxAge from its parent, and Book.title is a scalar# that inherits maxAge from its parent by default.query GetReaderBookTitle {reader { # 40book { # inheritstitle # inherits}}}
与联邦一起使用
在使用 Apollo Federation 时,@cacheControl
指令和 CacheControlScope
枚举可以在 subgraph 的模式中定义。基于 Apollo Server 的 subgraph 将根据非联邦 Apollo Server 向客户端发送响应的方式计算和设置它向网关发送的响应的缓存提示。然后,网关将根据所有子图在 查询计划 执行中接收到的最严格设置来计算整体响应的缓存提示。
设置实体缓存提示
Subgraph schemas 包含在 Query
类型上的 _entities
根 field,因此所有需要 entity 解析 query plans均会默认将 maxAge
设置为 0
。要覆盖此默认行为,您可以在实体的定义中添加一个 @cacheControl
指令:
type Book @key(fields: "isbn") @cacheControl(maxAge: 30) {isbn: String!title: String}
当 _entities 字段被解析时,它将检查适用于缓存提示的具体类型(在上面的示例中为 Book 类型)并应用该提示。
要动态设置缓存提示,在 __resolveReference 解析器的 info 参数中也可以使用 cacheControl 对象及其方法。
在网关中覆盖子图缓存提示
如果一个子图没有指定 max-age,网关将假设其响应(以及随之而来的整体响应)不能被缓存。要覆盖此行为,你可以在 RemoteGraphQLDataSource 的 didReceiveResponse 方法中设置 Cache-Control 头。
另外,如果网关应该忽略将影响操作缓存策略的子图的 Cache-Control 响应头,则可以将 RemoteGraphQLDataSource 的 honorSubgraphCacheControlHeader 属性设置为 false(默认值为 true):
const gateway = new ApolloGateway({// ...buildService({ url }) {return new RemoteGraphQLDataSource({url,honorSubgraphCacheControlHeader: false;});}});
将 honorSubgraphCacheControlHeader 设置为 false 的效果是,对响应的缓存能力没有任何影响。换句话说,此属性不会决定响应是否可缓存,但它排除了子图的 Cache-Control 头在网关计算中的考虑。如果在计算整体 Cache-Control 头时排除了所有子图,发送给客户端的响应将不会被缓存。
使用 CDN 进行缓存
默认情况下,Apollo Server 会为所有响应发送一个 Cache-Control 头,该头描述了响应的缓存策略。
当响应可缓存时,头部的格式如下
Cache-Control: max-age=60, private
当响应不可缓存时,头的值是 Cache-Control: no-store。
为了可缓存,以下所有条件都必须成立
- 操作有一个非零的 maxAge。
- 操作有一个单一响应而不是增量交付响应。
- 响应中没有错误。
如果您在 CDN 或其他缓存代理后面运行 Apollo Server,您可以配置它使用此头部的值来适当地缓存响应。有关详细信息,请参阅您的 CDN 文档(例如,这里是 Amazon CloudFront 的文档)。
某些 CDN 需要自定义头部来缓存或设置cache-control
头部的自定义值,如s-maxage
。您可以相应地配置您的ApolloServer
实例,通过告诉内置的缓存控制插件仅计算策略而不设置 HTTP 头部,并指定您的自定义插件。
new ApolloServer({plugins: [ApolloServerPluginCacheControl({ calculateHttpHeaders: false }),{async requestDidStart() {return {async willSendResponse(requestContext) {const { response, overallCachePolicy } = requestContext;const policyIfCacheable = overallCachePolicy.policyIfCacheable();if (policyIfCacheable && !response.headers && response.http) {response.http.headers.set('cache-control',// ... or the values your CDN recommends`max-age=0, s-maxage=${overallCachePolicy.maxAge}, ${policyIfCacheable.scope.toLowerCase()}`,);}},};},},],});
使用 GET 请求
因为 CDN 和缓存代理只缓存 GET 请求(不缓存 POST 请求,默认情况下Apollo Client为所有操作发送),我们建议在 Apollo Client 中启用自动持久化查询以及useGETForHashedQueries
选项。
或者,您可以在您的HttpLink实例中设置ApolloClient
的useGETForQueries
选项。然而,大多数浏览器对 GET 请求强制实施大小限制,并且较大的查询字符串可能会超过此限制。
禁用缓存控制
您可以通过自己安装Apollo Server Plugin Cache Control 插件来阻止Cache-Control
头部的设置,并将calculateHttpHeaders
设置为 false:
import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';const server = new ApolloServer({// ...other options...plugins: [ApolloServerPluginCacheControl({ calculateHttpHeaders: false })],});
如果您这样做,缓存控制插件仍然为每个操作响应计算缓存行为。然后您可以使用这些信息与其它插件(如响应缓存插件)一起使用。
如果要完全禁用缓存控制计算,则安装ApolloServerPluginCacheControlDisabled
插件(此插件除了防止安装缓存控制插件外没有其他作用):
import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';const server = new ApolloServer({// ...other options...plugins: [ApolloServerPluginCacheControlDisabled()],});
使用 responseCachePlugin
缓存(高级)
您可以在Redis、Memcached或Apollo Server的默认内存缓存等存储中缓存Apollo Server查询响应。更多信息,请参阅配置缓存后端。
内存缓存设置
要设置您的内存响应缓存,首先导入responseCachePlugin
并将其提供给ApolloServer
构造函数:
import responseCachePlugin from '@apollo/server-plugin-response-cache';const server = new ApolloServer({// ...other options...plugins: [responseCachePlugin()],});
在初始化时,此插件会根据字段设置自动开始缓存响应。
该插件使用与Apollo Server其他功能相同的内存LRU缓存。对于具有多个服务器实例的环境,您可能希望使用共享缓存后端,如Memcached或Redis。
Memcached/Redis设置
请参阅配置外部缓存。
为PRIVATE
响应识别用户
如果缓存响应具有PRIVATE
作用域,则其值仅可供单个用户访问。为了强制执行此限制,缓存需要知道如何识别该用户。
要启用此识别,您需要向responseCachePlugin
提供sessionId
函数,例如:
import responseCachePlugin from '@apollo/server-plugin-response-cache';const server = new ApolloServer({// ...other settings...plugins: [responseCachePlugin({sessionId: (requestContext) =>requestContext.request.http.headers.get('session-id') || null,}),],});
重要:如果您不定义sessionId
函数,则不会缓存任何PRIVATE
响应。
缓存使用此函数的返回值来识别之后可以访问缓存PRIVATE
响应的用户。在上面的示例中,函数使用原始操作请求中的session-id
头。
如果客户端稍后执行了完全相同的查询并且具有相同的标识符,Apollo Server会在响应值仍然可用的情况下返回PRIVATE
缓存的响应。
隔离已登录和未登录用户的响应
默认情况下,PUBLIC
缓存的响应可供所有用户访问。但是,如果您定义了sessionId
函数(如上面所示),则Apollo Server将为每个PUBLIC响应缓存多达两个版本:
- 一个版本针对具有无
sessionId
的用户 - 一个版本针对具有非空
sessionId
的用户
这使您可以为已登录和未登录的用户缓存不同的响应。例如,您可能希望根据用户的登录状态显示不同页眉菜单项。
配置读取和写入
除了 的 sessionId
功能外,您还可以向您的 responseCachePlugin
提供以下功能来配置缓存读取和写入。每个这些函数都将 GraphQLRequestContext
(表示传入的操作)作为参数。
函数 | 描述 |
---|---|
extraCacheKeyData | 此函数的返回值(任何可 JSON 序列化的对象)将添加到缓存的键中。例如,如果您的 API 包含可翻译的文本,则此函数可以返回从 requestContext.request.http.headers.get('accept-language') 导出的字符串。 |
shouldReadFromCache | 如果此函数返回 false ,Apollo Server 跳过 缓存进行传入的操作,即使有有效的响应可用。 |
shouldWriteToCache | 如果此函数返回 false ,Apollo Server 不会将其响应缓存到传入的操作中,即使响应的 maxAge 大于 0 。 |
generateCacheKey | 自定义生成缓存键。默认情况下,这是包含相关数据的对象的 JSON 编码的 SHA256 哈希。 |