在我上一篇关于多语言的文章中,我分享过基于 Key-Value(KV) 模式的实现。它通过 (entity_id, field_key, field_value) 实现了“零 Schema 改动”的翻译支持。
但在万级 TPS、大实体、多品类的资产管理场景下,KV 模式遇到了阿喀琉斯之踵:
新的架构共识: 当业务实体的字段逐渐标准化,我们需要从“行扩展”回归到“表扩展”。通过**一主多从(i18n扩展表)**的结构,换取极致的查询性能与链路的统一。
在确定了放弃 KV 垂直模式后,如何在保证主数据简洁性的同时,支撑起结构化、高性能的多语言查询?
我的答案是:“一主多副本 + 维度垂直拆分”。
我建立了主从分离的存储模型。主表(asset_item)作为单实体的“真实来源(Source of Truth)”,只存储与语言无关的物理事实;而所有涉及文本描述的内容,全部外排至扩展表。
SQL
-- 1. 资产主表:极致精简,存储物理事实
CREATE TABLE asset_item (
id BIGINT PRIMARY KEY,
category VARCHAR(20) NOT NULL, -- 手表, 车等
source_type INT DEFAULT 0, -- 官方/非官方
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 仅保留主语言(默认英文)的影子字段,用于 Resolver 缺失时的极速回退
default_name TEXT NOT NULL
);
-- 2. 基础多语言表:存储跨品类通用字段
CREATE TABLE asset_item_i18n (
asset_id BIGINT NOT NULL,
language_locale VARCHAR(10) NOT NULL, -- zh-CN, zh-TW, en
name TEXT,
description TEXT,
analysis_text TEXT, -- 专家分析
PRIMARY KEY (asset_id, language_locale),
CONSTRAINT fk_asset_i18n FOREIGN KEY (asset_id) REFERENCES asset_item(id) ON DELETE CASCADE
);业务实体管理系统中,不同品类(Category)的规格字段差异极大。如果全部塞进一张 asset_item_i18n,会导致该表拥有几百个字段且极其稀疏。
采取了**“维度扩展表”**策略。例如,针对汽车和雪茄品类:
SQL
-- 汽车品类专属翻译
CREATE TABLE asset_car_i18n (
asset_id BIGINT NOT NULL,
language_locale VARCHAR(10) NOT NULL,
engine_type TEXT, -- 发动机配置翻译
exterior_color TEXT, -- 外观颜色翻译
PRIMARY KEY (asset_id, language_locale)
);
在进行物理设计时,针对高并发场景做了如下深度优化:
(asset_id, language_locale) 设为复合主键。在物理存储上,同一个资产的不同语言版本会紧凑地排列在一起。这种局部性原理使得数据库在执行字段回退(查询某资产的所有语言版本)时,只需要一次 IO 预读即可将所有候选翻译数据载入 Buffer Pool,性能远超 KV 模式下的多行扫描。default_name。当 request_language == 'en' 时,Resolver 会直接读取主表字段。对于占比 40% 以上的英文请求,成功实现了零 I18n 表查询开销,极大缓解了翻译中台的吞吐压力。snake_case 到 camelCase 转换规则),这能让你后续通过 Java 反射或元编程极大简化 Resolver 的代码量。领域层的核心挑战在于:当数据库中的翻译数据存在“缺口”时,如何保证输出结果的连续性与可用性?传统的重写方案通常采用“整对象回退”,即如果缺失繁体版记录,则直接展示全量英文。这种处理方式过于粗糙,会导致用户体验产生剧烈的断层感。
本次架构改造引入了**字段级级联回退(Field-level Cascading Fallback)**机制,将其作为多语言中台的决策中枢。
EntityI18nResolverService 承担了从原始数据到本地化 DTO 的“最后一步装配”职责。其核心逻辑不再是简单的字段映射,而是一套基于优先级链路的探测算法。
该服务通过**回退链(Fallback Chain)**定义不同语言环境下的寻址优先级。例如:
zh-HK $\rightarrow$ zh-CN $\rightarrow$ 主表默认值(en)。zh-CN $\rightarrow$ zh-HK $\rightarrow$ 主表默认值(en)。这种设计的精妙之处在于:回退动作发生在字段级别。如果一个资产的“名称”有繁体翻译但“描述”只有简体,系统最终会拼装出“繁体名称 + 简体分析”的组合,最大限度地保留了本地化信息的覆盖率。
由于不同资产的规格字段(Spec)完全不同,Resolver 内部采用了策略模式(Strategy Pattern)。主解析服务负责调度,具体的品类处理器(Handler)负责执行特定的回退逻辑。
Java
/**
* 字段级回退的核心逻辑实现示例
*/
@Service
public class EntityI18nResolverService {
// 预定义回退链路配置
private static final Map<String, List<String>> FALLBACK_MAP = Map.of(
"zh-TW", List.of("zh-TW", "zh-CN"),
"zh-CN", List.of("zh-CN", "zh-TW")
);
public void resolve(AssetCardDto dto, String lang) {
// 1. 语言归一化与链路获取
List<String> chain = FALLBACK_MAP.getOrDefault(lang, Collections.emptyList());
// 2. 批量拉取所有候选语言的翻译数据(避免在循环中查库)
// 关键点:一次性取出所有语种副本,在内存中计算回退
Map<String, AssetItemI18n> dataMap = i18nRepository.findAllByAssetIdAsMap(dto.getId());
// 3. 执行字段级回退探测
dto.setName(pickField(chain, dataMap, AssetItemI18n::getName, dto.getDefaultName()));
dto.setDesc(pickField(chain, dataMap, AssetItemI18n::getDesc, dto.getDefaultDesc()));
// 4. 委派给品类特定的处理器(如汽车、雪茄等)处理 Spec 字段
categoryHandlers.stream()
.filter(h -> h.supports(dto.getCategory()))
.findFirst()
.ifPresent(h -> h.resolveSpec(dto, dataMap, chain));
}
/**
* 回退探测器:按优先级链寻找第一个非空值
*/
private String pickField(List<String> chain, Map<String, AssetItemI18n> data,
Function<AssetItemI18n, String> getter, String fallback) {
for (String locale : chain) {
AssetItemI18n record = data.get(locale);
if (record != null && StringUtils.hasText(getter.apply(record))) {
return getter.apply(record);
}
}
return fallback; // 最终兜底:主表原始数据
}
}在实现字段级回退时,存在一个典型的技术权衡:
pickField 运算。这种做法将复杂的业务决策从 SQL 逻辑(如极其繁琐的 COALESCE 或 LEFT JOIN)中剥离出来,利用应用服务器的 CPU 换取了宝贵的数据库 IO 性能。
对于核心字段(如 name),主表保留了一份“影子字段(Shadow Field)”作为全局兜底。这保证了即使 i18n 系统因为极端原因不可用,系统依然能够输出最基本的英文名称,而不是展示意义不明的 ID 或空白卡片,确保了系统在高并发场景下的防御性架构能力。
多语言架构最常见的失效场景并非发生在“翻译缺失”,而是发生在**“参数丢失”**。在一个复杂的微服务或分布式架构中,language 参数如果仅存在于 API 入口,极易在异步调用、消息队列传输或历史数据回放过程中丢失,导致用户看到的界面在不同语种间反复“跳变”。
本次改造的核心目标是实现语言状态的全链路隐形传播,确保“请求语言”与“返回语言”在任何时空维度下均保持一致。
为了避免在每一个 Service 方法中显式传递 String lang 参数(这会产生严重的参数污染),架构上引入了基于 ThreadLocal 的上下文拦截器。
SupportedLanguage 枚举对入参进行标准化校验,随后将其存入 LanguageContextHolder。@Async)或线程池调用,使用 TransmittableThreadLocal 解决父子线程上下文丢失问题,确保 Resolver 在任何执行环境下都能感知到当前所需的标准 Locale。在资产管理系统中,一个极具挑战的场景是历史会话回放(Chat/Asset History Rehydration)。当用户查看三个月前的资产卡片时,系统通常面临两种选择:
asset_id。回放时根据当前用户的请求语言,实时调用多语言中台进行填充。本次改造采用了动态水合方案。通过将 language 参数透传至持久化层的 convertAssetItemToSimplifiedCardDto 统一入口,系统能够在回放历史消息时,实时将存储的资产 ID 转化为符合当前用户语种偏好的动态卡片。这保证了用户即使在切换系统语言后,回看历史记录依然能获得无缝的本地化体验。
全链路透传的本质是将多语言属性从“业务参数”降级为“基础设施参数”。通过在底层的 PersistenceService 实现语言注入的统一收口,i18n 从一个“需要额外关注的功能”变成了“系统默认具备的属性”。这种设计大幅降低了业务开发者的心智负担:他们只需关注业务逻辑,而输出的每一张卡片,天然就是具备全球化能力的。
在完成从“碎块化 KV”到“中台化链路”的改造后,我们需要从工程实践的角度出发,客观评估这套架构带来的收益以及在极端场景下的权衡(Trade-offs)。
PersistenceService 转换层实现了统一收口,普通业务开发者在编写逻辑时不再需要关注 language 参数。i18n 能力成为了系统底座的一部分,实现了“开发无感知,产出天然本地化”。asset_item_i18n 进行批量翻译质量审计、热词补全,而无需担心误触或锁定主业务流程。架构设计本质上是平衡的艺术。在追求标准化和一致性的同时,我们也面临着新的挑战:
asset_id 为维度缓存该实体的所有语种 Map,而非按请求语言缓存 DTO。本次重构通过“语言标准化 + EntityI18nResolver 集中决策 + 全链路隐形透传 + 维度扩展存储”的设计,将多语言能力从分散的补丁升级为生产级的中台链路。
总结建议:
架构不是一成不变的,它是业务规模与技术边界博弈后的产物。希望本次实践能为正在经历全球化架构转型的同行提供一份可落地的参考。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。