本文目录导读:
选择全栈框架的缓存策略,本质上是在数据一致性、访问延迟、系统复杂度之间做权衡,没有“最好”的策略,只有“最适合”你业务场景的方案。
为了帮你做出决策,我把缓存策略分为三个层级来拆解,并给出不同场景下的推荐组合。
第一层:浏览器端(客户端)缓存 —— 减少网络请求
这是成本最低、效果最明显的缓存,全栈框架(如 Next.js、Nuxt.js、Remix)通常直接集成。
- 策略选择:
- 静态资源(JS/CSS/图片): 使用 Hash 文件名 + 长期强缓存(
Cache-Control: max-age=31536000),框架构建时自动生成哈希文件名,内容变了文件名就变,浏览器自动获取新的。 - 页面/API 响应:
- SSG(静态生成): 如果页面内容不常变(如博客、营销页),优先用,构建时生成 HTML,可直接设置 CDN 缓存(如 1 小时到永远)。
- ISR(增量静态生成): 如果你需要“伪动态”且内容可接受延迟更新(如新闻列表),用 ISR,设置
revalidate时间(60 秒),过期后首次请求触发后台重建。 - SWR(Stale-While-Revalidate): 如果对用户体验要求高,可以接受先展示旧数据再刷新,请求时会先返回缓存数据(Stale),后台再获取新数据更新缓存(Revalidate)。
- 静态资源(JS/CSS/图片): 使用 Hash 文件名 + 长期强缓存(
适用场景: 所有全栈框架,这是“第一道防线”。
第二层:服务端渲染缓存 —— 提升 TTFB 和 SSR 性能
对于需要 SSR(服务端渲染)或 API 数据获取的场景,缓存渲染结果或数据可以大幅降低数据库压力。
-
全栈框架内置方案:
- Next.js (App Router): 使用
unstable_cache(或 React 的cache函数),它会在服务端内存或自定义存储层(如 Redis)缓存异步函数的返回结果。 - Nuxt.js (V3): 使用
useAsyncData的transform或cache选项,或集成nuxt-multi-cache模块。 - Remix / SvelteKit: 通常依赖
Cache-Control头,由前端传给 CDN 做缓存。
- Next.js (App Router): 使用
-
通用分布式方案(推荐):
- Redis / Memcached: 对于高频读、低频写的数据(如用户信息、商品详情),使用 “旁路缓存”模式:
- 读: 先查 Redis,命中返回;不命中,查 DB,回写 Redis。
- 写: 先更新 DB,然后删除 Redis 中的老数据(延迟双删或消息队列保证最终一致)。
- CDN(边缘缓存): 将渲染好的 HTML 缓存到 CDN 节点(如 Cloudflare、Vercel Edge、Akamai),这是成本最高但效果最好的 SSR 性能提升方式,策略通常是 “Cache Everything” 配合
stale-while-revalidate。
- Redis / Memcached: 对于高频读、低频写的数据(如用户信息、商品详情),使用 “旁路缓存”模式:
适用场景: 高并发 SSR 应用、数据主要读多写少的页面。
第三层:数据库查询缓存 —— 减少数据库压力
- 策略:
- ORM 内置缓存(如 Prisma 的字段级缓存): 对单个字段或小对象做内存缓存,适用低频更新数据。
- 查询结果缓存: 将复杂 SQL 结果或聚合计算结果存入 Redis 或 Memcached。注意: 这种缓存容易导致“数据不一致地狱”,通常建议只在极端性能瓶颈时才用。
适用场景: 复杂报表、聚合查询、低频更新的热数据。
具体选择决策树
你可以根据业务特性来匹配:
| 场景 | 推荐策略组合 | 原因 | | :--- | :--- | :--- |型网站(博客、文档、营销页) | SSG + CDN 缓存(长期) | 构建时生成静态 HTML,CDN 缓存至永远,更新时重新构建。 | | 高并发动态应用(电商、论坛、SaaS) | SSR + Redis(旁路缓存)+ CDN(SWR) | 数据库压力转移到 Redis,CDN 用 SWR 策略,兼顾速度与内容新鲜度。 | | 实时性要求高(股票、聊天、协同编辑) | 不缓存 / 或仅浏览器缓存(短期) | 依靠 WebSocket 或 SSE 推送,仅对静态资源做缓存。 | | 中后台管理系统(低并发、数据敏感) | 不缓存 / 或仅客户端状态管理 | 数据频繁更新且要求绝对一致性,缓存价值低,风险高。 | | 数据及时性+性能均衡(新闻列表、排行榜) | ISR + Redis(带 TTL 的旁路)** | ISR 保证页面不垮;Redis 缓存热点数据,设置较短 TTL(如 30 秒)。 |
避坑指南(重要)
- 不要缓存所有东西: 缓存只解决“读多写少”的问题,对于写频繁的数据(如评论数、点赞数),缓存反而会引入巨大的缓存失效和一致性问题。
- 缓存穿透、击穿、雪崩:
- 穿透: 查一个不存在的数据,每次都穿透缓存打 DB,解决:布隆过滤器 或 缓存空值(
null)并设置短 TTL。 - 击穿: 一个热点 key 在缓存失效的瞬间,大量请求同时打到 DB,解决:互斥锁(Mutex) 或 永不过期 + 异步更新。
- 雪崩: 大量缓存同时过期,解决:设置不同的 随机 TTL,或使用高可用缓存集群。
- 穿透: 查一个不存在的数据,每次都穿透缓存打 DB,解决:布隆过滤器 或 缓存空值(
- 正确设置失效策略:
- TLL(生存时间): 简单粗暴,最常用,适合不需要强一致性的场景。
- 主动淘汰: 数据更新后,由业务代码主动删除或更新缓存,适合对一致性有要求的场景(如订单状态)。
- 被动淘汰(LRU/LFU): 内存不够时,淘汰最近最少使用的数据,由缓存系统自动完成。
- 避免“缓存雪崩”变体: 在微服务或全栈框架的 Serverless 场景(如 Vercel Edge Functions),函数实例的冷启动本身也是一个“缓存雪崩”问题——所有刚启动的实例同时去查数据库,解决方案是使用全局共享缓存(如 Redis、Durable Objects)而不是本地内存缓存。
总结建议
- No.1 优先级: 无脑开启浏览器端静态资源强缓存(框架默认)。
- No.2 优先级: 对非实时、热点数据,使用 Redis 旁路缓存,TTL 设置比你的预期数据更新时间短 20%(比如预期 5 分钟更新一次,你设 4 分钟 TTL)。
- No.3 优先级: 如果用了 Next.js/Nuxt,合理利用 ISR 或 SWR,它们对开发者最友好,代码侵入性最低。
- 终极建议: 先测量,再缓存。 用 APM(应用性能监控,如 Datadog、New Relic)找到真正的性能瓶颈(通常是数据库查询或 N+1 问题),而不是盲目地在每一层堆缓存。
记住一句名言:“计算机科学中只有两件难事:缓存失效和命名。” 选择策略时,优先考虑简单、可维护、可观测的方案,而不是最复杂的。
标签: 缓存策略