黑白梦黑白梦

toggle navtoggle nav
  • 文章
  • 专栏
  • 文章
  • 专栏

Next.js 缓存与 Redis 广播多实例模式

发布于 2026-02-16

使用 unstable_cache + revalidateTag 组合实现 Next.js 服务端函数缓存,并结合 Redis 广播,在多实例部署中实现缓存一致性。

Next.js 缓存常见用法

基于数据函数的缓存 unstable_cache

https://nextjs.org/docs/app/api-reference/functions/unstable_cache

适合服务端数据读取,如直接读数据库或聚合多源数据的服务端函数,配合 tags 与 revalidate 实现自动失效与定时刷新。

典型结构:

return unstable_cache(
  async () => getPublicPostList(input),
  ['public-post-list', String(input.page), String(input.count), String(version)],
  { revalidate: 300, tags: ['public-posts'] },
)();

关键点:

  • cache key 需要包含业务参数与版本号,保证多实例下能强制刷新
  • tags 用于数据变更时统一失效
  • revalidate 作为兜底的时间刷新

主动失效缓存 revalidateTag

https://nextjs.org/docs/app/api-reference/functions/revalidateTag

当写入数据后主动失效缓存。

  • 第一个参数是需要失效的 tags ,支持传多个。
  • 第二个参数是 缓存“profile” (缓存策略配置)的名称,用来决定 tag 被 revalidate 后的行为。
    • max :更偏向 stale‑while‑revalidate ,用户更可能先拿到旧数据,后台更新,适合读多写少、对瞬时一致性要求不高的场景。
    • default :取决于框架默认策略(通常也偏 SWR,但具体行为受版本和配置影响),更保守。

写入数据后触发,如:

  • 更新文章后失效公共文章与专栏缓存
  • 更新专栏关联后失效专栏缓存

fetch 缓存和 revalidatePath 页面级失效

还有一种常见的缓存用法是 fetch 级别缓存与刷新:

  • 在 Server Component 或 Route Handler 中使用 fetch 的 cache/next.revalidate 控制
  • 适合通过 HTTP 请求获取数据的场景,依赖 Next.js 内建的请求缓存与 tag/revalidate
  • 粒度是“请求级别”,更容易和外部 API 或内部 HTTP 接口对齐
await fetch(url, { cache: 'force-cache' });
await fetch(url, { cache: 'no-store' });
await fetch(url, { next: { revalidate: 300, tags: ['posts'] } });

fetch 缓存失效:

  • 按路径失效 revalidatePath:适合页面级缓存失效
    • 粒度是“路由级别”,主要影响对应路径的页面渲染缓存
    • 维护成本低、心智负担小
  • 可以通过 revalidateTag 实现”请求级别“的粒度失效
import { revalidatePath } from 'next/cache';
revalidatePath('/posts');

Redis 广播 + 多实例缓存失效

项目采用了 unstable_cache + revalidateTag 来管理缓存。使用 pm2 多实例部署后,有一个问题,每个实例 revalidateTag 只能影响自己本身,缓存刷新不一致。

解决方案

采用 Redis 广播的方式,多实例部署时,确保 unstable_cache 的 tag 失效可以同步到所有实例。

  • 每个 tag 维护一个版本号(next:tagver:<tag>)
  • 读缓存前先读取版本号并加入 cache key
  • 变更时在本实例 revalidateTag,同时写入版本号并广播到 Redis
  • 其他实例收到广播后更新本地版本号

Redis 广播基础

  • 采用 Redis Pub/Sub 机制:发布者向指定频道发布消息,所有订阅该频道的实例都会收到
  • 广播只负责通知“事件发生了”,具体业务由订阅者自行处理

发布者示例:

import { createClient } from 'redis';

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
await client.publish('events', JSON.stringify({ type: 'POST_UPDATED', id: 1 }));

订阅者示例:

import { createClient } from 'redis';

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
await client.subscribe('events', (message) => {
  const payload = JSON.parse(message);
  console.log('收到事件', payload);
});

tag 版本号的作用与用法

  • 作用:把“版本变化”编码进缓存 key,让多实例即使没有立即 revalidate,也能通过版本号变化触发新的缓存读取
  • 用法:读缓存前取版本号并拼进 key,写入时对版本号自增并广播
  • 好处:避免单实例 revalidate 造成的缓存不同步问题,且不会强制所有实例立即重算

读缓存流程

  1. ensureRevalidateSubscriber() 启动订阅
  2. getCacheTagVersion(tag) 读本地或 Redis 版本号
  3. 版本号拼进 unstable_cache key

示例(文章列表):

ensureRevalidateSubscriber();
const version = await getCacheTagVersion(PUBLIC_POST_TAG);
return unstable_cache(
  async () => getPublicPostList(input),
  ['public-post-list', String(input.page), String(input.count), String(version)],
  { revalidate: 300, tags: [PUBLIC_POST_TAG] },
)();

写入失效流程

  1. 写库后调用 revalidateTags([tag...])
  2. 本实例执行 revalidateTag
  3. Redis 中 tag 版本号自增
  4. 广播版本变更消息,其他实例更新本地版本

降级策略

  • 没有 REDIS_URL 时只在本实例失效,版本号走内存 Map
  • 有 Redis 时采用发布订阅,确保多实例一致

使用建议

  • 读缓存统一走 services 层,避免组件直接操作
  • 变更数据只调用 revalidateTags,不要在组件/Action 里直接 revalidateTag
  • cache key 必须包含版本号,否则多实例下无法强制刷新

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com