内容提要
文章介绍多语言方案从三级缓存改为数据库加全量内存的演进。原方案因历史代码逐条查询,一次渲染触发数百次缓存链路,性能反而变慢。新方案以数据库为唯一真相,启动时全量加载译文到内存,用xxHash64做key,运行时零查询,支持增量同步与跨语言全文检索,节省约1GB内存,适合原文万级、查询频繁、SEO敏感场景。
延伸解读
缓存失效的根源:调用模式与缓存粒度错配
文章指出,三级缓存方案上线后变慢,并非缓存技术本身的问题,而是历史代码的零散查询方式与缓存粒度不匹配。一个页面模板中可能有上百处独立的多语言调用,每次调用都完整走一遍L1→L2→L3链路。即使Redis命中率高达95%,单次网络往返约0.5ms,几百次累计仍达数百毫秒。这提醒我们,在引入缓存前,应先评估实际调用次数和查询模式,否则缓存层再多也挡不住调用次数带来的IO等待。
LRU缓存对SEO的隐性伤害
LRU的语义是“用过的才缓存”,这导致搜索引擎爬虫首次抓取页面时恰好遇到缓存未命中,渲染出的是默认语言而非目标语言,使搜索引擎索引到未翻译版本。预热虽可缓解,但全语言×全页面的组合成本可能比全量加载更高,且长尾页面永远预热不到。对于SEO敏感的多语言站点,“首次访问即返回正确语言”是刚需,LRU难以满足,这成为推动全量内存方案的重要因素。
用xxHash64做key:内存与兼容性的平衡
新方案以原文的xxHash64作为内存map的key,而非直接使用原文字符串。这样做既兼容了历史代码中“原文即key”的调用方式,又大幅节省内存:每条原文仅占8字节,且近20种语言共享同一份hash key,无需为每种语言独立维护字符串key。文章提到,相比旧方案节省约1GB内存。代价是原文修改一个标点就会导致hash变化、译文失效,需业务侧显式调用更新接口来同步变更。
全量内存方案的适用边界与取舍
文章明确列出了该方案的适用场景:原文万级到十万级、译文可全量放入内存、翻译为离线任务、SEO敏感、需要跨语言全文检索,以及历史代码难以重构为批量接口。不适合的场景包括:原文百万级以上、多实例独立部署且增量同步复杂、要求翻译后毫秒级生效、有严格删除语义。作者强调,这是用内存换零延迟、用接受内存脏数据换简化、用离线翻译加增量同步换实时性的主动取舍,并非通用银弹。
Q&A
为什么原来的三级缓存方案上线后反而变慢了?
因为历史代码中多语言调用是零散的,一个页面模板可能有上百处 T("文案", lang) 调用,每次渲染触发几百次 Get,每次都走 L1→L2→L3 链路。即使 Redis 命中率 95%,仍有若干次网络往返,单次 0.5ms 累计几百毫秒,导致页面加载慢了好几倍。瓶颈是调用次数本身,而不是缓存技术。
新方案为什么用 xxHash64 做 key 而不是直接用原文?
直接用原文做 key 内存开销大,原文可能长达几万字符,几万条原文乘以近 20 种语言,字符串本身占用可观。用 uint64 的 xxHash64 做 key,每条原文仅 8 字节,map entry 约 16 字节,且所有语言共享同一份 key。xxHash64 速度是 MD5 的十几倍,输出 8 字节,碰撞概率在万级规模可忽略。
全量内存方案如何保证翻译更新后能生效?
翻译是离线任务,跑在独立 CLI 进程里。翻译完成后,CLI 主动向服务的 HTTP 接口发请求,服务收到后按 updated_at 增量拉取译文变更并更新内存。删除不处理,内存里旧数据保留,重启后自然清理。没有定时轮询。
新方案在 SEO 方面比 LRU 缓存有什么优势?
LRU 是“用过的才缓存”,搜索引擎爬虫首次抓取时正好未命中,会渲染默认语言而非目标语言,导致索引到未翻译版本。预热成本高且长尾页面无法覆盖。新方案启动时全量加载所有语言到内存,首次访问就能返回正确语言,满足 SEO 首访即正确语言的刚需。
这套全量内存方案适合和不适合哪些场景?
适合:原文万级到十万级、译文可全量放内存、翻译离线、SEO 敏感、需要跨语言全文检索、历史代码按“原文即 key”调用难以重构。不适合:原文百万级以上、多实例独立内存部署、需要翻译后毫秒级生效、有严格删除语义。
新方案相比旧方案节省了多少内存?主要原因是什么?
节省约 1GB 内存,内存使用率降低 15%。主要原因是:用 hash key 替代字符串 key,每条至少省几十字节;近 20 种语言共享同一份 text_hash,无需每种语言独立维护 key;去掉了 LRU、Redis 客户端库、序列化中间产物等额外缓存层。