缓存
本指南介绍 AdonisJS 应用中的缓存。你将学到如何:
- 配置使用不同驱动(Redis、Memory、Database、DynamoDB)的缓存存储
- 存储、检索和使缓存数据失效
- 使用带有 L1(内存)和 L2(分布式)层的多层缓存
- 使用命名空间和标签组织缓存条目
- 通过宽限期、防击穿保护和超时提升弹性
- 使用 Ace 命令管理你的缓存
概述
@adonisjs/cache 包为你的 AdonisJS 应用提供统一的缓存 API。它构建于
Bentocache
之上,通过提供多层缓存、防缓存击穿保护、宽限期等特性,超越了简单的键值存储。
该包引入了两个关键概念。**驱动(driver)**是底层的存储机制(Redis、内存、数据库)。**存储(store)**是一个配置好的缓存层,它组合一个或多个驱动。你可以在应用中配置多个存储,每个存储使用不同的驱动和设置,并在运行时在它们之间切换。
多层缓存是其最突出的特性。通过将内存 L1 缓存与分布式 L2 缓存(如 Redis)结合,你既能获得本地内存的速度,又能获得共享缓存的持久性和可扩展性。与单层方案相比,这种配置能将响应速度提升 2,000 到 5,000 倍。
安装
使用以下命令安装并配置该包:
node ace add @adonisjs/cache
查看 add 命令执行的步骤
-
使用检测到的包管理器安装
@adonisjs/cache包。 -
在
adonisrc.ts文件中注册以下服务提供者和命令。adonisrc.ts{ commands: [ // ...other commands () => import('@adonisjs/cache/commands') ], providers: [ // ...other providers () => import('@adonisjs/cache/cache_provider') ] } -
创建
config/cache.ts文件。 -
为所选驱动定义环境变量及其验证。
配置
缓存配置位于 config/cache.ts。该文件定义你的存储、默认存储以及驱动特定的设置。
另见: 配置 stub
import { defineConfig, store, drivers } from '@adonisjs/cache'
const cacheConfig = defineConfig({
/**
* The store to use when none is specified
*/
default: 'redis',
/**
* Default TTL for all cached entries.
* Can be overridden per-store or per-operation.
*/
ttl: '30s',
/**
* Configure one or more stores. Each store defines
* its caching layers and driver settings.
*/
stores: {
/**
* A multi-tier store combining in-memory speed
* with Redis persistence and cross-instance sync.
*/
redis: store()
.useL1Layer(drivers.memory({ maxSize: '100mb' }))
.useL2Layer(drivers.redis({ connectionName: 'main' }))
.useBus(drivers.redisBus({ connectionName: 'main' })),
/**
* A simple in-memory store for single-instance apps
*/
memory: store()
.useL1Layer(drivers.memory({ maxSize: '100mb' })),
/**
* A database-backed store using your Lucid connection
*/
database: store()
.useL2Layer(drivers.database({ connectionName: 'default' })),
},
})
export default cacheConfig
可用驱动
Redis
使用 Redis 作为分布式缓存。需要安装并配置 @adonisjs/redis 包。兼容 Redis、Upstash、Vercel KV、Valkey、KeyDB 和 DragonFly。
{
stores: {
redis: store()
.useL2Layer(drivers.redis({
connectionName: 'main',
}))
}
}
另见: Redis 设置指南
Memory
使用内存 LRU(最近最少使用)缓存。最适合作为多层配置中的 L1 层,或用于单实例应用。
{
stores: {
memory: store()
.useL1Layer(drivers.memory({
maxSize: '100mb',
maxItems: 1000,
}))
}
}
Database
使用你的数据库作为缓存存储。需要 @adonisjs/lucid。默认情况下会自动创建缓存表。
{
stores: {
database: store()
.useL2Layer(drivers.database({
connectionName: 'default',
tableName: 'cache',
autoCreateTable: true,
}))
}
}
DynamoDB
使用 AWS DynamoDB 作为缓存存储。需要 @aws-sdk/client-dynamodb。你必须事先创建表,其字符串分区键名为 key,并在 ttl 属性上启用 TTL。
npm i @aws-sdk/client-dynamodb
{
stores: {
dynamo: store()
.useL2Layer(drivers.dynamodb({
table: { name: 'cache' },
region: 'us-east-1',
credentials: {
accessKeyId: env.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: env.get('AWS_SECRET_ACCESS_KEY'),
},
}))
}
}
存储和检索数据
导入缓存服务以与你的缓存交互。所有缓存操作都可通过 cache 对象获得。
import cache from '@adonisjs/cache/services/main'
获取和设置值
最常见的模式是 getOrSet。它尝试在缓存中查找某个值,如果缺失,则执行工厂函数计算该值、存储它并返回它。
import type { HttpContext } from '@adonisjs/core/http'
import cache from '@adonisjs/cache/services/main'
import Post from '#models/post'
export default class PostsController {
async index({ request }: HttpContext) {
const page = request.input('page', 1)
const posts = await cache.getOrSet({
key: `posts:page:${page}`,
ttl: '10m',
factory: () => Post.query().paginate(page, 20),
})
return posts
}
}
当你需要对流程有更多控制时,也可以独立使用 get 和 set。
import cache from '@adonisjs/cache/services/main'
/**
* Store a value with a 5-minute TTL
*/
await cache.set({
key: 'app:settings',
value: { maintenance: false, theme: 'dark' },
ttl: '5m',
})
/**
* Retrieve a value. Returns undefined if the key
* does not exist.
*/
const settings = await cache.get({ key: 'app:settings' })
/**
* Store a value that never expires
*/
await cache.setForever({
key: 'app:version',
value: '2.0.0',
})
缓存的数据必须可序列化为 JSON。如果你正在缓存 Lucid 模型,请在存储前调用 .toJSON() 或 .serialize(),或者使用会自动处理序列化的 getOrSet。
检查是否存在
使用 has 和 missing 来检查某个键是否存在于缓存中,而无需检索其值。
import cache from '@adonisjs/cache/services/main'
if (await cache.has({ key: 'products:featured' })) {
// Key exists in cache
}
if (await cache.missing({ key: 'products:featured' })) {
// Key does not exist
}
拉取值
pull 方法检索一个值并立即将它从缓存中删除。这对于一次性使用的数据(如闪现消息或临时令牌)很有用。
import cache from '@adonisjs/cache/services/main'
/**
* Get the token and remove it from cache in one operation
*/
const token = await cache.pull({ key: `email-verify:${userId}` })
删除数据
使用 delete 删除单个条目,使用 deleteMany 删除多个条目,或使用 clear 删除所有条目。
import cache from '@adonisjs/cache/services/main'
/**
* Delete a single key
*/
await cache.delete({ key: 'posts:page:1' })
/**
* Delete multiple keys at once
*/
await cache.deleteMany({
keys: ['posts:page:1', 'posts:page:2', 'posts:page:3'],
})
/**
* Delete all entries in the cache
*/
await cache.clear()
标签(Tagging)
标签让你可以对相关的缓存条目进行分组,以便一起使它们失效。当一次变更影响多个缓存值时,这尤其有用。例如,当一篇文章被更新时,你可以使所有可能展示它的页面失效。
import cache from '@adonisjs/cache/services/main'
import Post from '#models/post'
export default class PostsController {
async index({ request }: HttpContext) {
const page = request.input('page', 1)
/**
* Tag the cached page with "posts" so we can
* invalidate all pages when any post changes.
*/
const posts = await cache.getOrSet({
key: `posts:page:${page}`,
ttl: '10m',
tags: ['posts'],
factory: () => Post.query().paginate(page, 20),
})
return posts
}
async update({ params, request }: HttpContext) {
const post = await Post.findOrFail(params.id)
post.merge(request.all())
await post.save()
/**
* Invalidate all cache entries tagged with "posts".
* Every paginated page will be refreshed on next request.
*/
await cache.deleteByTag({ tags: ['posts'] })
return post
}
}
你可以为单个条目分配多个标签。当条目的任意一个标签被失效时,该条目就会被失效。
import cache from '@adonisjs/cache/services/main'
await cache.getOrSet({
key: `dashboard:user:${userId}`,
ttl: '5m',
tags: ['dashboard', `user:${userId}`],
factory: () => buildDashboard(userId),
})
/**
* Invalidate a specific user's dashboard
*/
await cache.deleteByTag({ tags: [`user:${userId}`] })
/**
* Or invalidate all dashboards
*/
await cache.deleteByTag({ tags: ['dashboard'] })
避免为每个条目使用过多标签。BentoCache 使用客户端标签,这意味着每次检索都会检查标签失效时间戳。每个条目标签数量过多会拖慢查找速度。
命名空间
命名空间将缓存键归入一个共同前缀之下,让你可以清除某个命名空间中的所有条目,而不影响缓存的其余部分。
import cache from '@adonisjs/cache/services/main'
const usersCache = cache.namespace('users')
/**
* Keys are automatically prefixed with "users:"
* This stores the value under "users:42"
*/
await usersCache.set({ key: '42', value: { name: 'John' } })
await usersCache.set({ key: '43', value: { name: 'Jane' } })
/**
* Retrieve from the namespace
*/
const user = await usersCache.get({ key: '42' })
/**
* Clear only the "users" namespace.
* Other cache entries remain untouched.
*/
await usersCache.clear()
切换存储
当你配置了多个存储时,可以使用 use 方法在它们之间切换。不使用它时,则使用默认存储。
import cache from '@adonisjs/cache/services/main'
/**
* Use the default store
*/
await cache.getOrSet({
key: 'products:featured',
factory: () => Product.query().where('featured', true).exec(),
})
/**
* Use the "memory" store for short-lived data
*/
await cache.use('memory').set({
key: 'rate-limit:user:42',
value: 1,
ttl: '1m',
})
/**
* Use the "database" store for long-lived data
*/
await cache.use('database').setForever({
key: 'site:config',
value: { theme: 'dark' },
})
多层缓存
多层缓存将快速的内存 L1 缓存与持久的分布式 L2 缓存结合。这是运行多个实例的生产应用的推荐配置。
工作原理
当你读取一个值时,缓存首先检查 L1(内存)层。如果找到该值,它会立即返回,无需任何网络调用。如果缺失,它会从 L2(Redis)层获取,在 L1 中存储一份副本,并返回它。
当你写入或删除一个值时,两层都会被更新。一个 bus 会通知其他应用实例逐出它们过时的 L1 条目,从而使每个实例保持一致。
配置
组合 useL1Layer、useL2Layer 和 useBus 来创建一个多层存储。
import { defineConfig, store, drivers } from '@adonisjs/cache'
const cacheConfig = defineConfig({
default: 'multitier',
stores: {
multitier: store()
.useL1Layer(drivers.memory({ maxSize: '100mb' }))
.useL2Layer(drivers.redis({ connectionName: 'main' }))
.useBus(drivers.redisBus({ connectionName: 'main' })),
},
})
export default cacheConfig
如果你的应用运行在单个实例上,可以省略 bus。只有当多个实例需要同步它们的 L1 缓存时,才需要 bus。
Bus 只向其他实例发送失效消息(而非实际的值)。当某个实例收到失效消息时,它会从其 L1 缓存中移除该键。下一次读取将从 L2 获取更新后的值。
宽限期
宽限期让你可以在后台刷新值的同时,提供略微过时的缓存数据。这使你的应用能够抵御数据源的临时故障(数据库停机、API 失败)。
当某个缓存条目已过期但仍在其宽限期内时,缓存会向调用方返回过时的值并触发后台刷新。如果刷新失败(例如因为数据库宕机),则会继续提供过时的值,而不是返回错误。
import cache from '@adonisjs/cache/services/main'
import Product from '#models/product'
const products = await cache.getOrSet({
key: 'products:featured',
/**
* Data is "fresh" for 10 minutes
*/
ttl: '10m',
/**
* After expiring, stale data remains available for 6 hours.
* If the factory fails during this window, the stale
* value is returned instead of an error.
*/
grace: '6h',
factory: () => Product.query().where('featured', true).exec(),
})
退避策略
当工厂调用在宽限期内失败时,你可能不希望在之后的每个请求上都重试。graceBackoff 选项设置重试尝试之间的延迟。
const products = await cache.getOrSet({
key: 'products:featured',
ttl: '10m',
grace: '6h',
/**
* After a failed refresh, wait 5 minutes before
* trying again. Stale data is served in the meantime.
*/
graceBackoff: '5m',
factory: () => Product.query().where('featured', true).exec(),
})
你也可以在配置中全局启用宽限期,这样就不必在每个操作上重复设置它们。
const cacheConfig = defineConfig({
default: 'redis',
ttl: '10m',
grace: '6h',
graceBackoff: '30s',
stores: {
// ...
},
})
防击穿保护
当一个缓存条目过期,且许多并发请求同时尝试重新生成它,从而压垮你的数据源时,就会发生缓存击穿(cache stampede)。BentoCache 会自动防止这种情况。
当第一个请求发现某个键缺失或过期时,它会获取一个锁并执行工厂函数。所有其他针对同一键的并发请求都会等待锁释放,然后接收缓存的结果。这意味着无论同时到达多少请求,都只会执行一次工厂函数。
例如,如果 10,000 个请求同时命中一个过期的键,只会执行一次数据库查询。其余 9,999 个请求会在缓存结果可用后接收它。这种保护是内置的,无需任何配置。
超时
超时可防止缓慢的工厂函数阻塞你的响应。BentoCache 支持两种类型。
软超时
软超时与宽限期配合工作。如果工厂耗时超过超时时间,且宽限期内存在过时条目,则会立即返回过时的值,同时工厂在后台继续运行。
const products = await cache.getOrSet({
key: 'products:featured',
ttl: '10m',
grace: '6h',
/**
* If the factory takes more than 200ms, return the
* stale value immediately. The factory keeps running
* in the background to update the cache.
*/
timeout: '200ms',
factory: () => Product.query().where('featured', true).exec(),
})
软超时仅在宽限期内存在过时条目时生效。如果不存在过时条目(首次填充缓存),请求会等待工厂完成。
硬超时
硬超时对等待工厂的时长设置一个绝对上限。如果超过,则抛出异常。工厂会在后台继续执行,以便为后续请求填充缓存。
const products = await cache.getOrSet({
key: 'products:featured',
ttl: '10m',
/**
* If the factory takes more than 1 second,
* throw an error.
*/
hardTimeout: '1s',
factory: () => Product.query().where('featured', true).exec(),
})
你可以组合使用两种超时。软超时快速返回过时数据,硬超时则充当安全网。
const products = await cache.getOrSet({
key: 'products:featured',
ttl: '10m',
grace: '6h',
timeout: '200ms',
hardTimeout: '1s',
factory: () => Product.query().where('featured', true).exec(),
})
自适应缓存
自适应缓存让你可以根据被缓存的数据动态调整缓存选项。当理想的 TTL 取决于实际值时,这很有用。
工厂函数会接收一个上下文对象,其中包含辅助方法,可在检视获取到的数据后调整缓存行为。
import cache from '@adonisjs/cache/services/main'
const token = await cache.getOrSet({
key: `auth:token:${provider}`,
ttl: '1h',
factory: async (ctx) => {
const token = await fetchOAuthToken(provider)
/**
* Set the TTL based on the token's actual expiration
* rather than using a fixed value
*/
ctx.setOptions({ ttl: `${token.expiresIn}s` })
return token
},
})
上下文对象还提供:
ctx.skip()用于阻止缓存所返回的值ctx.fail()用于阻止缓存并抛出错误ctx.setTags([...])用于根据缓存的值动态设置标签ctx.gracedEntry用于访问宽限期内的过时值(如果有)
Edge 集成
缓存服务在你的 Edge 模板中可用。你可以用它直接在视图中展示缓存的值。
<p>Hello {{ await cache.get({ key: 'username' }) }}</p>
Ace 命令
@adonisjs/cache 包提供了 Ace 命令,用于从终端管理你的缓存。
cache:clear
从某个存储、命名空间或标签中移除所有条目。
# Clear the default store
node ace cache:clear
# Clear a specific store
node ace cache:clear redis
# Clear a specific namespace
node ace cache:clear --namespace=users
# Clear entries matching specific tags
node ace cache:clear --tags=products --tags=users
cache:delete
从缓存中移除特定的键。
# Delete from the default store
node ace cache:delete posts:page:1
# Delete from a specific store
node ace cache:delete posts:page:1 redis
cache:prune
从不支持自动 TTL 过期的驱动(如数据库和文件系统驱动)中移除过期条目。Redis 原生处理过期,无需清理。
# Prune the default store
node ace cache:prune
# Prune a specific store
node ace cache:prune database
方法参考
所有方法都可在从 @adonisjs/cache/services/main 导入的 cache 对象上使用。它们也可在 cache.use() 和 cache.namespace() 返回的实例上使用。
getOrSet
尝试从缓存中获取一个值。如果缺失,执行工厂、缓存结果并返回它。
await cache.getOrSet({
key: 'users:1',
ttl: '10m',
grace: '6h',
tags: ['users'],
factory: () => User.find(1),
})
get
从缓存中检索一个值。如果键不存在,返回 undefined。
const user = await cache.get({ key: 'users:1' })
set
将一个值存储到缓存中,可选带 TTL。
await cache.set({ key: 'users:1', value: user, ttl: '10m' })
setForever
将一个永不过期的值存储到缓存中。
await cache.setForever({ key: 'app:version', value: '2.0.0' })
has
检查某个键是否存在于缓存中。
const exists = await cache.has({ key: 'users:1' })
missing
检查某个键是否不存在于缓存中。
const notCached = await cache.missing({ key: 'users:1' })
pull
从缓存中检索一个值并立即删除它。
const token = await cache.pull({ key: 'verify:token:123' })
delete
从缓存中移除单个键。
await cache.delete({ key: 'users:1' })
deleteMany
从缓存中移除多个键。
await cache.deleteMany({ keys: ['users:1', 'users:2'] })
deleteByTag
移除与给定标签关联的所有条目。
await cache.deleteByTag({ tags: ['users'] })
clear
从缓存(或当前命名空间)中移除所有条目。
await cache.clear()
namespace
返回一个命名空间实例。返回实例上的所有操作都限定在该命名空间内。
const usersCache = cache.namespace('users')
await usersCache.set({ key: '1', value: user })
use
按名称返回特定的存储实例。
const redisCache = cache.use('redis')
await redisCache.get({ key: 'users:1' })