跨请求 LRU 缓存:React Server 端共享数据缓存的工程实践(OpenMetadata)
【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata
导读:本文以仓库中 server-cache-lru.md 这一规则文档为骨架,系统讲解 React/Next.js 服务端场景下"跨请求共享数据"的核心缓存方案——LRU 缓存。你会掌握:为什么
React.cache()无法覆盖跨请求场景、如何用lru-cache实现带容量上限与 TTL 的进程内缓存、缓存键的选取与失效策略、Vercel Fluid Compute 与传统 Serverless 两种运行模型下的差异,以及 LRU 缓存与仓库中其他服务端性能规则的搭配边界。该规则属于 react-best-practices 技能包"Server-Side Performance(服务端性能,HIGH 影响等级)"板块,是 AI 代理在审查或重构服务端代码时的高优先级检查项。
规则定位:服务端性能体系中的跨请求缓存
在 React Best Practices 规则体系中,本规则被归入Section 3 Server-Side Performance(HIGH),对应的规则文件为 server-cache-lru.md(frontmatter 中impact: HIGH、impactDescription: caches across requests)。
服务端性能板块包含多条相互配合的规则,理解本规则需要先看清它们的边界:
| 规则文件 | 作用域 | 核心目标 |
|---|---|---|
| server-cache-react.md | 单次请求内 | 用React.cache()做请求级去重(去重查询/鉴权) |
| server-cache-lru.md(本文) | 跨请求、进程内 | 用 LRU 缓存共享高频只读数据 |
| server-no-shared-module-state.md | 进程内共享状态 | 禁止用模块级可变变量传递请求数据 |
| server-hoist-static-io.md | 模块初始化 | 将静态 I/O(字体、Logo、配置)提升到模块级只加载一次 |
规则文档原文开宗明义地指出:
React.cache()only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
即:React.cache()只在一次请求内生效;对于需要在连续多次请求之间共享的数据(例如用户先点击按钮 A、再点击按钮 B,两个请求都需要同一份数据),应当使用 LRU 缓存。这与 server-no-shared-module-state.md 中列出的"安全例外"正好呼应——该规则明确允许"Shared caches intentionally designed for cross-request reuse and keyed correctly(有意设计用于跨请求复用且正确加键的共享缓存)",LRU 缓存正是这种合法共享状态的标准形态。
为什么React.cache()不够用:请求级去重 vs 跨请求复用
先明确一个前提。React 的cache()API 用于单次请求内的去重:在一个请求的渲染过程中,多个组件同时调用同一个缓存函数,底层只执行一次查询。规则文档 server-cache-react.md 给出了典型用法:
import { cache } from 'react' export const getCurrentUser = cache(async () => { const session = await auth() if (!session?.user?.id) return null return await db.user.findUnique({ where: { id: session.user.id } }) })同一请求内多次调用getCurrentUser()只执行一次查询。但它的缓存生命周期以请求为边界:请求结束后缓存即被丢弃。当用户的连续多个请求(比如点击按钮 A 触发请求 1、点击按钮 B 触发请求 2)都需要同一份数据(如用户资料、权限位、低频变化的配置)时,React.cache()无法避免请求 2 再次打数据库。
此时就需要一个跨请求存活、按 LRU 策略淘汰的进程内缓存。二者的分工可以概括为:
React.cache():解决"同一个请求里重复调用"的重复查询;- LRU 缓存:解决"连续多个请求读取同一份数据"的重复查询,以空间(内存)换时间(数据库往返)。
规则文档 server-no-shared-module-state.md 同时提醒:模块级共享状态用于跨请求缓存时,必须确保缓存键正确,绝不能把请求相关、用户相关的可变数据放进无键共享状态,否则会引发并发污染与数据泄露。LRU 缓存以"键 → 值"组织数据,天然满足"正确加键"的要求,但键的选取仍需谨慎(见下文"缓存键的设计")。
核心实现:基于lru-cache的跨请求缓存
规则文档给出了可直接落地的完整实现(TypeScript 示例,对应 server-cache-lru.md 原文档):
import { LRUCache } from 'lru-cache' const cache = new LRUCache<string, any>({ max: 1000, ttl: 5 * 60 * 1000 // 5 minutes }) export async function getUser(id: string) { const cached = cache.get(id) if (cached) return cached const user = await db.user.findUnique({ where: { id } }) cache.set(id, user) return user } // Request 1: DB query, result cached // Request 2: cache hit, no DB query这段代码展示了 LRU 缓存的完整闭环:
- 初始化缓存实例:
LRUCache<string, any>指定键为字符串、值为任意类型; - 设置容量上限:
max: 1000限制最多缓存 1000 个条目,超过后自动淘汰最久未使用的条目(LRU 语义); - 设置过期时间:
ttl: 5 * 60 * 1000表示条目存活 5 分钟(毫秒单位),到期后自动失效,避免缓存"永远新鲜"导致数据陈旧; - 读缓存(cache-aside 模式):先
cache.get(id),命中直接返回; - 未命中回源:未命中才执行数据库查询
db.user.findUnique(...); - 回填缓存:
cache.set(id, user)将查询结果写入缓存,供后续请求复用。
规则文档明确其适用场景:"Use when sequential user actions hit multiple endpoints needing the same data within seconds."——当用户的连续操作会命中多个接口、且这些接口在几秒内需要同一份数据时使用。典型例子包括:
- 用户资料、租户/工作空间信息等跨接口高频复用的实体;
- 权限位、特性开关(feature flag)等低频变化的配置;
- 字典表、元数据等只读且体量可控的数据。
参数解读:max与ttl
规则文档示例中出现的两个核心参数,来自lru-cache包(官方参考 的 references 中也列入了该项目):
max(最大条目数):限制缓存占用的条目数量,防止内存无限增长。达到上限时,lru-cache会按照"最近最少使用"的顺序淘汰最久未被访问的条目。实际项目中应根据单条数据的内存占用与可用堆内存来设定,例如缓存单条约 1KB 的用户对象时,max: 10000约占用 10MB 级别内存。ttl(生存时间,毫秒):为条目设置绝对过期时间。过期后条目变为不可用(get返回undefined),下次访问会触发重新回源。ttl: 5 * 60 * 1000即 5 分钟。注意 TTL 解决的是"数据新鲜度",max解决的是"内存上限",两者需要组合使用:只有max没有ttl,数据可能长时间陈旧;只有ttl没有max,内存可能失控。
从lru-cache的实现语义看,get命中条目时还会将该条目标记为"最近使用",使其在淘汰排序中靠后,这正是"LRU(Least Recently Used)"名称的来源:最久没被访问的条目最先被淘汰。
缓存键的设计
缓存键决定了"什么算同一份数据"。规则文档示例以id作为键,这是最直接的形态,但实际项目中还需注意:
- 键必须能唯一定义数据:例如按用户维度缓存时用
user:${userId},按"用户+资源"维度缓存时用${userId}:${resourceId},避免不同语义的数据互相覆盖; - 避免把完整对象作为键:对象键会涉及引用相等性问题(与 server-cache-react.md 中
React.cache()使用Object.is浅比较的坑类似),应使用原始值或规范化后的字符串键; - 键命名空间化:多个缓存职责共享一个实例时,用前缀(如
user:、config:)隔离,防止碰撞,也便于在排查问题时按前缀过滤。
运行模型差异:Fluid Compute 与 Serverless 的取舍
规则文档专门讨论了缓存效果与部署运行模型的强耦合关系,这是理解本规则"何时有效"的关键:
Vercel Fluid Compute:进程内缓存天然跨请求共享
With Vercel's Fluid Compute: LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
在 Vercel Fluid Compute 模型下,多个并发请求可以共享同一个函数实例与其中的缓存。因此:
- 进程内的 LRU 缓存能够跨请求持续存活,无需引入 Redis 等外部存储即可获得显著的缓存命中率;
- 模块级缓存(含 LRU 实例)的收益被放大——同一实例上连续到达的请求直接命中内存。
这与同板块的另一条规则 server-hoist-static-io.md 的论断一致:该规则同样指出 Fluid Compute 下模块级资源(字体、Logo)可跨请求常驻内存,"without cold start penalties"。
传统 Serverless:每次调用隔离,进程内缓存无效
In traditional serverless: Each invocation runs in isolation, so consider Redis for cross-process caching.
在传统 Serverless 模型下,每次函数调用都在相互隔离的环境中运行,进程内缓存无法跨调用复用。此时:
- LRU 缓存退化为"单次调用内有效",失去跨请求意义;
- 规则文档明确建议改用 Redis 等外部存储实现跨进程缓存;
- 需要权衡的外部缓存设计点:Redis 键的 TTL、缓存穿透保护(同一热点键并发回源)、缓存雪崩(大量键同时过期)等。
换言之,选用进程内 LRU 还是外部 Redis,首先取决于运行平台,其次才是数据特征。若应用部署在可复用实例的平台上(Fluid Compute、Node 常驻进程、Kubernetes Pod 等),LRU 是零额外依赖的高性价比方案;若部署在纯 Serverless 上,则必须外置缓存。
边界与组合:与相邻规则的协同
LRU 缓存不是孤立的技巧,它与服务端性能板块的其他规则构成一套组合拳。以下是规则文档及其相邻规则共同勾勒出的边界:
与React.cache()的边界(server-cache-react.md)
React.cache()面向请求内去重,LRU 面向跨请求复用;- 实践中可以叠加使用:先在外层用 LRU 缓存"跨请求命中",再用
React.cache()包裹以在单个请求内进一步去重(LRU 命中后的结果在同一请求内多次读取只取一次); - Next.js 环境下,
fetch自带请求记忆化(request memoization),同 URL 同参数的fetch在单请求内自动去重,因此React.cache()主要服务于数据库查询、重计算、鉴权、文件系统等非 fetch 异步操作;LRU 缓存则适用于这些操作的跨请求维度。
与"禁止共享模块状态"的边界(server-no-shared-module-state.md)
该规则警告:不要用模块级可变变量传递请求级数据,因为服务端渲染可并发执行,写共享状态会导致竞态、跨请求污染甚至安全漏洞。LRU 缓存之所以是安全的例外,是因为它:
- 以键寻址,不同请求读取自己的键,互不覆盖;
- 值应当是与请求无关的共享数据(用户资料、配置等),而不是"当前请求的临时状态";
- 具备淘汰与过期机制,不会无限累积。
反过来,若把"当前请求的用户上下文"存进 LRU 缓存并错误地复用于其他请求,就违背了该规则的精神,属于应当避免的误用。
与"提升静态 I/O"的边界(server-hoist-static-io.md)
- 完全静态、永不变更的资产(字体、Logo、模板)应提升到模块级只加载一次,无需 TTL;
- 会变化但低频的数据(配置、用户资料)适合LRU + TTL;
- 该规则的"不适用场景"清单也给出了对照:按请求或用户变化的资产不要做静态提升,而应走带键的缓存或按需获取;运行期可能变化的文件要用带 TTL 的缓存——这正是 LRU 缓存的用武之地。
参考与实践指引
本规则的完整实现与说明,可在仓库以下文件中继续深入:
- 规则原文:skills/vendor/react-best-practices/rules/server-cache-lru.md
- 请求内去重对照规则:skills/vendor/react-best-practices/rules/server-cache-react.md
- 共享模块状态红线:skills/vendor/react-best-practices/rules/server-no-shared-module-state.md
- 静态 I/O 提升:skills/vendor/react-best-practices/rules/server-hoist-static-io.md
- 规则集合总览:skills/vendor/react-best-practices/README.md、skills/vendor/react-best-practices/AGENTS.md
- 依赖清单(含
lru-cache引用):skills/vendor/react-best-practices/metadata.json
落地时建议按以下清单自查:
- 该数据是否会被多个连续请求共享?——是,才用跨请求缓存;否则用
React.cache()即可; - 运行平台是否支持实例复用?——支持,进程内 LRU 即可;纯 Serverless 请改用 Redis 等外部存储;
- 是否同时设置了
max与ttl?——两者缺一不可,分别防内存失控与数据陈旧; - 缓存键是否唯一且不含请求级可变状态?——确保不违反"禁止共享模块状态"规则;
- 数据变更时如何失效?——必要时提供显式
cache.delete(key)或cache.clear()的更新路径,避免脏读。
【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考