第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程序的全部。我画了张图,你看一眼就明白了。

Flink DataStream 三要素架构图 Source 数据源接入 Transformation 数据转换处理 Sink 数据输出 常见实现: Source: KafkaConsumer, Socket, File, 自定义SourceFunction Transformation: map, flatMap, filter, keyBy, window, process Sink: KafkaProducer, JDBC, Elasticsearch, 自定义SinkFunction

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能在提交前做全局优化。