 的請求邊界)
Polar 中的跨請求 LRU 緩存在 Next.js/Vercel 上打通 React.cache() 的請求邊界【免費下載鏈接】polarPolar — A billing platform for the intelligence era項目地址: https://gitcode.com/GitHub_Trending/po/polar導(dǎo)讀本指南講解來自 Vercel React 最佳實踐技能包vercel-react-best-practices位于 clients/apps/web/.agents/skills/vercel-react-best-practices/中的一條 HIGH 優(yōu)先級規(guī)則跨請求 LRU 緩存Cross-Request LRU Caching。React.cache()只能在一個請求內(nèi)做去重而 LRU 緩存能把用戶連續(xù)點擊多個端點時共享的數(shù)據(jù)在函數(shù)實例內(nèi)跨請求復(fù)用避免重復(fù)數(shù)據(jù)庫查詢。讀完本文你將掌握LRU 緩存的標(biāo)準(zhǔn)實現(xiàn)、它與React.cache()的邊界劃分、在 Vercel Fluid Compute 與傳統(tǒng) Serverless 兩種部署模型下的取舍以及 Polar 倉庫中真實存在的每請求緩存代碼作為對照。一、問題背景為什么React.cache()不夠用在 server-cache-react.md 規(guī)則中React.cache()被用于單請求內(nèi)的去重在同一個請求的組件樹里多次調(diào)用同一個異步函數(shù)只會真正執(zhí)行一次。典型場景包括數(shù)據(jù)庫查詢Prisma、Drizzle 等認(rèn)證信息獲取文件系統(tǒng)操作其他非fetch的異步工作這在 Polar 的 web 端有大量真實應(yīng)用。以 src/utils/organization.ts 為例// Tell React to memoize it for the duration of the request const _getOrganizationBySlugCached ( api: Client, slug: string, cached: boolean true, ) _getOrganizationBySlug(api, slug, cached) export const getOrganizationBySlug cache(_getOrganizationBySlugCached)類似的模式還出現(xiàn)在 src/utils/order.tsgetOrderById cache(_getOrderById)、src/utils/checkout.ts、src/utils/product.ts、src/utils/subscription.ts 等數(shù)據(jù)訪問工具中。但React.cache()有一個明確的邊界緩存生命周期只有一個請求。當(dāng)用戶點擊按鈕 A、再點擊按鈕 B這是兩個順序發(fā)生的獨立請求React.cache()的緩存已經(jīng)失效相同的數(shù)據(jù)會被重新查詢。這正是 server-cache-lru.md 這條規(guī)則要解決的問題對于順序請求之間共享的數(shù)據(jù)改用 LRU 緩存。判斷基準(zhǔn)如果多個端點需要在幾秒內(nèi)讀到同一份數(shù)據(jù)例如用戶連續(xù)操作觸發(fā)的多個請求就應(yīng)該考慮 LRU 緩存而不是只依賴React.cache()。二、標(biāo)準(zhǔn)實現(xiàn)基于lru-cache的跨請求緩存規(guī)則給出了一個可直接落地的實現(xiàn)骨架lru-cache是社區(qū)廣泛使用的 LRU 緩存庫import { LRUCache } from lru-cache const cache new LRUCachestring, 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兩個核心配置項說明如下配置項示例值作用與建議max1000緩存最大條目數(shù)超出后按 LRU 策略淘汰最久未訪問的條目防止內(nèi)存無限增長。應(yīng)根據(jù)單實例可容納的數(shù)據(jù)量合理設(shè)置ttl5 * 60 * 1000條目存活時間毫秒。過期條目在下次訪問時自動失效并重新查詢是緩存與數(shù)據(jù)一致性之間的第一道平衡注意緩存讀取邏輯存在一個常見陷阱如果緩存值本身可能是null/undefined例如用戶不存在簡單的if (cached) return cached會把合法空值當(dāng)成未命中。更穩(wěn)妥的寫法是用cache.has(id)先判斷存在性或引入空值占位符來緩存負(fù)面結(jié)果。另外模塊級單例是這種緩存的關(guān)鍵cache實例必須定義在模塊頂層而非函數(shù)內(nèi)部或每個請求內(nèi)新建否則每個請求都會創(chuàng)建全新的緩存跨請求共享就無從談起。這與server-hoist-static-io將靜態(tài) I/O 提升到模塊級的思路一脈相承。三、與 Vercel Fluid Compute 的協(xié)同無需 Redis 的跨請求共享規(guī)則特別指出在Vercel Fluid Compute模型下LRU 緩存的收益會被放大多個并發(fā)請求可以共享同一個函數(shù)實例function instance和它的內(nèi)存緩存。這意味著緩存可以跨請求持久存在無需引入 Redis 這類外部存儲。這是因為 Fluid Compute 讓函數(shù)實例在處理完一個請求后繼續(xù)存活、復(fù)用而非每次冷啟動銷毀。實例內(nèi)存中的 LRU 緩存因此天然成為進(jìn)程內(nèi)共享緩存請求 1 查詢數(shù)據(jù)并寫入緩存請求 2、請求 3…落在同一實例上時直接命中內(nèi)存零網(wǎng)絡(luò)開銷、零外部依賴在流量較小時單個實例即可服務(wù)大量順序請求命中率可觀。這種方案的工程價值在于架構(gòu)極簡沒有額外的運維組件、沒有緩存一致性的分布式難題、也沒有冷熱數(shù)據(jù)遷移成本。對 Polar 這種以計費數(shù)據(jù)為主的 Web 應(yīng)用凡是秒級窗口內(nèi)、跨端點復(fù)用的熱點數(shù)據(jù)都符合這種模式的使用前提。四、傳統(tǒng) Serverless 下的取舍何時該上 Redis規(guī)則同時給出了對照傳統(tǒng) Serverless 每次調(diào)用都在隔離環(huán)境中運行函數(shù)實例之間不共享內(nèi)存進(jìn)程內(nèi) LRU 緩存無法跨調(diào)用復(fù)用。此時需要區(qū)分兩種訴求單次調(diào)用內(nèi)的去重仍然用React.cache()參考 server-cache-react.md跨進(jìn)程/跨實例的共享緩存引入 Redis 等外部存儲例如ioredis 帶 TTL 的SET/GET或 Redis 的 LRU 淘汰策略maxmemory-policy allkeys-lru。結(jié)論可以歸納為一張決策表部署形態(tài)請求內(nèi)去重跨請求復(fù)用推薦方案任意 Next.js 部署?React.cache()?每請求去重即可Vercel Fluid Compute?React.cache()? 實例內(nèi)存 LRU進(jìn)程內(nèi)lru-cache免外部依賴傳統(tǒng) Serverless?React.cache()?需外部存儲跨進(jìn)程共享才上 Redis需要謹(jǐn)慎的是進(jìn)程內(nèi)緩存意味著數(shù)據(jù)可能存在秒級滯后且實例回收scale-to-zero會導(dǎo)致緩存清空。因此 LRU 緩存只適合可容忍短暫陳舊的讀密集型數(shù)據(jù)不能用于強一致性的計費、訂單狀態(tài)等關(guān)鍵寫路徑。五、與 React.cache() 的組合用法分層緩存server-cache-react與server-cache-lru兩條規(guī)則并不互斥而是可以組合成兩級緩存第一級請求內(nèi)React.cache()保證同一請求內(nèi)組件樹多次調(diào)用只執(zhí)行一次避免重復(fù)的get與潛在的重復(fù)回填第二級跨請求LRU 緩存命中則直接返回未命中才執(zhí)行真實 IO 并回填同時維持ttl與max約束。import { cache } from react import { LRUCache } from lru-cache const lru new LRUCachestring, unknown({ max: 1000, ttl: 300_000 }) const _getUser async (id: string) { const hit lru.get(id) if (hit ! undefined) return hit const user await db.user.findUnique({ where: { id } }) lru.set(id, user ?? null) return user } export const getUser cache(_getUser)Polar 的 src/utils/organization.ts 展示了另一個實戰(zhàn)細(xì)節(jié)getOrganizationBySlugOrNotFound在緩存未命中時主動繞過緩存重新拉取一次用于規(guī)避新組織剛創(chuàng)建時的競態(tài)條件——這提示我們跨請求緩存必須為緩存擊穿/陳舊數(shù)據(jù)預(yù)留顯式的繞過通道。六、實踐要點速查明確緩存生命周期React.cache()止于請求LRU 止于實例Fluid Compute或需要 Redis傳統(tǒng) Serverless。實例必須模塊級單例放在模塊頂層避免每個請求重建。配置max與ttlmax防內(nèi)存膨脹ttl控制陳舊度空值結(jié)果建議顯式緩存或使用has()判斷。只在讀多寫少、可容忍短暫陳舊的數(shù)據(jù)上使用計費、訂單等強一致路徑不要放入進(jìn)程內(nèi)緩存。預(yù)留繞過通道參考 Polar 的getOrganizationBySlugOrNotFound命中失敗時允許強制刷新規(guī)避競態(tài)。參考規(guī)則原文rules/server-cache-lru.md姊妹規(guī)則請求內(nèi)去重rules/server-cache-react.md技能包總覽64 條規(guī)則 / 8 大分類README.md 與 SKILL.mdPolar 每請求緩存實戰(zhàn)src/utils/organization.ts、src/utils/order.ts、src/utils/checkout.ts【免費下載鏈接】polarPolar — A billing platform for the intelligence era項目地址: https://gitcode.com/GitHub_Trending/po/polar創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考