第2章 Flink编程模型:DataStream API概览、Source/Sink/Transformation三要素、执行环境与延迟执行
好,咱们正式开始写代码了。这一章我带你搭起Flink程序的骨架。说白了,就是搞清楚一个Flink任务到底长什么样,由哪几块拼起来的。
我记得刚接触Flink时,看着官方文档里那一堆API,说实话有点懵。后来做多了才发现,核心就三件事:数据从哪来、数据怎么变、数据去哪了。这就是Source、Transformation、Sink三要素。再加上一个执行环境,整个程序就活了。
2.1 DataStream API 概览
DataStream API是Flink处理无界数据流(也就是实时流)的核心API。你想想看,Kafka里源源不断的事件、点击日志、传感器数据,这些都是无界流。DataStream API就是专门处理这种场景的。
它跟DataSet API(批处理)在Flink 1.12之后已经统一了,现在都叫DataStream API。批处理被看作“有界流”的特殊情况。我个人习惯,不管实时还是离线,都用DataStream API写,省得切换心智模型。
核心概念速览:
- DataStream:代表一个持续不断的数据流,可以是有界或无界的
- 算子(Operator):对数据流做转换操作,比如map、filter、keyBy
- 并行度(Parallelism):每个算子可以跑在多个子任务上,实现并行处理
2.2 三要素:Source / Transformation / Sink
这三个东西构成了Flink程序的全部。我画了张图,你看一眼就明白了。
2.2.1 Source:数据从哪来
Source就是数据入口。Flink内置了很多Source实现,最常用的就是Kafka Source。我在项目中做过一个用户行为实时归因系统,每天几十亿条事件从Kafka进来,Source的并行度调优直接决定了吞吐上限。
常见的Source类型:
| Source类型 | 适用场景 | 示例 |
|---|---|---|
| Kafka Source | 消息队列、事件流 | KafkaSource.builder() |
| Socket Source | 测试、调试 | socketTextStream() |
| 文件 Source | 批处理、回溯 | readTextFile() |
| 自定义 Source | 特殊数据源 | 实现 SourceFunction |
2.2.2 Transformation:数据怎么变
这是Flink最核心的部分。数据进来后,经过一系列算子转换,变成你想要的格式。我常用的算子有这些:
- map:一对一转换,比如把JSON解析成Java对象
- flatMap:一对多转换,比如一句话拆成多个单词
- filter:过滤掉不需要的数据
- keyBy:按某个字段分组,后续做窗口聚合
- window:开窗口做时间维度的聚合计算
- process:最灵活的算子,可以访问状态和时间
我的经验:能用map就别用flatMap,能用filter就别在map里写if-else。算子越简单,Flink的优化器越好做优化。我曾经接手过一个项目,一个flatMap里写了300行逻辑,跑起来慢得要命,拆成5个简单算子后性能翻倍。
2.2.3 Sink:数据去哪了
Sink是数据出口。实时特征计算中,最典型的就是把计算结果写回Kafka或者写入Redis/MySQL。我一般这么选:
- Kafka Sink:下游还有别的消费方,或者需要解耦
- JDBC Sink:结果需要持久化到数据库
- File Sink:做离线归档或者批处理
- 自定义Sink:对接公司内部中间件
2.3 执行环境(ExecutionEnvironment)
执行环境是Flink程序的起点。没有它,你连个流都创建不出来。Flink提供了三种执行环境:
| 环境类型 | 类名 | 适用场景 |
|---|---|---|
| 流处理环境 | StreamExecutionEnvironment | 实时流处理(推荐) |
| 批处理环境 | ExecutionEnvironment(已废弃) | 旧版批处理,现在统一用流环境 |
| 本地环境 | StreamExecutionEnvironment.createLocalEnvironment() | 本地开发调试 |
| 集群环境 | StreamExecutionEnvironment.getExecutionEnvironment() | 提交到集群时自动识别 |
我个人习惯,开发时用createLocalEnvironment(),部署时用getExecutionEnvironment()。这样代码不用改,环境自动适配。
2.4 程序骨架与延迟执行
一个完整的Flink程序,骨架长这样:
public class FlinkJobSkeleton {
public static void main(String[] args) throws Exception {
// 1. 创建执行环境
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 设置Source
DataStream<String> sourceStream = env
.socketTextStream("localhost", 9999);
// 3. 定义Transformation
DataStream<String> resultStream = sourceStream
.flatMap(new Tokenizer())
.keyBy(value -> value.f0)
.sum(1)
.map(value -> "Word: " + value.f0 + ", Count: " + value.f1);
// 4. 设置Sink
resultStream.print();
// 5. 启动执行
env.execute("My First Flink Job");
}
}
这里有个关键概念——延迟执行(Lazy Execution)。什么意思呢?你写env.socketTextStream()、.map()、.print()这些代码时,Flink并没有真正开始处理数据。它只是在构建一个执行计划(Execution Graph)。直到你调用了env.execute(),Flink才会把这个计划编译成JobGraph,提交给JobManager去执行。
避坑指南:我曾经犯过一个错误——在execute()之前就去读取计算结果。结果当然是空指针,因为数据根本没开始跑。记住:execute()之前,所有操作都是“画饼”,只有调用了execute(),饼才开始烙。
延迟执行的好处很明显:Flink可以在提交前对整个数据流图做优化,比如算子链合并、分区策略调整。你想想看,如果每写一行代码就执行一次,那优化空间就没了。
2.5 本章小结
这一章我们走通了Flink程序的完整骨架。Source负责接入数据,Transformation负责转换逻辑,Sink负责输出结果。执行环境是程序的起点,延迟执行机制让Flink能在提交前做全局优化。