缓存驱逐响应
使用自定义缓存键的高级缓存驱逐模式
💡 提示
要获取此缓存驱逐方案的实用示例,请参阅响应缓存驱逐仓库。
Apollo Server's 全响应缓存插件(@apollo/server-plugin-response-cache
)缓存操作结果一段时间(生存时间或TTL)。之后,结果将从缓存中删除,并在客户端执行操作时下次由服务器完全解析。
避免响应缓存中出现陈旧数据的简单方法是为默认TTL设置一个较短的时间。然而,这限制了缓存对很少(或永不)更改的响应的有效性。
从 3.7.0 版本开始,通过自定义缓存键支持全响应缓存插件的高级缓存驱逐模式。这允许你设置一个较长的默认TTL,并提高缓存命中率,因为你可以在相关事件发生时选择性地驱逐缓存的响应。
自定义缓存键
这是通过定义一个可以在缓存中稍后搜索的模式来定义自定义响应缓存键。这个键应该由什么组成以及如何结构化取决于用例以及缓存实现支持的搜索模式。
确保缓存键的唯一性
请注意,每个键都链接到一个完整的响应对象,因此如果你的键太通用,你可能会返回错误的查询数据。例如,仅根据操作名称生成缓存键,对所有同名操作都将产生相同响应,即使整个查询不同。确保您的键为每次返回不同数据的传入操作都是唯一的。
定义自定义缓存键
如上所述,Full Response Cache 插件的3.7.0
版本引入了generateCacheKey
配置方法。这个函数的响应将被用作缓存键以存储当前的查询响应。
以下是方法签名
generateCacheKey(requestContext: GraphQLRequestContext<Record<string, any>>,keyData: unknown,): string;
参数requestContext
包含有关运行中的GraphQL请求的数据,例如请求/响应对象以及传递到你的上下文对象函数。这些数据对象的任何部分都可以用作你的缓存键的一部分。
"keyData" 参数可以用于确保您的键的唯一性。在大多数情况下,通过哈希该变量应该就足以为每次操作生成唯一的键。实际上,默认实现将此参数的 JSON.stringify 版本哈希为缓存键。。
在这个例子中,我们将默认键名与传入的操作名称前缀一起使用:
import { createHash } from 'crypto';function sha(s: string) {return createHash('sha256').update(s).digest('hex');}generateCacheKey(requestContext, keyData) {const operationName = requestContext.request.operationName ?? 'unnamed';const key = operationName + ':' + sha(JSON.stringify(keyData));return key;}
名为 "MyOpName" 的操作示例键:
keyv:fqc:MyOpName:e7eed80930547ed4ab4ece81a18955967831ff4c40757eda9bf1f0de84e042f8
这种方法确保所有缓存键都足够唯一以存储唯一的响应,但同时也提供了一个模式,我们可以使用这个模式来选择性地根据我们的操作名称移除缓存条目。
移除缓存条目
删除缓存响应条目主要有两种策略:从shell提示符手动删除,或者响应某些事件,如突变。
一旦使用自定义缓存键,从缓存中实际删除条目将取决于您的缓存后端,因为每个后端都有不同的列表示例和删除键的方式。我们将使用Redis来探索这两种选项。
手动删除
如果您需要删除用于本地测试或调试的缓存条目,可能只需定义特定的缓存键模式,并使用redis-cli
按需删除条目即可。
以下是一个示例,展示如何删除匹配给定模式的Redis实例中的所有键
redis-cli --raw KEYS "$PATTERN" | xargs redis-cli del
该命令列出匹配任何glob-style "$PATTERN" 的所有键,并逐个删除它们。
以下是一个示例,使用上面描述的操作名称前缀模式来删除所有未命名操作的条目:
redis-cli --raw KEYS "keyv:fqc:unnamed*" | xargs redis-cli del
Redis KEYS 命令的文档不推荐在生产应用程序代码中使用 KEYS
函数,并且在使用时要“极端谨慎”。Redis特别推荐使用 SCAN
,这在 事件驱动逐出 中进行了描述.
这种方法的实用性将取决于您缓存中存储的记录数量以及模式搜索的性能,以及需要删除的记录数量。如果您的搜索扫描并/或返回数百万条记录,此方法可能应该避免在生产环境中使用。
事件驱动逐出
大多数其他情况需要根据某些事件逐出缓存条目。Response Cache Eviction 存储库提供了在执行特定 更改 时从缓存中逐出某些 操作 响应的完整教程。
Redis客户端目前没有根据模式批量删除条目的方法。因此,我们的基于事件的解决方案需要做类似算法:通过模式查找键,然后删除这些键。
以下代码片段来自 上述存储库:
import {createClient, RedisClientType} from 'redis';const deleteByPrefix = async (prefix: string) => {const client = createClient({url: 'redis://localhost:6379'});await client.connect();const scanIterator = client.scanIterator({MATCH: `keyv:fqc:${prefix}*`,COUNT: 2000});let keys = [];for await (const key of scanIterator) {keys.push(key);}if (keys.length > 0) {await client.del(keys); // This is blocking, consider handling async in production if the number of keys is large}return keys;};
此解决方案使用 scanIterator
函数(该函数使用 SCAN
Redis函数)以内存高效的方式遍历缓存条目,而不是上述 KEYS
方法。 SCAN
方法更适用于生产环境。
可以在您的上下文对象中添加 deleteByPrefix
方法,然后执行您的 mutation 解析器 来从缓存中删除某些 operations。
最后的想法
应谨慎使用上述提到的任何逐出解决方案。确保您了解您的设置将对您的缓存产生何种影响。在测试不同的使用案例时监测您的缓存服务器是个好主意,以确保您不会过度负担您的缓存。