跨请求 LRU 缓存:React Server 端共享数据缓存的工程实践(OpenMetadata)
2026/9/16 21:35:26 网站建设 项目流程

跨请求 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: HIGHimpactDescription: 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 缓存的完整闭环:

  1. 初始化缓存实例LRUCache<string, any>指定键为字符串、值为任意类型;
  2. 设置容量上限max: 1000限制最多缓存 1000 个条目,超过后自动淘汰最久未使用的条目(LRU 语义);
  3. 设置过期时间ttl: 5 * 60 * 1000表示条目存活 5 分钟(毫秒单位),到期后自动失效,避免缓存"永远新鲜"导致数据陈旧;
  4. 读缓存(cache-aside 模式):先cache.get(id),命中直接返回;
  5. 未命中回源:未命中才执行数据库查询db.user.findUnique(...)
  6. 回填缓存cache.set(id, user)将查询结果写入缓存,供后续请求复用。

规则文档明确其适用场景:"Use when sequential user actions hit multiple endpoints needing the same data within seconds."——当用户的连续操作会命中多个接口、且这些接口在几秒内需要同一份数据时使用。典型例子包括:

  • 用户资料、租户/工作空间信息等跨接口高频复用的实体;
  • 权限位、特性开关(feature flag)等低频变化的配置;
  • 字典表、元数据等只读且体量可控的数据。

参数解读:maxttl

规则文档示例中出现的两个核心参数,来自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 缓存之所以是安全的例外,是因为它:

  1. 以键寻址,不同请求读取自己的键,互不覆盖;
  2. 值应当是与请求无关的共享数据(用户资料、配置等),而不是"当前请求的临时状态";
  3. 具备淘汰与过期机制,不会无限累积。

反过来,若把"当前请求的用户上下文"存进 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

落地时建议按以下清单自查:

  1. 该数据是否会被多个连续请求共享?——是,才用跨请求缓存;否则用React.cache()即可;
  2. 运行平台是否支持实例复用?——支持,进程内 LRU 即可;纯 Serverless 请改用 Redis 等外部存储;
  3. 是否同时设置了maxttl?——两者缺一不可,分别防内存失控与数据陈旧;
  4. 缓存键是否唯一且不含请求级可变状态?——确保不违反"禁止共享模块状态"规则;
  5. 数据变更时如何失效?——必要时提供显式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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询