文章目录
- 前言
- [一、DataStream API是什么?](#一、DataStream API是什么?)
- [二、执行环境(Execution Environment)](#二、执行环境(Execution Environment))
- 三、转换算子
-
- (一)定义
- (二)种类
-
- [1.基本转换算子(map/ filter/ flatMap)](#1.基本转换算子(map/ filter/ flatMap))
- [2.聚合算子(map/ filter/ flatMap)](#2.聚合算子(map/ filter/ flatMap))
- 3.用户自定义函数(UDF)
- [4.物理分区算子(Physical Partitioning)](#4.物理分区算子(Physical Partitioning))
- (三)分流和合流
- 四、输出算子(Sink)
-
- (一)连接到外部系统
-
- [1.旧方法(flink 1.12.0版本以前)](#1.旧方法(flink 1.12.0版本以前))
- [2.新方法(flink 1.12.0版本后进行重构)](#2.新方法(flink 1.12.0版本后进行重构))
- (二)输出到文件
- (三)输出到Kafka
-
- [1.添加Kafka 连接器依赖](#1.添加Kafka 连接器依赖)
- 2.启动Kafka集群
- (四)输出到MySQL(JDBC)
- (五)自定义Sink输出
- 总结
前言
在大数据实时处理领域,Apache Flink 凭借其卓越的流处理性能和精确的状态管理,已成为事实上的标准。而 DataStream API 正是 Flink 面向数据流应用的核心编程接口,它提供了丰富的算子、灵活的窗口机制以及端到端的一致性保障,让开发者能够以声明式的方式构建复杂的数据处理流水线。本文旨在系统梳理 DataStream API 的基础知识体系,从执行环境的配置、源算子(Source)的读取,到各类转换算子的使用,再到物理分区分流合流以及最终的结果输出(Sink),力求为初学者或希望巩固基础的开发者提供一份清晰、实用的学习指南。
一、DataStream API是什么?
DataStream API是Flink的核心层API。一个Flink程序,其实就是对DataStream的各种转换。具体来说,代码基本上都由以下几部分构成:

二、执行环境(Execution Environment)
Flink程序可以在各种上下文环境中运行:我们可以在本地JVM中执行程序,也可以提交到远程集群上运行。
不同的环境,代码的提交运行的过程会有所不同。这就要求我们在提交作业执行计算时,首先必须获取当前Flink的运行环境,从而建立起与Flink框架之间的联系。
- 1.创建执行环境
- 2.执行模式(Execution Mode)
- 3.触发程序执行
(一)创建执行环境
我们要获取的执行环境,是StreamExecutionEnvironment类的对象,这是所有Flink程序的基础。在代码中创建执行环境的方式,就是调用这个类的静态方法,具体有以下三种。
- 1.getExecutionEnvironment(重点)
它会根据当前运行的上下文 直接得到正确的结果:如果程序是独立运行的,就返回一个本地执行环境;如果是创建了jar包,然后从命令行调用它并提交到集群执行,那么就返回集群的执行环境。
也就是说,这个方法会根据当前运行的方式,自行决定该返回什么样的运行环境。这种方式,用起来简单高效,是最常用的一种创建执行环境的方式。
java
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
- 2.createLocalEnvironment(了解)
这个方法返回一个本地执行环境。可以在调用时传入一个参数,指定默认的并行度;如果不传入,则默认并行度就是本地的CPU核心数。
java
StreamExecutionEnvironment localEnv = StreamExecutionEnvironment.createLocalEnvironment();
- 3.createRemoteEnvironment(了解)
这个方法返回集群执行环境。需要在调用时指定JobManager的主机名和端口号,并指定要在集群中运行的Jar包。
java
StreamExecutionEnvironment remoteEnv = StreamExecutionEnvironment
.createRemoteEnvironment(
"host", // JobManager主机名
1234, // JobManager进程端口号
"path/to/jarFile.jar" // 提交给JobManager的JAR包
);
(二)执行模式(Execution Mode)
从Flink 1.12开始,官方推荐的做法是直接使用DataStream API,在提交任务时通过将执行模式设为BATCH来进行批处理。不建议使用DataSet API。
DataStream API执行模式包括:流执行模式、批执行模式和自动模式。
1.流执行模式(Streaming)
这是DataStream API最经典的模式,一般用于需要持续实时处理的无界数据流 。默认情况下,程序使用的就是Streaming执行模式。
2.批执行模式(Batch)
专门用于批处理的执行模式(有界流)。批执行模式的使用主要有两种方式:
(1)命令行配置
实际生产环境常用于命令行,不灵活
powershell
bin/flink run -Dexecution.runtime-mode=BATCH ...
(2)代码配置
实际生产环境不会在代码改,不灵活
java
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
3.自动模式(AutoMatic)
在这种模式下,将由程序根据输入数据源是否有界,来自动选择执行模式。
(三)触发程序执行
-
需要注意的是,写完输出(sink)操作并不代表程序已经结束。因为当main()方法被调用时,其实只是定义了作业的每个执行操作,然后添加到数据流图中;这时并没有真正处理数据------因为数据可能还没来。
-
Flink是由事件驱动的,只有等到数据到来,才会触发真正的计算,这也被称为**"延迟执行"或"懒执行"。**
所以我们需要显式地调用执行环境的execute()方法,来触发程序执行。execute()方法将一直等待作业完成,然后返回一个执行结果(JobExecutionResult)。
java
env.execute();
(四)源算子
1.概念
Flink可以从各种来源获取数据,然后构建DataStream进行转换处理。
一般将数据的输入来源称为数据源(data source),而读取数据的算子就是源算子(source operator)。所以,source就是我们整个处理程序的输入端。

- 在Flink1.12以前 ,旧的添加source的方式,是调用执行环境的
addSource()方法:
java
DataStream<String> stream = env.addSource(...);
- 方法传入的参数是一个"源函数"(source function),需要实现
SourceFunction接口。 - 从Flink1.12开始,主要使用流批统一的新Source架构:
DataStreamSource<String> stream = env.fromSource(...)
Flink直接提供了很多预实现的接口,此外还有很多外部连接工具也帮我们实现了对应的Source,通常情况下足以应对我们的实际需求。
三、转换算子
(一)定义
数据源读入数据之后,我们就可以使用各种转换算子,将一个或多个DataStream转换为新的DataStream。

(二)种类
- 基本转换算子(map/ filter/ flatMap)
- 聚合算子(Aggregation)
- 用户自定义函数(UDF)
- 物理分区算子(Physical Partitioning)
- 分流
- 基本合流操作
1.基本转换算子(map/ filter/ flatMap)
(1)映射(map)
-
map主要用于将数据流中的数据进行转换,形成新的数据流。简单来说,就是一个一一映射 ,消费一个元素就产出一个元素。
-
只需要基于DataStream调用map()方法就可以进行转换处理。方法需要传入的参数是接口MapFunction的实现;返回值类型还是DataStream,不过泛型(流中的元素类型)可能改变。

实现方法:1.匿名内部类 2.lambda表达式 3.定义一类实现方法
java
// 方式一:匿名内部类
SingleOutputStreamOperator<String> map = sensorDS.map(new MapFunction<WaterSensor, String>() {
@Override
public String map(WaterSensor waterSensor) throws Exception {
return waterSensor.getId();
}
});
java
// 方式二:lambda表达式
SingleOutputStreamOperator<String> map1 = sensorDS.map(
(WaterSensor waterSensor) -> {return waterSensor.getId();}
);
java
// 3.方式三:定义一类实现mapfunction
public class MapDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<WaterSensor> sensorDS = env.fromElements(
new WaterSensor("sensor_1", 1L, 10),
new WaterSensor("sensor_2", 2L, 20),
new WaterSensor("sensor_3", 3L, 30)
);
SingleOutputStreamOperator<String> map2 = sensorDS.map(new MyMapFunction());
map.print();
env.execute();
}
public static class MyMapFunction implements MapFunction<WaterSensor, String>{
@Override
public String map(WaterSensor waterSensor) throws Exception {
return waterSensor.getId();
}
}
}
(2)扁平映射(flatMap)
-
flatMap操作又称为扁平映射,主要是将数据流中的整体(一般是集合类型)拆分成一个一个的个体使用。消费一个元素,可以产生0到多个元素。flatMap可以认为是"扁平化"(flatten)和"映射"(map)两步操作的结合 ,也就是先按照某种规则对数据进行打散拆分,再对拆分后的元素做转换处理。
-
同map一样,flatMap也可以使用Lambda表达式或者FlatMapFunction接口实现类的方式来进行传参,返回值类型取决于所传参数的具体逻辑,可以与原数据流相同,也可以不同。

(3)过滤(filter)
-
filter转换操作,顾名思义是对数据流执行一个过滤,通过一个布尔条件表达式设置过滤条件 ,对于每一个流内元素进行判断,若为true则元素正常输出,若为false则元素被过滤掉。
-
进行filter转换之后的新数据流的数据类型与原数据流是相同的 。filter转换需要传入的参数需要实现FilterFunction接口
只有函数式接口,才能实现lambda表达式,而FilterFunction内要实现filter()方法,就相当于一个返回布尔类型的条件表达式 。

2.聚合算子(map/ filter/ flatMap)
聚合分子一般可以分为如下三类:
- 1.按键分区(keyBy)
- 2.简单聚合(sum/min/max/minBy/maxBy)
- 3.归约聚合(reduce)
(1)按键分区(keyby)
- 对于Flink而言,DataStream是没有直接进行聚合的API的。因为我们对海量数据做聚合肯定要进行分区并行处理,这样才能提高效率。所以在Flink中,要做聚合,需要先进行分区;这个操作就是通过keyBy来完成的。
- keyBy是聚合前必须要用到的一个算子。keyBy通过指定键(key),可以将一条流从逻辑上划分成不同的分区(partitions)。这里所说的分区,其实就是并行处理的子任务。
- 基于不同的key,流中的数据将被分配到不同的分区中去;这样一来,所有具有相同的key的数据,都将被发往同一个分区。

(2)简单聚合(keyby)
有了按键分区的数据流KeyedStream,我们就可以基于它进行聚合操作了。Flink为我们内置实现了一些最基本、最简单的聚合API,主要有以下几种:
sum():在输入流上,对指定的字段做叠加求和的操作。min():在输入流上,对指定的字段求最小值。max():在输入流上,对指定的字段求最大值。minBy():与min()类似,在输入流上针对指定字段求最小值。不同的是,min()只计算指定字段的最小值,其他字段会保留最初第一个数据的值;而minBy()则会返回包含字段最小值的整条数据。maxBy():与max()类似,在输入流上针对指定字段求最大值。两者区别与min()/minBy()完全一致。
(3)归约聚合(reduce)
- reduce可以对已有的数据进行归约处理,把每一个新输入的数据和当前已经归约出来的值,再做一个聚合计算。
- reduce操作也会将KeyedStream转换为DataStream。它不会改变流的元素数据类型,所以输出类型和输入类型是一样的。
- 调用KeyedStream的reduce方法时,需要传入一个参数,实现ReduceFunction接口。接口在源码中的定义如下:
java
public interface ReduceFunction<T> extends Function, Serializable {
T reduce(T value1, T value2) throws Exception;
}

ReduceFunction接口里需要实现reduce()方法,这个方法接收两个输入事件,经过转换处理之后输出一个相同类型的事件。在流处理的底层实现过程中,实际上是将中间"合并的结果"作为任务的一个状态保存起来的;之后每来一个新的数据,就和之前的聚合状态进一步做归约。- 我们可以单独定义一个函数类实现
ReduceFunction接口,也可以直接传入一个匿名类。当然,同样也可以通过传入Lambda表达式实现类似的功能。 - reduce同简单聚合算子一样,也要针对每一个key保存状态。因为状态不会清空,所以我们需要将reduce算子作用在一个有限key的流上。
3.用户自定义函数(UDF)
用户自定义函数(user-defined function,UDF),即用户可以根据自身需求,重新实现算子的逻辑。- 用户自定义函数分为:函数类、匿名函数、富函数类
4.物理分区算子(Physical Partitioning)
- 常见的物理分区策略有:
- 随机分配(Random shuffle)
- 轮询分配(Round-Robin)
- 重缩放(Rescale)
- 广播(Broadcast)
(1)随机分区(shuffle)
- 最简单的 重分区方式就是直接**"洗牌"**。通过调用
DataStream的.shuffle()方法,将数据随机地分配到下游算子的并行任务中去。 - 随机分区服从
均匀分布(uniform distribution),所以可以把流中的数据随机打乱,均匀地传递到下游任务分区。因为是完全随机 的,所以对于同样的输入数据, 每次执行得到的结果也不会相同。 - 经过随机分区之后,得到的依然是一个DataStream。

(2)轮询分区(Round-Robin)
轮询,简单来说就是"发牌" ,按照先后顺序 将数据做依次分发。通过调用DataStream的.rebalance()方法,就可以实现轮询重分区。rebalance()使用的是Round-Robin负载均衡算法,可以将输入流数据平均分配到下游的并行任务中去。

(3)重缩放轮区
重缩放分区和轮询分区非常相似。当调用rescale()方法时,其实底层也是使用Round-Robin算法进行轮询,但是只会将数据轮询发送到下游并行任务的一部分中。rescale的做法是分成小团体,发牌人只给自己团体内的所有人轮流发牌。

(4)广播(broadcast)
这种方式其实不应该叫做"重分区",因为经过广播之后,数据会在不同的分区都保留一份 ,可能进行重复处理。可以通过调用DataStream的broadcast()方法,将输入数据复制并发送到下游算子的所有并行任务中去。
(5)全局分区(global)
全局分区也是一种特殊的分区方式。 这种做法非常极端,通过调用.global()方法,会将所有的输入流数据都发送到下游算子的第一个并行子任务中去。这就相当于强行让下游任务并行度变成了1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力。
(6)自定义分区(custom)
当Flink提供的所有分区策略都不能满足用户的需求时,我们可以通过使用partitionCustom()方法来自定义分区策略。
(三)分流和合流
1.分流
所谓"分流",就是将一条数据流拆分成完全独立的两条、甚至多条流。也就是基于一个DataStream,定义一些筛选条件,将符合条件的数据拣选出来放到对应的流里。

- 使用侧输出流 :简单来说,只需要调用上下文
ctx的.output()方法,就可以输出任意类型的数据了。而侧输出流的标记和提取,都离不开一个"输出标签"(OutputTag),指定了侧输出流的id和类型。
2.合流
四、输出算子(Sink)
- Flink作为数据处理框架,最终还是要把计算处理的结果写入外部存储,为外部应用提供支持。一般有如下四种类型:

- 输出到文件
- 输出到Kafka
- 输出到MySQL(JDBC)
- 自定义Sink输出
(一)连接到外部系统
1.旧方法(flink 1.12.0版本以前)
- Flink的DataStream API专门提供了向外部写入数据的方法:addSink。与addSource类似,addSink方法对应着一个"Sink"算子,主要就是用来实现与外部系统连接、并将数据提交写入的;Flink程序中所有对外的输出操作,一般都是利用Sink算子完成的。
- 注:Flink1.12以前,Sink算子的创建是通过调用DataStream的.addSink()方法实现的。
java
stream.addSink(new SinkFunction(...));
2.新方法(flink 1.12.0版本后进行重构)
- Flink1.12开始,同样重构了Sink架构
cpp
stream.sinkTo(...)
- 当然,Sink多数情况下同样并不需要我们自己实现。之前我们一直在使用的print方法其实就是一种Sink,它表示将数据流写入标准控制台打印输出。Flink官方为我们提供了一部分的框架的Sink连接器。如下图所示,列出了Flink官方目前支持的第三方系统连接器

- 除Flink官方之外,Apache Bahir框架,也实现了一些其他第三方系统与Flink的连接器。

(二)输出到文件
-
Flink专门提供了一个流式文件系统的连接器:FileSink,为批处理和流处理提供了一个统一的Sink,它可以将分区文件写入Flink支持的文件系统。
-
FileSink支持行编码(Row-encoded)和批量编码(Bulk-encoded)格式。这两种不同的方式都有各自的构建器(builder),可以直接调用FileSink的静态方法:
-
行编码: FileSink.forRowFormat(basePath,rowEncoder)
-
批量编码: FileSink.forBulkFormat(basePath,bulkWriterFactory)
(三)输出到Kafka
1.添加Kafka 连接器依赖
由于我们已经测试过从Kafka数据源读取数据,连接器相关依赖已经引入,这里就不重复介绍了。
2.启动Kafka集群
(四)输出到MySQL(JDBC)
1.添加依赖
(1)添加MySQL驱动:
java
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
(2)添加依赖
java
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc</artifactId>
<version>1.17-SNAPSHOT</version>
</dependency>
2.启动MySQL,在test库下建表ws
java
mysql>
CREATE TABLE `ws` (
`id` varchar(100) NOT NULL,
`ts` bigint(20) DEFAULT NULL,
`vc` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
(五)自定义Sink输出
- 如果我们想将数据存储到我们自己的存储设备中,而Flink并没有提供可以直接使用的连接器,就只能自定义Sink进行输出了。与Source类似,Flink为我们提供了通用的
SinkFunction接口和对应的RichSinkDunction抽象类,只要实现它,通过简单地调用DataStream的.addSink()方法就可以自定义写入任何外部存储。
java
stream.addSink(new MySinkFunction<String>());
- 在实现
SinkFunction的时候,需要重写的一个关键方法invoke(),在这个方法中我们就可以实现将流里的数据发送出去的逻辑。 - 这种方式比较通用,对于任何外部存储系统都有效;不过自定义Sink想要实现状态一致性并不容易,所以一般只在没有其它选择时使用。实际项目中用到的外部连接器Flink官方基本都已实现,而且在不断地扩充,因此自定义的场景并不常见。
总结
本文全面介绍了 Flink DataStream API 的核心组成部分。我们从执行环境开始,了解了如何通过 getExecutionEnvironment() 获取上下文环境,以及批流一体的执行模式(Streaming / Batch / AutoMatic)。接着,我们梳理了源算子的读取方式(fromSource / addSource)以及 Flink 1.12 之后的新架构变化。在转换算子部分,重点讲解了 map / flatMap / filter 基本转换,keyBy、简单聚合(sum/min/max/minBy/maxBy)及 reduce 归约聚合,并提及了用户自定义函数(UDF)与富函数。物理分区部分涵盖了随机(shuffle)、轮询(rebalance)、重缩放(rescale)、广播(broadcast)甚至全局和自定义分区,帮助理解数据如何在下游并行任务间分布。此外,分流(侧输出流)与合流的基本思路也做了简要介绍。最后,在输出算子方面,我们对比了旧版 addSink 与新版的 sinkTo,并给出了输出到文件、Kafka、MySQL(JDBC)以及自定义 Sink 的实践要点。