按请求头协商语言的站点加了缓存,译文为什么会发给用另一种语言的人
按请求头协商语言的站点加了缓存之后,译文发给用另一种语言的访客,成因几乎总是同一个:缓存键里只有地址和方法,没有语言这个影响因素。MDN 对 Vary 的定义正好说明这一点:该响应头描述的是请求信息中除方法与 URL 之外、影响响应内容的那部分;包含 Vary 头可确保响应依据所列首部字段分别被缓存,最常见的用途便是在内容协商时构造缓存键。没有这个声明,中间层会把第一位访客拿到的译文当作该地址的标准答案。
缓存键默认只看地址与方法
缓存判断「是否命中」的依据是缓存键。默认情况下它由请求方法与目标地址组成,最多再加**问路径的某些片段。
内容协商破坏了这一前提。同一个地址,因为请求头里 Accept-Language 的取值不同,服务器生成完全不同的正文。从缓存的视角看却是两次相同键的请求,于是第二次直接复用第一次的响应。
这类问题的特征很稳定:本地直连源站正常,接入缓存或加速节点后出现;清一次缓存后短时间内正常,随后又开始串;不同浏览器设置的人看到同一种语言。出现这三条中的任意两条,就应优先怀疑缓存键,而不是翻译逻辑。
协商语言时缺了什么
缺的不是翻译能力,而是「把影响因素写进响应」。
服务端在决定返回哪种语言时,读取了请求头;但响应本身并没有告诉下游「我的内容取决于那个头」。这是一次单向的信息丢失:程序知道,缓存不知道。
Vary 就是补上这条信息的机制。把它写成包含语言相关首部之后,缓存会按该字段的不同取值分别存副本,命中率会下降,但不会错发。
还有一个更彻底的取值:Vary 取星号时,意味着有其他因素影响该响应的生成,等价于该响应不可缓存。这个写法诚实但代价大,一般只在确实无法界定影响因素时使用。
需要特别注意的是取值一致性。同一 URL 的所有响应都应使用相同的 Vary 值,包括未修改响应与默认或错误响应。只在成功页面写、在错误页不写,会让缓存里同时存在两种声明的对象,行为比不写更难预期。
响应头里该怎么补
落地时按三步写。
第一步,凡是依据语言首部生成正文的地址,都补上语言相关的声明。若同时对压缩方式做协商,两者要一起列出,因为分别命中错误版本的原因是一样的。
第二步,把协商规则固定下来。语言匹配应当有明确顺序,例如精确语言优先、同语系次选、最后落到默认语言,而不是「看哪个头先来」。规则不稳定时,即使缓存正确,同一用户两次访问也可能拿到不同译文。
第三步,给可选语言留出显式入口。依赖请求头做判断,用户其实无法主动选择;提供可点击的语言切换并让选择结果落到可寻址的地址上,比纯协商更容易被缓存与抓取方正确处理。
多语言站点的两种排法
站内支持多语言站点配置与切换,并支持整页 HTML 翻译。实现上通常有两条路线,它们与缓存的关系不同。
一条是按请求头协商,同一地址返回不同语言版本。它的好处是地址简短、结构不用扩张;代价就是本文讨论的这类缓存问题,以及对抓取方而言,同一地址对应多种内容带来的歧义。
另一条是按语言分址,例如用子目录或子域名把不同语言隔开。它的地址与内容一一对应,缓存几乎不会出错,代价是站点结构变大,翻译内容的维护量随之增加。
整页翻译这类由系统生成译文的能力,还会引入第三种情况:译文与原页可能同址也可能分址,取决于实现。部署时要确认译文产物走的是哪条链路——若译文被写成静态文件放在同一地址之前,缓存与文件层的优先级会盖过协商逻辑,排查时容易看错方向。
自建站与托管环境在这点上差别明显:自己部署时可以同时控制协商规则与缓存层配置;托管场景常常只能改站点侧,动不了中间层的缓存键策略,此时更应倾向分址方案。
验证顺序
按这个顺序做,能在几分钟内定位问题。
先直连源站,用两种语言首部分别请求同一地址,确认响应确实不同,并且两条响应都带有语言影响因素的声明。再穿过缓存层重复同样的两次请求,确认各自命中自己的副本而不是同一份。最后核对未修改响应与错误响应,确保它们的声明与正常响应一致。
如果第二步出现串版本,处理方式按优先级排列:补声明、把该地址改为不可缓存、或迁移到分址方案。三者对命中率的影响依次增大,但确定性也依次增大。
常见问题
加了声明后命中率下降怎么办? 这是正确行为带来的必然代价。可以把与语言无关的资源(样式、图片)单独放址,让它们继续享受高命中。
搜索引擎抓到的是哪种语言? 抓取方的请求头通常偏向默认语言。若站点面向多语言市场,分址更可控。
Cookie 里的语言偏好能不能替代请求头? 可以参与判断,但会让缓存问题更严重:个性化凭据带来的响应差异同样需要在缓存与声明层面处理。
为什么有时刷新一下又对了? 缓存对象过期或被替换时会出现这种间歇表现,不能作为问题已解决的依据,仍应按验证顺序复核。
整页翻译生成的内容需要单独处理吗? 需要确认它与原页是否同址。同址就要走协商声明那条路;分址则要核对新地址的缓存与站点地图覆盖。