一、DataStream:你在搭的"数据乐高"
DataStream 是不可变 的数据集抽象(可包含重复元素),既可以是有限 的,也可以是无界 的。与一般集合不同,你不能随意增删元素,只能通过 API 派生新的流。
按"如何分区(partitioned)"来划分,常见 4 类流:
-
Global Stream
强制单分区/单并行度;很多顺序敏感 或不支持并发的场景会用到(例如某些老旧外部系统的写入)。
-
Partition Stream (统称:分区流)
数据被切成多个分区;状态只在分区内可见。
- Keyed Partition Stream :每个 key 即一个分区 ,数据归属分区确定。
- Non-Keyed Partition Stream :每个并行度视为一个分区 ,归属分区不确定(类似轮询/随机)。
-
Broadcast Stream
同一份数据复制到每个 下游分区,典型用于规则/字典/配置下发。
重要事实:
- 一个分区只能被一个任务处理 ;一个任务可以处理多个分区。
- Keyed 流上的状态天然按 key 进行隔离与迁移,是弹性缩扩容的根基。
二、Partitioning:在不同"分区形态"间切换
有了流,还需要在不同分区形态间转换。DataStream 提供 4 种基础分区变换:
- KeyBy:按指定 key 重分区(NonKeyed → Keyed)。
- Shuffle:全量打散重分区(常用于均衡负载)。
- Global:把所有分区合并成一个(强制单并行)。
- Broadcast:把上游数据复制到下游所有分区(只能与其他输入配合使用)。
代码示例(NonKeyed → Keyed):
java
NonKeyedPartitionStream<Tuple<Integer, String>> stream = ...;
KeyedPartitionStream<Integer, String> keyed = stream.keyBy(rec -> rec.f0);
提醒 :Broadcast 不能 直接"转"成其他流,只能作为辅助输入参与下游算子。
三、ProcessFunction:唯一的"处理入口"
对 DataStream 的一切算子处理,都可归结为 ProcessFunction 。它是你定义业务逻辑(含状态/定时器)的唯一入口。
3.1 分类(按输入/输出数量)
| 类型 | 输入 | 输出 |
|---|---|---|
| OneInputStreamProcessFunction | 1 | 1 |
| TwoInputNonBroadcastStreamProcessFunction | 2 | 1 |
| TwoInputBroadcastStreamProcessFunction | 2 | 1 |
| TwoOutputStreamProcessFunction | 1 | 2 |
多输入/多输出可通过组合 多个 ProcessFunction 实现。
process(...)、connectAndProcess(...)是两个核心入口。
3.2 输入/输出兼容性(单输入)
OneInputStreamProcessFunction:
| 输入流 | 输出流 |
|---|---|
| Global | Global |
| Keyed | Keyed / NonKeyed |
| NonKeyed | NonKeyed |
| Broadcast | 不支持 |
TwoOutputStreamProcessFunction:
| 输入流 | 输出流 |
|---|---|
| Global | Global + Global |
| Keyed | Keyed + Keyed / NonKeyed + NonKeyed |
| NonKeyed | NonKeyed + NonKeyed |
| Broadcast | 不支持 |
3.3 输入/输出兼容性(双输入)
下表给出两输入之间的兼容与输出类型(❎ 不支持):
| 输出类型 | Global | Keyed | NonKeyed | Broadcast |
|---|---|---|---|---|
| Global | Global | ❎ | ❎ | ❎ |
| Keyed | ❎ | NonKeyed / Keyed | ❎ | NonKeyed / Keyed |
| NonKeyed | ❎ | ❎ | NonKeyed | NonKeyed |
| Broadcast | ❎ | NonKeyed / Keyed | NonKeyed | ❎ |
直观理解:
- Global 非常挑剔(只能和 Global 出结果)。
- Broadcast 作为输入存在感强,但不能独活(需与另一输入配合)。
- Keyed 组合最灵活,可输出 Keyed 或 NonKeyed。
四、配置处理节点:withName / withParallelism
process/connectAndProcess 的返回值既是流,也是可配置句柄,你可以链式设置名称、并行度等属性:
java
inputStream
.process(func1) // 处理 1
.withName("my-process-func") // 命名
.withParallelism(2) // 并行度
.process(func2); // 处理 2
建议统一给关键节点命名:方便 UI/日志定位与告警绑定。
五、把"积木"拼起来:三个常用套路
5.1 单输入映射 + 串行落库(Global)
java
// 1) 创建环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// 2) Source → NonKeyed
env.fromSource(someSource)
// 3) 逐条 +1
.process(new OneInputStreamProcessFunction<Integer, Integer>() {
@Override
public void processRecord(Integer x, Collector<Integer> out) throws Exception {
out.collect(x + 1);
}
})
// 4) 下游不支持并发写:强制单分区
.global()
// 5) 落库 / 打印
.toSink(someSink);
// 6) 触发
env.execute();
5.2 规则广播 + 事件主流(Broadcast + Keyed)
典型:规则/黑名单/阈值通过 Broadcast 下发,事件按 key 处理。
java
NonKeyedPartitionStream<Rule> rules = env.fromSource(ruleSource).broadcast();
KeyedPartitionStream<String, Event> events = env.fromSource(eventSource)
.keyBy(e -> e.userId);
events
.connectAndProcess(
rules,
new TwoInputBroadcastStreamProcessFunction<Event, Rule, Alert>() {
@Override
public void processElement(Event ev, Context ctx, Collector<Alert> out) {
Rule r = /* 从广播状态读取相应规则 */;
if (r.match(ev)) out.collect(toAlert(ev, r));
}
@Override
public void processBroadcastElement(Rule r, Context ctx, Collector<Alert> out) {
/* 更新广播状态中的规则 */
}
})
.withName("event-with-rules")
.withParallelism(8)
.toSink(alertSink);
记住:Broadcast 流不能单独转换,必须配合另一输入。
5.3 单输入双路输出(TwoOutput)
典型:按条件分流(如异常→告警流,正常→明细流)。
java
env.fromSource(someSource)
.process(new TwoOutputStreamProcessFunction<Event, Event, Alert>() {
@Override
public void processRecord(Event e, Collector<Event> main, Collector<Alert> side) {
if (isAnomaly(e)) side.collect(toAlert(e));
else main.collect(e);
}
})
.withName("split")
.withParallelism(4);
// 假设框架提供对两个输出的后续接法(略)
六、设计选型指南(3 步走)
-
先判定"分区形态"
- 需要按用户/订单维度关帐?→ Keyed。
- 只是并行提升吞吐?→ NonKeyed + 必要时 Shuffle。
- 下游不支持并发?→ Global。
- 动态规则/字典?→ Broadcast + 另一条输入。
-
再挑 ProcessFunction 形态
- 单流处理:
OneInputStreamProcessFunction - 双流汇合:
TwoInput*(是否含 Broadcast 取决于场景) - 需要分两路输出:
TwoOutputStreamProcessFunction
- 单流处理:
-
最后补齐配置与产线能力
withName/withParallelism、异常处理、幂等/事务、监控与告警。
七、工程化最佳实践与避坑
- 状态作用域只在分区内:跨 key 的逻辑请谨慎(必要时引入 Global 辅助或外部存储)。
- Broadcast 不能独活:只能作为双输入之一参与处理。
- Global 慎用:是"单核"开关,会成为吞吐瓶颈;只在确实需要串行时使用。
- 命名与观测 :所有关键处理节点都要
withName,方便 UI/日志/报警。 - 分流一致性:TwoOutput 的两路要各自保证语义(例如都写成功/失败时的补偿策略)。
- 外部写入 :不支持并发的 Sink →
global();支持并发但要幂等/事务保障。 - 压测先行 :对 Keyed 流关注热点 key;必要时做前缀打散或二级键拆分。
八、一页速查表(Cheat-Sheet)
分区变换:
keyBy:NonKeyed → Keyed(确定路由)shuffle:打散重分区(均衡负载)global:合并为单分区(串行)broadcast:复制到所有分区(需与另一输入连用)
ProcessFunction 选择:
- 单输入单输出:
OneInputStreamProcessFunction - 单输入双输出:
TwoOutputStreamProcessFunction - 双输入:
TwoInputNonBroadcastStreamProcessFunction/TwoInputBroadcastStreamProcessFunction
兼容规则要点:
- Global 基本只和 Global 兼容。
- Broadcast 只能作为双输入之一;输出通常是 Keyed/NonKeyed。
- Keyed 组合最灵活,产出 Keyed 或 NonKeyed。
九、结语
把 DataStream、Partitioning、ProcessFunction 三块"地基"吃透,你就具备了自下而上搭建任何实时拓扑的能力:
- 用 分区形态 把并行与一致性讲清楚;
- 用 ProcessFunction 把计算与状态讲扎实;
- 用 withName/withParallelism 把工程生产化。