Flink 时态维度表 Join 与缓存机制实战

[复制链接]
发表于 2025-9-19 17:45:57 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
一、引言:为什么必要时态维度表?

在实时数仓建设中,维度表是不可或缺的一环,比方:

  •         风控系统中,用户的风险等级在差别时间大概变革;
  •         营销体系中,商品的促销标签会动态调解;
  •         运营数据中,组织架构经常有调解。
时态维度表(Temporal Table),允许我们在流处理过程中,按事件时间 Join 上对应时候的维度信息,是保障数据正确性的关键。
如果直接实时查询外部数据库,轻易造成:

  •         高延迟:每条流式数据都查询一次,压力大。
  •         不稳固:外部系统故障会影响整个作业。
因此,本地缓存机制 + 定时革新 成为实时场景下维度 Join 的标准做法。

二、常见的 Flink 维度表 Join 方式

Join 方式特点场景适用直接关联(数据库查)简单、易实现;但延迟高流量极小的场景广播流 Join把维度表广播到全部 Task,内存查询小规模维度表Async I/O Join异步哀求外部系统,支持高吞吐得当中等规模,需处理超时Temporal Table JoinFlink 官方支持,按时间正确匹配数据正确性要求高自定义缓存机制本地缓存+定时革新,灵活可控大规模、高并发必备重点:本篇专注讲授「广播流 Join」和「自定义缓存」实战。

三、业务需求举例:实时用户信息增补


  •         究竟流:用户欣赏日记流(user_id、page_id、event_time)
  •         维度表:用户信息表(user_id、user_level、user_tag,有效期变革)
实时需求:

  •         实时给欣赏日记增补用户当前等级、标签
  •         支持用户信息动态更新(比方用户升等级)

四、广播维度表 Join 示例

1. 维度表读取并广播
  1. DataStream<UserInfo> userInfoStream = env .addSource(new UserInfoSource()) // 自定义 Source,定时拉取维度表 .broadcast(userInfoStateDescriptor);
复制代码
这里 userInfoStateDescriptor 是定义的广播状态。
2. 究竟流与维度表 Join
  1. SingleOutputStreamOperator<UserLogEnriched> enrichedStream = userLogStream .connect(userInfoStream) .process(new EnrichUserLogFunction());
复制代码
此中 EnrichUserLogFunction 是 BroadcastProcessFunction,实现流对广播状态的查询与增补。

五、完备代码工程模板

项目结构示例:
  1. flink-realtime-standardization/
  2. ├── pom.xml
  3. ├── src/main/java/
  4. │   ├── com.example.flink.config/
  5. │   │    └── SourceBuilder.java
  6. │   │    └── SinkBuilder.java
  7. │   ├── com.example.flink.model/
  8. │   │    └── UserLog.java
  9. │   │    └── UserInfo.java
  10. │   ├── com.example.flink.process/
  11. │   │    └── EnrichUserLogFunction.java
  12. │   ├── com.example.flink.job/
  13. │   │    └── UserLogStandardizationJob.java
  14. └── src/main/resources/
  15.     └── application.yml
复制代码
核心类阐明:

  •         SourceBuilder:同一封装 Kafka Source / 维度表 Source 创建。
  •         SinkBuilder:封装写回 Kafka / Doris 等 Sink。
  •         UserLog、UserInfo:标准化后的究竟表和维度表 POJO。
  •         EnrichUserLogFunction:处理究竟流和广播流 Join。

六、标准化字典管理样例

维度表管理,可以参考:
  1. [ { "table": "user_info", "key": "user_id", "fields": [ {"name": "user_level", "type": "STRING"}, {"name": "user_tag", "type": "STRING"} ], "update_mode": "periodic", "refresh_interval_seconds": 300 } ]
复制代码
设置支持阐明:

  •         table:维度表名
  •         key:Join 的主键
  •         fields:必要提取的字段
  •         update_mode:更新模式(periodic 定时革新 / trigger 触发革新)
  •         refresh_interval_seconds:革新频率
实际步调中,可以根据设置动态加载/革新缓存。

七、自定义缓存机制优化

缓存结构计划:
  1. Map<String, UserInfo> cache = new ConcurrentHashMap<>();
复制代码
定时革新机制示例:
  1. // 每5分钟异步拉取最新数据更新本地缓存 env.addSource(new RefreshUserInfoSource()) .addSink(new UpdateUserInfoCacheSink());
  2. RefreshUserInfoSource:拉取外部 MySQL / Redis 等数据源
  3. UpdateUserInfoCacheSink:更新本地缓存
复制代码
查询使用示例:
  1. UserInfo userInfo = cache.get(userId); if (userInfo != null) { // enrich }
复制代码

八、时态表 Join 示例(高级)

如果使用 Flink SQL,可以直接通过 Temporal Join:
  1. SELECT log.user_id, log.page_id, user.user_level FROM user_log AS log LEFT JOIN user_info FOR SYSTEM_TIME AS OF log.event_time AS user ON log.user_id = user.user_id
复制代码
留意事项:

  •         FOR SYSTEM_TIME AS OF 是关键字。
  •         维度表必要是 Versioned Table Source 支持版本控制。
  •         可基于 Hudi、Iceberg、Delta 等支持时间版本控制的存储。

九、总结与最佳实践

问题最佳实践维度表小且更新频仍广播 Join + 定时革新维度表中等规模Async I/O Join(加超时重试)维度表超大且变革频仍建议异步预加载 + 缓存分片管理对划一性要求极高使用 Temporal Join(Flink SQL 或 API)附:维度表缓存管理模块工程示例
1. 工程结构

假设挂到你的 com.example.flink.dim 包下面:
  1. com.example.flink.dim/
  2. ├── cache/
  3. │   ├── DimensionCacheManager.java
  4. │   ├── DimensionCacheLoader.java
  5. │   ├── DimensionBroadcastProcessFunction.java
  6. ├── model/
  7. │   ├── DimensionConfig.java
  8. │   ├── DimensionRecord.java
  9. └── source/
  10.     └── DimensionSourceFunction.java
复制代码

2. 重要模块讲授

(1)DimensionConfig.java 【维度表设置信息】
  1. package com.example.flink.dim.model;
  2. import java.io.Serializable;
  3. import java.util.List;
  4. public class DimensionConfig implements Serializable {
  5.     private String tableName;
  6.     private String keyField;
  7.     private List<String> fields;
  8.     private String updateMode; // periodic / trigger
  9.     private int refreshIntervalSeconds;
  10.     // getters and setters
  11. }
复制代码

(2)DimensionRecord.java 【单条维度记录】
  1. package com.example.flink.dim.model;
  2. import java.io.Serializable;
  3. import java.util.Map;
  4. public class DimensionRecord implements Serializable {
  5.     private String key;
  6.     private Map<String, Object> fieldMap; // 字段名 -> 字段值
  7.     public DimensionRecord(String key, Map<String, Object> fieldMap) {
  8.         this.key = key;
  9.         this.fieldMap = fieldMap;
  10.     }
  11.     // getters
  12. }
复制代码

(3)DimensionCacheManager.java 【本地缓存管理】
  1. package com.example.flink.dim.cache;
  2. import com.example.flink.dim.model.DimensionRecord;
  3. import java.util.concurrent.ConcurrentHashMap;
  4. import java.util.Map;
  5. public class DimensionCacheManager {
  6.     private static final Map<String, DimensionRecord> cache = new ConcurrentHashMap<>();
  7.     public static void put(String key, DimensionRecord record) {
  8.         cache.put(key, record);
  9.     }
  10.     public static DimensionRecord get(String key) {
  11.         return cache.get(key);
  12.     }
  13.     public static void updateAll(Map<String, DimensionRecord> newCache) {
  14.         cache.clear();
  15.         cache.putAll(newCache);
  16.     }
  17.     public static int size() {
  18.         return cache.size();
  19.     }
  20. }
复制代码

(4)DimensionSourceFunction.java 【维度表拉取 Source】
  1. package com.example.flink.dim.source;
  2. import com.example.flink.dim.cache.DimensionCacheManager;
  3. import com.example.flink.dim.model.DimensionRecord;
  4. import org.apache.flink.streaming.api.functions.source.RichSourceFunction;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7. public class DimensionSourceFunction extends RichSourceFunction<Map<String, DimensionRecord>> {
  8.     private volatile boolean running = true;
  9.     private final int refreshIntervalSeconds;
  10.     public DimensionSourceFunction(int refreshIntervalSeconds) {
  11.         this.refreshIntervalSeconds = refreshIntervalSeconds;
  12.     }
  13.     @Override
  14.     public void run(SourceContext<Map<String, DimensionRecord>> ctx) throws Exception {
  15.         while (running) {
  16.             // 从外部系统加载数据
  17.             Map<String, DimensionRecord> dimData = loadDimensionData();
  18.             ctx.collect(dimData);
  19.             Thread.sleep(refreshIntervalSeconds * 1000L);
  20.         }
  21.     }
  22.     @Override
  23.     public void cancel() {
  24.         running = false;
  25.     }
  26.     private Map<String, DimensionRecord> loadDimensionData() {
  27.         // 🚀 这里你可以接 MySQL / Redis / Hudi 之类的
  28.         Map<String, DimensionRecord> result = new HashMap<>();
  29.         // 示例
  30.         result.put("user_1", new DimensionRecord("user_1", Map.of("level", "VIP", "tag", "new_user")));
  31.         return result;
  32.     }
  33. }
复制代码

(5)DimensionBroadcastProcessFunction.java 【究竟流与广播流的处理器】
  1. package com.example.flink.dim.cache;
  2. import com.example.flink.dim.model.DimensionRecord;
  3. import org.apache.flink.streaming.api.functions.co.BroadcastProcessFunction;
  4. import org.apache.flink.util.Collector;
  5. import org.apache.flink.api.common.state.MapStateDescriptor;
  6. public class DimensionBroadcastProcessFunction<IN, OUT> extends BroadcastProcessFunction<IN, Map<String, DimensionRecord>, OUT> {
  7.     private final MapStateDescriptor<String, DimensionRecord> stateDescriptor;
  8.     private final EnrichmentFunction<IN, OUT> enrichmentFunction;
  9.     public DimensionBroadcastProcessFunction(MapStateDescriptor<String, DimensionRecord> stateDescriptor,
  10.                                               EnrichmentFunction<IN, OUT> enrichmentFunction) {
  11.         this.stateDescriptor = stateDescriptor;
  12.         this.enrichmentFunction = enrichmentFunction;
  13.     }
  14.     @Override
  15.     public void processElement(IN value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception {
  16.         String key = enrichmentFunction.extractKey(value);
  17.         DimensionRecord dimRecord = DimensionCacheManager.get(key);
  18.         OUT enriched = enrichmentFunction.enrich(value, dimRecord);
  19.         out.collect(enriched);
  20.     }
  21.     @Override
  22.     public void processBroadcastElement(Map<String, DimensionRecord> dimData, Context ctx, Collector<OUT> out) throws Exception {
  23.         DimensionCacheManager.updateAll(dimData);
  24.     }
  25.     public interface EnrichmentFunction<IN, OUT> {
  26.         String extractKey(IN input);
  27.         OUT enrich(IN input, DimensionRecord dimRecord);
  28.     }
  29. }
复制代码
✅ 支持自定义的 enrich 逻辑,不限制数据模子!
(6)UserLogEnrichmentFunction.java 【用于维度 enrich】
  1. package com.example.flink.dim.cache;
  2. import com.example.flink.dim.model.DimensionRecord;
  3. import com.example.flink.model.UserLog;
  4. import com.example.flink.model.UserLogEnriched;
  5. public class UserLogEnrichmentFunction implements DimensionBroadcastProcessFunction.EnrichmentFunction<UserLog, UserLogEnriched> {
  6.     @Override
  7.     public String extractKey(UserLog input) {
  8.         return "user_" + input.getUserId();
  9.     }
  10.     @Override
  11.     public UserLogEnriched enrich(UserLog input, DimensionRecord dimRecord) {
  12.         if (dimRecord == null) {
  13.             // 如果没有找到维度,直接返回原数据
  14.             return new UserLogEnriched(
  15.                 input.getUserId(),
  16.                 input.getEventType(),
  17.                 null,
  18.                 null,
  19.                 input.getTimestamp()
  20.             );
  21.         }
  22.         String level = (String) dimRecord.getFieldMap().getOrDefault("level", null);
  23.         String tag = (String) dimRecord.getFieldMap().getOrDefault("tag", null);
  24.         return new UserLogEnriched(
  25.             input.getUserId(),
  26.             input.getEventType(),
  27.             level,
  28.             tag,
  29.             input.getTimestamp()
  30.         );
  31.     }
  32. }
复制代码
UserLog.java(原始究竟流)
  1. package com.example.flink.model;
  2. import java.io.Serializable;
  3. public class UserLog implements Serializable {
  4.     private String userId;
  5.     private String eventType;
  6.     private long timestamp;
  7.     // constructor
  8.     public UserLog() {}
  9.     public UserLog(String userId, String eventType, long timestamp) {
  10.         this.userId = userId;
  11.         this.eventType = eventType;
  12.         this.timestamp = timestamp;
  13.     }
  14.     // getters and setters
  15. }
复制代码
UserLogEnriched.java(颠末 enrich 后的新模子)
  1. package com.example.flink.model;
  2. import java.io.Serializable;
  3. public class UserLogEnriched implements Serializable {
  4.     private String userId;
  5.     private String eventType;
  6.     private String userLevel;
  7.     private String userTag;
  8.     private long timestamp;
  9.     public UserLogEnriched() {}
  10.     public UserLogEnriched(String userId, String eventType, String userLevel, String userTag, long timestamp) {
  11.         this.userId = userId;
  12.         this.eventType = eventType;
  13.         this.userLevel = userLevel;
  14.         this.userTag = userTag;
  15.         this.timestamp = timestamp;
  16.     }
  17.     // getters and setters
  18. }
复制代码

3. 主步调集成示例(挂到你的 Job 中)
  1. StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
  2. // 维度流
  3. DataStream<Map<String, DimensionRecord>> dimStream = env
  4.     .addSource(new DimensionSourceFunction(300)) // 每300秒刷新
  5.     .broadcast(new MapStateDescriptor<>("dimensionBroadcastState", String.class, DimensionRecord.class));
  6. // 事实流
  7. DataStream<UserLog> userLogStream = SourceBuilder.getKafkaSource(env);
  8. // 事实流和维度表广播流 Join
  9. SingleOutputStreamOperator<UserLogEnriched> enrichedStream = userLogStream
  10.     .connect(dimStream)
  11.     .process(new DimensionBroadcastProcessFunction<>(
  12.         new MapStateDescriptor<>("dimensionBroadcastState", String.class, DimensionRecord.class),
  13.         new UserLogEnrichmentFunction() // 你自己定义具体 enrich 逻辑
  14.     ));
复制代码

✨ 团体特点


  •         模块化清晰:可以直接插到你原有 Flink 工程里。
  •         支持动态革新:支持定时拉取最新维度。
  •         高性能:本地 ConcurrentHashMap 查询,低延迟。
  •         灵活扩展:只要实现 EnrichmentFunction 就能适配各种究竟表。
记住一句话:缓存是为了抗压,时态是为了正确,选择符合的计谋,均衡延迟与正确率。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表