—— 构建可配置、可扩展的实时标准化清洗链路
本文是「Flink + Kafka 构建实时数仓实战」专栏的第 4 篇,将围绕字段标准化这一核心问题,从业务痛点、技术架构、配置设计到完整代码工程,系统讲透标准化实践。
在真实业务中,数据往往来源于多个系统,字段命名不一致、取值不规范是常态:
字段 | 原始值 | 问题 | 影响 |
---|---|---|---|
platform | ios、iOS、苹果、android、安卓 | 命名不一致 | 报表维度混乱 |
gender | 男、male、M、1、女、F | 表达混杂 | 用户标签识别异常 |
channel | 官网、weixin、AppStore、appstore | 无归一化 | 推广渠道归因失败 |
如果不清洗、标准化,上层的指标分析、推荐、风控等全部都「靠不住」。
支持多主题、多字段标准化
配置驱动、动态字典更新
高性能:广播状态替代外部维表 Join
低耦合,适配不同业务领域(营销/风控/运营)
我们采用 Kafka → Flink 清洗标准化 → Kafka/Hudi 的结构:
Kafka 多主题(event_log / user_action 等)
↓
Flink 主流数据流
↓
广播维表流(配置映射、标准化字典广播)
↓
BroadcastProcessFunction 字段标准化处理
↓
输出至 Kafka / DWS / Hudi
flink-standardize-demo/
├── pom.xml
├── src/main/java/com/demo/
│ ├── MainJob.java # Flink Job 启动类
│ ├── model/EventLog.java # 业务字段模型
│ ├── util/SourceBuilder.java # Kafka Source 封装
│ ├── util/SinkBuilder.java # Kafka Sink 封装
│ ├── util/DictLoader.java # 字典文件加载工具
│ └── func/StandardizeFunction.java # 标准化函数(Broadcast)
└── resources/
├── dicts/
│ ├── dict_platform.json
│ ├── dict_gender.json
│ └── dict_channel.json
└── mapping-config.json # 字段-字典配置
mapping-config.json
){
"event_log": {
"mappings": {
"platform": "dict_platform",
"gender": "dict_gender",
"channel": "dict_channel"
}
},
"user_action": {
"mappings": {
"os": "dict_platform",
"sex": "dict_gender"
}
}
}
每个 Kafka 主题可定义自己要标准化的字段,以及所使用的字典。
dict_gender.json
){
"男": "1",
"male": "1",
"M": "1",
"女": "2",
"female": "2",
"F": "2"
}
更多如 dict_platform.json
、dict_channel.json
可类比定义。
MapStateDescriptor> dictStateDescriptor =
new MapStateDescriptor<>("dictState", Types.STRING, Types.MAP(Types.STRING, Types.STRING));
@Override
public void processBroadcastElement(Map> value, Context ctx, Collector out) throws Exception {
BroadcastState> dictState = ctx.getBroadcastState(dictStateDescriptor);
for (Map.Entry> entry : value.entrySet()) {
dictState.put(entry.getKey(), entry.getValue());
}
}
@Override
public void processElement(EventLog value, ReadOnlyContext ctx, Collector out) throws Exception {
ReadOnlyBroadcastState> dicts = ctx.getBroadcastState(dictStateDescriptor);
Map genderDict = dicts.get("dict_gender");
if (genderDict != null && genderDict.containsKey(value.getGender())) {
value.setGender(genderDict.get(value.getGender()));
}
out.collect(value);
}
更新方式 | 实现推荐 | 特点 |
---|---|---|
Kafka 广播 Topic | 每天定时推送字典 JSON | ✅ 推荐,自动同步 |
外部 API 拉取 | Flink 自定义 Source | 适合高频更新字典 |
本地配置轮询 | FileSource + Map 更新 | 简单、适合 PoC 测试 |
场景 | 建议 |
---|---|
多系统数据集成 | 每个系统字段映射集中管理 |
跨业务复用字段 | 字典可复用,映射配置拆分维护 |
字典频繁变动 | 推荐 Kafka 热更新或外部 API 拉取 |
性能优化 | 使用 Broadcast State 缓存,避免外部 Join |
第五篇:Flink 时态维度表 Join 与缓存机制实战
将聚焦实时数据与维度数据如何进行:
广播状态 Join
Temporal Join 实现
缓存刷新策略优化