Flink第五章:DataStream API

文章目录


前言

在大数据实时处理领域,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输出

(一)连接到外部系统

  • Flink的DataStream API专门提供了向外部写入数据的方法:addSink。与addSource类似,addSink方法对应着一个"Sink"算子,主要就是用来实现与外部系统连接、并将数据提交写入的;Flink程序中所有对外的输出操作,一般都是利用Sink算子完成的。
  • 注:Flink1.12以前,Sink算子的创建是通过调用DataStream的.addSink()方法实现的。
java 复制代码
stream.addSink(new SinkFunction(...));
  • 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 的实践要点。

相关推荐
大大大大晴天2 小时前
每天认识一个组件:数据治理Apache Atlas
大数据
数字新视界2 小时前
2026模块化机房选型指南:行业市场发展趋势、占有率与竞争梯队分析报告解析
大数据·人工智能·物联网·数据中心·微模块机房·模块化机房·冷通道
姜穆澜4 小时前
OneID 从 0 到 1 完整生产案例(四)
大数据
OsDepK5 小时前
项目快速Git至仓库(完整版)
大数据·git·elasticsearch·搜索引擎
合米AI SOP系统5 小时前
传统产线如何快速上马落地 AI 防错?合米科技 AI SOP 7天即可上线。
大数据·人工智能·科技
思录Echo5 小时前
什么决定具身智能的最终走向?多技术路线与落地现实辨析
大数据·人工智能
xiaohaiAIgeo6 小时前
【2026年】AI监控加行为分析守护实验室安全
大数据·人工智能·科普知识
林墨聊AIGC6 小时前
动漫AI视频创作工具在哪找到的?2026年最新动漫AI视频平台与软件指南
大数据·人工智能·ai作画·aigc·音视频
故七月8 小时前
告别 AI 时代品牌 “隐身”:万域智瞰 AI‑GEO,构建品牌大模型时代营销新基建
大数据·人工智能
数字孪生视频孪生9 小时前
三维实时重构异构底座 核工危化无感定位跨境轨迹一屏统揽
大数据·运维·人工智能·重构·架构