返回技术文章

工程实践

Vue 全局请求缓存工程级实现、过期策略与脏数据规避

给 Vue 项目加全局缓存时踩过的坑:过期策略、脏数据、并发去重,一篇讲清楚。

做中后台系统的人大概都有这种经历:页面来回切几次,同一个接口被反复请求,服务端 QPS 蹭蹭往上涨,页面还卡。我后来给项目加了一套全局内存缓存,把这类低频变更的接口请求给压了下来。

先说缓存的对象。不是所有接口都适合缓存,我只针对三类:静态配置、列表查询、基础数据查询——这些数据半天都不变一次,缓存个几分钟毫无风险。

但缓存这东西,做得不好比不做更坑。最容易踩的坑有三个:永久缓存不设过期、没有刷新机制、数据变更后缓存残留。最典型的就是——服务端数据明明改了,前端还傻乎乎显示旧值,这就是"脏数据"滞留,线上出过这种问题的都懂。

先想清楚缓存放哪一层

做缓存之前,得先分清"缓存分层"这件事。浏览器有 HTTP 缓存(靠 Cache-Control、ETag 这些响应头),中间件有反向代理缓存,应用层有内存缓存,再往下还有 Redis 这类分布式缓存。对个人站和中后台系统来说,大多数场景不需要上 Redis,浏览器 HTTP 缓存 + 应用内存缓存两层就够用。

我最后选的是应用内存缓存,理由很简单:接口数据在服务端生成,在内存里缓存命中率最高、延迟最低,而且实现成本最低。HTTP 缓存适合纯静态资源,动态接口还是内存缓存更可控。

这里再展开讲讲 HTTP 缓存:它靠响应头控制,Cache-Control: max-age=60 告诉浏览器缓存一分钟,ETag 则是资源的版本标记,浏览器用它做"条件请求"——资源没变就返回 304,不重传内容。对个人站的静态资源(CSS、JS、图片)来说,配合指纹文件名做长缓存,是性价比最高的优化。但动态接口别指望它,还是得靠服务端缓存。

我踩过的坑和解法

先说说缓存 Key。用"接口 URL + 请求参数"组合生成,保证不同参数的请求不会互相串。参数序列化直接用 JSON.stringify,够用也够稳。这里有个细节:参数顺序必须固定,不然 {a:1,b:2} 和 {b:2,a:1} 会被当成两个不同的 Key,白白浪费缓存。更稳妥的做法是参数先排序再序列化。

过期时间按场景分开配:纯静态接口缓存 10 分钟,列表类缓存 60 秒,实时性要求高的(支付状态、统计数据)直接不缓存。这个"分级 TTL"很关键——不是所有数据都用同一个过期时间,而是根据数据的新鲜度要求来定。定 TTL 的原则是:宁可短一点,不要脏数据。缓存多命中几秒,远不如"数据准确"重要。

脏数据治理的关键在"写操作主动清缓存"。所有 POST、PUT、DELETE 请求执行成功后,主动清掉对应查询接口的缓存,做到更新和刷新强同步。我实现了一个 invalidateByTag,给每条缓存打上标签,写操作按标签批量清除,比一个个找 Key 优雅多了。这套机制在行业里叫"写穿透"(Write-Through)思路——更新数据的同时更新缓存,保证缓存和数据库的一致性。

还有一个很多人忽略的细节:并发去重。同一个 Key 有多个请求同时进来的时候,只发一个真实请求,其余的等它返回,避免缓存击穿瞬间把后端打爆。我是在 CacheStore 里存了一个 inflight Promise 来做的,这个模式在 Go 和 Java 里叫"single-flight",本质就是合并并发的相同请求。它还有个配套的坑:缓存击穿(缓存过期瞬间大量请求同时打到数据库),single-flight 正好能挡住第一波。

一点提醒

这套方案落地之后,页面切换、重复操作带来的无效请求明显少了,后端 QPS 的压力也降下来了。但我也得提醒一句:缓存是双刃剑,宁可少缓存,也不要让脏数据坑了业务。分级管控、写时清理,这两条别省。缓存解决的是"读多写少"的场景,如果你的数据写得很频繁,那缓存反而会成为一致性的负担——这种情况,该优化的是接口本身,而不是硬套缓存。

分享这篇文章

如果你在这里有了 10 分钟的顿悟,那它折叠得刚刚好。