缓存

本指南介绍 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 命令执行的步骤
  1. 使用检测到的包管理器安装 @adonisjs/cache 包。

  2. adonisrc.ts 文件中注册以下服务提供者和命令。

    adonisrc.ts
    {
      commands: [
        // ...other commands
        () => import('@adonisjs/cache/commands')
      ],
      providers: [
        // ...other providers
        () => import('@adonisjs/cache/cache_provider')
      ]
    }
  3. 创建 config/cache.ts 文件。

  4. 为所选驱动定义环境变量及其验证。

配置

缓存配置位于 config/cache.ts。该文件定义你的存储、默认存储以及驱动特定的设置。

另见: 配置 stub

config/cache.ts
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。

config/cache.ts
{
  stores: {
    redis: store()
      .useL2Layer(drivers.redis({
        connectionName: 'main',
      }))
  }
}

另见: Redis 设置指南

Memory

使用内存 LRU(最近最少使用)缓存。最适合作为多层配置中的 L1 层,或用于单实例应用。

config/cache.ts
{
  stores: {
    memory: store()
      .useL1Layer(drivers.memory({
        maxSize: '100mb',
        maxItems: 1000,
      }))
  }
}
Database

使用你的数据库作为缓存存储。需要 @adonisjs/lucid。默认情况下会自动创建缓存表。

config/cache.ts
{
  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
config/cache.ts
{
  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 对象获得。

app/controllers/posts_controller.ts
import cache from '@adonisjs/cache/services/main'

获取和设置值

最常见的模式是 getOrSet。它尝试在缓存中查找某个值,如果缺失,则执行工厂函数计算该值、存储它并返回它。

app/controllers/posts_controller.ts
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
  }
}

当你需要对流程有更多控制时,也可以独立使用 getset

app/services/settings_service.ts
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',
})
Warning

缓存的数据必须可序列化为 JSON。如果你正在缓存 Lucid 模型,请在存储前调用 .toJSON().serialize(),或者使用会自动处理序列化的 getOrSet

检查是否存在

使用 hasmissing 来检查某个键是否存在于缓存中,而无需检索其值。

app/controllers/products_controller.ts
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 方法检索一个值并立即将它从缓存中删除。这对于一次性使用的数据(如闪现消息或临时令牌)很有用。

app/controllers/auth_controller.ts
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 删除所有条目。

app/controllers/posts_controller.ts
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)

标签让你可以对相关的缓存条目进行分组,以便一起使它们失效。当一次变更影响多个缓存值时,这尤其有用。例如,当一篇文章被更新时,你可以使所有可能展示它的页面失效。

app/controllers/posts_controller.ts
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
  }
}

你可以为单个条目分配多个标签。当条目的任意一个标签被失效时,该条目就会被失效。

app/services/dashboard_service.ts
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'] })
Tip

避免为每个条目使用过多标签。BentoCache 使用客户端标签,这意味着每次检索都会检查标签失效时间戳。每个条目标签数量过多会拖慢查找速度。

命名空间

命名空间将缓存键归入一个共同前缀之下,让你可以清除某个命名空间中的所有条目,而不影响缓存的其余部分。

app/services/user_service.ts
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 方法在它们之间切换。不使用它时,则使用默认存储。

app/controllers/products_controller.ts
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 条目,从而使每个实例保持一致。

配置

组合 useL1LayeruseL2LayeruseBus 来创建一个多层存储。

config/cache.ts
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
Tip

如果你的应用运行在单个实例上,可以省略 bus。只有当多个实例需要同步它们的 L1 缓存时,才需要 bus。

Bus 只向其他实例发送失效消息(而非实际的值)。当某个实例收到失效消息时,它会从其 L1 缓存中移除该键。下一次读取将从 L2 获取更新后的值。

宽限期

宽限期让你可以在后台刷新值的同时,提供略微过时的缓存数据。这使你的应用能够抵御数据源的临时故障(数据库停机、API 失败)。

当某个缓存条目已过期但仍在其宽限期内时,缓存会向调用方返回过时的值并触发后台刷新。如果刷新失败(例如因为数据库宕机),则会继续提供过时的值,而不是返回错误。

app/controllers/products_controller.ts
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 选项设置重试尝试之间的延迟。

app/controllers/products_controller.ts
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(),
})

你也可以在配置中全局启用宽限期,这样就不必在每个操作上重复设置它们。

config/cache.ts
const cacheConfig = defineConfig({
  default: 'redis',
  ttl: '10m',
  grace: '6h',
  graceBackoff: '30s',

  stores: {
    // ...
  },
})

防击穿保护

当一个缓存条目过期,且许多并发请求同时尝试重新生成它,从而压垮你的数据源时,就会发生缓存击穿(cache stampede)。BentoCache 会自动防止这种情况。

当第一个请求发现某个键缺失或过期时,它会获取一个锁并执行工厂函数。所有其他针对同一键的并发请求都会等待锁释放,然后接收缓存的结果。这意味着无论同时到达多少请求,都只会执行一次工厂函数。

例如,如果 10,000 个请求同时命中一个过期的键,只会执行一次数据库查询。其余 9,999 个请求会在缓存结果可用后接收它。这种保护是内置的,无需任何配置。

超时

超时可防止缓慢的工厂函数阻塞你的响应。BentoCache 支持两种类型。

软超时

软超时与宽限期配合工作。如果工厂耗时超过超时时间,且宽限期内存在过时条目,则会立即返回过时的值,同时工厂在后台继续运行。

app/controllers/products_controller.ts
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(),
})
Note

软超时仅在宽限期内存在过时条目时生效。如果不存在过时条目(首次填充缓存),请求会等待工厂完成。

硬超时

硬超时对等待工厂的时长设置一个绝对上限。如果超过,则抛出异常。工厂会在后台继续执行,以便为后续请求填充缓存。

app/controllers/products_controller.ts
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(),
})

你可以组合使用两种超时。软超时快速返回过时数据,硬超时则充当安全网。

app/controllers/products_controller.ts
const products = await cache.getOrSet({
  key: 'products:featured',
  ttl: '10m',
  grace: '6h',
  timeout: '200ms',
  hardTimeout: '1s',
  factory: () => Product.query().where('featured', true).exec(),
})

自适应缓存

自适应缓存让你可以根据被缓存的数据动态调整缓存选项。当理想的 TTL 取决于实际值时,这很有用。

工厂函数会接收一个上下文对象,其中包含辅助方法,可在检视获取到的数据后调整缓存行为。

app/services/auth_service.ts
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 模板中可用。你可以用它直接在视图中展示缓存的值。

resources/views/pages/home.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
Promise<T>

尝试从缓存中获取一个值。如果缺失,执行工厂、缓存结果并返回它。

await cache.getOrSet({
  key: 'users:1',
  ttl: '10m',
  grace: '6h',
  tags: ['users'],
  factory: () => User.find(1),
})
get
Promise<T | undefined>

从缓存中检索一个值。如果键不存在,返回 undefined

const user = await cache.get({ key: 'users:1' })
set
Promise<void>

将一个值存储到缓存中,可选带 TTL。

await cache.set({ key: 'users:1', value: user, ttl: '10m' })
setForever
Promise<void>

将一个永不过期的值存储到缓存中。

await cache.setForever({ key: 'app:version', value: '2.0.0' })
has
Promise<boolean>

检查某个键是否存在于缓存中。

const exists = await cache.has({ key: 'users:1' })
missing
Promise<boolean>

检查某个键是否不存在于缓存中。

const notCached = await cache.missing({ key: 'users:1' })
pull
Promise<T | undefined>

从缓存中检索一个值并立即删除它。

const token = await cache.pull({ key: 'verify:token:123' })
delete
Promise<void>

从缓存中移除单个键。

await cache.delete({ key: 'users:1' })
deleteMany
Promise<void>

从缓存中移除多个键。

await cache.deleteMany({ keys: ['users:1', 'users:2'] })
deleteByTag
Promise<void>

移除与给定标签关联的所有条目。

await cache.deleteByTag({ tags: ['users'] })
clear
Promise<void>

从缓存(或当前命名空间)中移除所有条目。

await cache.clear()
namespace
CacheNamespace

返回一个命名空间实例。返回实例上的所有操作都限定在该命名空间内。

const usersCache = cache.namespace('users')
await usersCache.set({ key: '1', value: user })
use
CacheStore

按名称返回特定的存储实例。

const redisCache = cache.use('redis')
await redisCache.get({ key: 'users:1' })