[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-vue-request-cache-implementation":3,"article-neighbors-vue-request-cache-implementation":18,"article-related-vue-request-cache-implementation":38},{"id":4,"slug":5,"title":6,"excerpt":7,"category":8,"tags":9,"content":14,"publishedAt":15,"readingMinutes":16,"published":17},24,"vue-request-cache-implementation","Vue 全局请求缓存工程级实现、过期策略与脏数据规避","给 Vue 项目加全局缓存时踩过的坑：过期策略、脏数据、并发去重，一篇讲清楚。","工程实践",[10,11,12,13],"Vue","TypeScript","缓存","性能","做中后台系统的人大概都有这种经历：页面来回切几次，同一个接口被反复请求，服务端 QPS 蹭蹭往上涨，页面还卡。我后来给项目加了一套全局内存缓存，把这类低频变更的接口请求给压了下来。\n\n先说缓存的对象。不是所有接口都适合缓存，我只针对三类：静态配置、列表查询、基础数据查询——这些数据半天都不变一次，缓存个几分钟毫无风险。\n\n但缓存这东西，做得不好比不做更坑。最容易踩的坑有三个：永久缓存不设过期、没有刷新机制、数据变更后缓存残留。最典型的就是——服务端数据明明改了，前端还傻乎乎显示旧值，这就是\"脏数据\"滞留，线上出过这种问题的都懂。\n\n### 先想清楚缓存放哪一层\n\n做缓存之前，得先分清\"缓存分层\"这件事。浏览器有 HTTP 缓存（靠 Cache-Control、ETag 这些响应头），中间件有反向代理缓存，应用层有内存缓存，再往下还有 Redis 这类分布式缓存。对个人站和中后台系统来说，大多数场景不需要上 Redis，浏览器 HTTP 缓存 + 应用内存缓存两层就够用。\n\n我最后选的是应用内存缓存，理由很简单：接口数据在服务端生成，在内存里缓存命中率最高、延迟最低，而且实现成本最低。HTTP 缓存适合纯静态资源，动态接口还是内存缓存更可控。\n\n这里再展开讲讲 HTTP 缓存：它靠响应头控制，`Cache-Control: max-age=60` 告诉浏览器缓存一分钟，`ETag` 则是资源的版本标记，浏览器用它做\"条件请求\"——资源没变就返回 304，不重传内容。对个人站的静态资源（CSS、JS、图片）来说，配合指纹文件名做长缓存，是性价比最高的优化。但动态接口别指望它，还是得靠服务端缓存。\n\n### 我踩过的坑和解法\n\n先说说缓存 Key。用\"接口 URL + 请求参数\"组合生成，保证不同参数的请求不会互相串。参数序列化直接用 `JSON.stringify`，够用也够稳。这里有个细节：参数顺序必须固定，不然 `{a:1,b:2}` 和 `{b:2,a:1}` 会被当成两个不同的 Key，白白浪费缓存。更稳妥的做法是参数先排序再序列化。\n\n过期时间按场景分开配：纯静态接口缓存 10 分钟，列表类缓存 60 秒，实时性要求高的（支付状态、统计数据）直接不缓存。这个\"分级 TTL\"很关键——不是所有数据都用同一个过期时间，而是根据数据的新鲜度要求来定。定 TTL 的原则是：宁可短一点，不要脏数据。缓存多命中几秒，远不如\"数据准确\"重要。\n\n脏数据治理的关键在\"写操作主动清缓存\"。所有 POST、PUT、DELETE 请求执行成功后，主动清掉对应查询接口的缓存，做到更新和刷新强同步。我实现了一个 `invalidateByTag`，给每条缓存打上标签，写操作按标签批量清除，比一个个找 Key 优雅多了。这套机制在行业里叫\"写穿透\"（Write-Through）思路——更新数据的同时更新缓存，保证缓存和数据库的一致性。\n\n还有一个很多人忽略的细节：并发去重。同一个 Key 有多个请求同时进来的时候，只发一个真实请求，其余的等它返回，避免缓存击穿瞬间把后端打爆。我是在 CacheStore 里存了一个 `inflight` Promise 来做的，这个模式在 Go 和 Java 里叫\"single-flight\"，本质就是合并并发的相同请求。它还有个配套的坑：缓存击穿（缓存过期瞬间大量请求同时打到数据库），single-flight 正好能挡住第一波。\n\n### 一点提醒\n\n这套方案落地之后，页面切换、重复操作带来的无效请求明显少了，后端 QPS 的压力也降下来了。但我也得提醒一句：缓存是双刃剑，宁可少缓存，也不要让脏数据坑了业务。分级管控、写时清理，这两条别省。缓存解决的是\"读多写少\"的场景，如果你的数据写得很频繁，那缓存反而会成为一致性的负担——这种情况，该优化的是接口本身，而不是硬套缓存。\n","2026-01-25T02:00:00Z",6,true,{"prev":19,"next":27},{"id":20,"slug":21,"title":22,"excerpt":23,"category":8,"tags":24,"publishedAt":26,"readingMinutes":16},31,"virtual-list-is-not-enough","前端性能优化：虚拟列表真实适用场景与完整优化链路","列表卡顿别急着上虚拟列表，先排查网络、数据、渲染整条链路，最后才是它。",[10,25,13],"虚拟列表","2025-12-10T02:00:00Z",{"id":28,"slug":29,"title":30,"excerpt":31,"category":32,"tags":33,"publishedAt":37,"readingMinutes":16},3,"certificate-system-retrospective","去中心化数据存证系统｜业务拆解、技术选型、避坑复盘","存证系统真的需要区块链吗？我用哈希加时间戳加链式关联，给出了一个轻量答案。","项目复盘",[34,35,36],"密码学","Java","分布式系统","2026-03-20T02:00:00Z",[39,49,59],{"id":40,"slug":41,"title":42,"excerpt":43,"category":8,"tags":44,"publishedAt":48,"readingMinutes":16},1,"build-a-reliable-personal-site","个人网站工程架构与长期可维护设计","这个站从零搭起来，分享我用 Nuxt 加 Spring Boot 把个人项目做成敢一直改下去的经过。",[45,46,47],"Nuxt","Spring Boot","部署","2026-07-20T02:00:00Z",{"id":50,"slug":51,"title":52,"excerpt":53,"category":8,"tags":54,"publishedAt":58,"readingMinutes":16},32,"personal-site-feature-bloat","个人项目如何规避「功能废墟」工程问题","功能越加越多，项目却越来越难改，最后重构比重写还贵——这是我踩过的\"功能废墟\"坑。",[55,56,57],"个人网站","工程管理","维护","2026-05-15T02:00:00Z",{"id":20,"slug":21,"title":22,"excerpt":23,"category":8,"tags":60,"publishedAt":26,"readingMinutes":16},[10,25,13]]