一、概览
scss
多流操作 DataStream API
├─ union() 同类型多流合并
├─ connect() 两条不同类型流连接
│ ├─ CoMapFunction / CoFlatMapFunction
│ ├─ CoProcessFunction
│ └─ KeyedCoProcessFunction
├─ broadcast() + connect() 规则流 / 配置流广播
│ ├─ BroadcastProcessFunction
│ └─ KeyedBroadcastProcessFunction
├─ intervalJoin() KeyedStream 双流时间区间 Join
├─ join() 窗口 Join
├─ coGroup() 窗口 CoGroup,类似全量分组关联
└─ side output 主流拆出多条旁路流,常用于告警/脏数据
二、union():同类型多流合并
适合多个来源结构完全一致的流,例如多个 Kafka topic、多个业务线的订单流。
ini
DataStream<Order> appOrders = env.fromSource(appSource, wm, "app-orders");
DataStream<Order> webOrders = env.fromSource(webSource, wm, "web-orders");
DataStream<Order> posOrders = env.fromSource(posSource, wm, "pos-orders");
DataStream<Order> allOrders = appOrders
.union(webOrders, posOrders);
特点:
- 只能合并相同数据类型的流。
- 不做去重、不做排序,只是把多条流合成一条流。
- 多流 watermark 会取各输入流 watermark 的最小值,因此某一路延迟会拖慢整体事件时间推进。
最佳实践:
- 多源合并前尽量统一 schema。
- 如果多个源可能重复生产同一事件,union() 后要基于业务主键做去重。
- 对空闲 source 配置 idleness,避免某个分区无数据导致 watermark 卡住。
scss
WatermarkStrategy<Order> wm = WatermarkStrategy
.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((e, ts) -> e.getEventTime())
.withIdleness(Duration.ofMinutes(1));
三、connect():两条不同类型流连接处理
connect()可以连接两条不同类型的流,得到ConnectedStreams,再用双输入函数处理。
CoMapFunction / CoFlatMapFunction适合简单双流转换。
typescript
DataStream<Order> orders = ...;
DataStream<UserEvent> userEvents = ...;
DataStream<String> result = orders
.connect(userEvents)
.flatMap(new CoFlatMapFunction<Order, UserEvent, String>() {
@Override
public void flatMap1(Order order, Collector<String> out) {
out.collect("order: " + order.getOrderId());
}
@Override
public void flatMap2(UserEvent event, Collector<String> out) {
out.collect("event: " + event.getUserId());
}
});
其中flatMap1() 处理第一条流,flatMap2()处理第二条流,两条流类型可以不同,但输出类型相同。
CoProcessFunction 适合需要状态、定时器、复杂逻辑的双流处理,例如订单流关联支付流,订单先到则缓存,支付先到也缓存,超时则输出未支付订单。
scss
DataStream<Order> orders = ...;
DataStream<Payment> payments = ...;
DataStream<Result> result = orders
.keyBy(Order::getOrderId)
.connect(payments.keyBy(Payment::getOrderId))
.process(new KeyedCoProcessFunction<String, Order, Payment, Result>() {
private ValueState<Order> orderState;
private ValueState<Payment> paymentState;
@Override
public void open(Configuration parameters) {
orderState = getRuntimeContext().getState(
new ValueStateDescriptor<>("orderState", Order.class));
paymentState = getRuntimeContext().getState(
new ValueStateDescriptor<>("paymentState", Payment.class));
}
@Override
public void processElement1(
Order order,
Context ctx,
Collector<Result> out) throws Exception {
Payment payment = paymentState.value();
if (payment != null) {
out.collect(Result.matched(order, payment));
paymentState.clear();
} else {
orderState.update(order);
ctx.timerService().registerEventTimeTimer(
order.getEventTime() + 15 * 60 * 1000L);
}
}
@Override
public void processElement2(
Payment payment,
Context ctx,
Collector<Result> out) throws Exception {
Order order = orderState.value();
if (order != null) {
out.collect(Result.matched(order, payment));
orderState.clear();
} else {
paymentState.update(payment);
}
}
@Override
public void onTimer(
long timestamp,
OnTimerContext ctx,
Collector<Result> out) throws Exception {
Order order = orderState.value();
if (order != null) {
out.collect(Result.timeout(order));
orderState.clear();
}
}
});
最佳实践:
- 双流关联优先使用 keyBy(...).connect(...).process(...)。
- 两条流必须按相同业务 key 分区,否则同一个订单和支付可能到不同 Task。
- 状态一定要清理,避免长期膨胀。
- 有超时语义时,优先使用事件时间定时器。
- 状态较大时开启 RocksDB StateBackend 和 TTL。
四、broadcast():广播流关联主流
适合"配置流 / 规则流 / 维表变更流"广播到所有并行实例。
典型场景:
- 风控规则实时更新。
- 活动配置实时生效。
- 黑名单、白名单、阈值规则更新。
ini
MapStateDescriptor<String, Rule> ruleStateDesc =
new MapStateDescriptor<>("rules", String.class, Rule.class);
BroadcastStream<Rule> ruleBroadcastStream = rules.broadcast(ruleStateDesc);
DataStream<Alert> alerts = events
.keyBy(Event::getUserId)
.connect(ruleBroadcastStream)
.process(new KeyedBroadcastProcessFunction<String, Event, Rule, Alert>() {
@Override
public void processElement(
Event event,
ReadOnlyContext ctx,
Collector<Alert> out) throws Exception {
ReadOnlyBroadcastState<String, Rule> rules =
ctx.getBroadcastState(ruleStateDesc);
for (Map.Entry<String, Rule> entry : rules.immutableEntries()) {
Rule rule = entry.getValue();
if (rule.match(event)) {
out.collect(new Alert(event, rule));
}
}
}
@Override
public void processBroadcastElement(
Rule rule,
Context ctx,
Collector<Alert> out) throws Exception {
BroadcastState<String, Rule> state =
ctx.getBroadcastState(ruleStateDesc);
state.put(rule.getRuleId(), rule);
}
});
最佳实践:
- 广播流数据量不能太大,因为每个并行实例都会保存一份。
- 规则要有版本号、更新时间、启停状态。
- 支持删除规则,不要只支持新增/覆盖。
- 广播状态适合小规模动态配置,不适合大维表。
- 大维表优先考虑异步 IO、外部 KV、Lookup Join 或 Table/SQL。
五、intervalJoin():双流时间区间 Join
适合两条 keyed 流按事件时间做区间关联。
例如:订单事件和支付事件,支付时间在订单后 15 分钟内即认为匹配。
sql
DataStream<OrderPayment> result = orders
.keyBy(Order::getOrderId)
.intervalJoin(payments.keyBy(Payment::getOrderId))
.between(Time.minutes(0), Time.minutes(15))
.process(new ProcessJoinFunction<Order, Payment, OrderPayment>() {
@Override
public void processElement(
Order order,
Payment payment,
Context ctx,
Collector<OrderPayment> out) {
out.collect(new OrderPayment(order, payment));
}
});
特点:
- 只能用于 KeyedStream。
- 基于事件时间。
- 会根据时间区间缓存两边数据。
- 不匹配的数据默认不会输出。
最佳实践:
- 用于明确的时间窗口关联,比如"点击后 10 分钟内下单"。
- 时间区间不要过大,否则状态会膨胀。
- 要求两条流都有正确 watermark。
- 如果需要输出未匹配数据,intervalJoin() 不够灵活,可以用 KeyedCoProcessFunction 自己实现。
六、join():窗口 Join
适合按窗口做内连接。
sql
DataStream<OrderPayment> result = orders
.join(payments)
.where(Order::getOrderId)
.equalTo(Payment::getOrderId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.apply(new JoinFunction<Order, Payment, OrderPayment>() {
@Override
public OrderPayment join(Order order, Payment payment) {
return new OrderPayment(order, payment);
}
});
特点:
- 两边在同一个窗口内且 key 相等时才会 join。
- 本质是窗口内笛卡尔匹配。
- 没有匹配的数据不会输出。
最佳实践:
- 适合批式窗口关联,例如每 5 分钟内订单和支付匹配。
- 如果关联语义是"订单后 15 分钟内支付",优先用 intervalJoin()。
- 窗口 join 对迟到数据敏感,要配置 watermark 和 allowed lateness。
七、coGroup():窗口内双流分组处理
coGroup()比join()更灵活,可以拿到窗口内两边的全部元素。
css
DataStream<Result> result = orders
.coGroup(payments)
.where(Order::getOrderId)
.equalTo(Payment::getOrderId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.apply(new CoGroupFunction<Order, Payment, Result>() {
@Override
public void coGroup(
Iterable<Order> orderGroup,
Iterable<Payment> paymentGroup,
Collector<Result> out) {
List<Order> orderList = Lists.newArrayList(orderGroup);
List<Payment> paymentList = Lists.newArrayList(paymentGroup);
if (paymentList.isEmpty()) {
for (Order order : orderList) {
out.collect(Result.unpaid(order));
}
} else {
for (Order order : orderList) {
for (Payment payment : paymentList) {
out.collect(Result.matched(order, payment));
}
}
}
}
});
适合:
- 需要处理未匹配数据。
- 需要一对多、多对多关联。
- 需要自定义窗口内关联逻辑。
八、Side Output:一条流拆出多条旁路流
虽然不是典型"多输入流",但常用于多流处理架构中,把主流拆成告警流、脏数据流、迟到数据流等。
ini
OutputTag<DirtyRecord> dirtyTag = new OutputTag<DirtyRecord>("dirty") {};
SingleOutputStreamOperator<Event> cleanStream = rawStream
.process(new ProcessFunction<RawEvent, Event>() {
@Override
public void processElement(
RawEvent value,
Context ctx,
Collector<Event> out) {
if (value.isValid()) {
out.collect(value.toEvent());
} else {
ctx.output(dirtyTag, new DirtyRecord(value));
}
}
});
DataStream<DirtyRecord> dirtyStream = cleanStream.getSideOutput(dirtyTag);
最佳实践:
- 脏数据、异常数据不要直接丢弃,优先通过 side output 输出到旁路。
- 迟到数据可以输出到独立 Kafka topic 供补偿处理。
- 主流程和异常流程解耦,便于监控和回放。
九、选型建议
| 场景 | 推荐 API |
|---|---|
| 多条同类型流合并 | union() |
| 两条不同类型流简单处理 | connect() + CoMapFunction / CoFlatMapFunction |
| 双流复杂状态关联 | connect() + KeyedCoProcessFunction |
| 主流关联动态规则流 | broadcast() + KeyedBroadcastProcessFunction |
| 两条 keyed 流按时间范围关联 | intervalJoin() |
| 两条流按窗口内 key 关联 | join() |
| 需要处理未匹配或复杂窗口分组 | coGroup() |
| 主流拆告警/脏数据/迟到数据 | side output |