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 小时前
HDFS数据迁移
大数据·hadoop·hdfs
N串2 小时前
2.4 采购部门——权力来自信息不对称
大数据
南棱笑笑生2 小时前
20260503给万象奥科的开发板HD-RK3576-PI适配瑞芯微原厂的Android14时适配AP6256
大数据·elasticsearch·搜索引擎·rockchip
王莎莎-MinerU2 小时前
从 PDF 到知识资产:MinerU 文档解析如何成为企业 RAG 系统的“数据基石”
大数据·人工智能·pdf·个人开发
缝艺智研社2 小时前
誉财 YC - 21 平板下摆机:服装下摆与袖口加工的卓越之选
大数据·人工智能·自动化·电脑·新人首发·线上模板机
逸Y 仙X3 小时前
文章二十:Elasticsearch高亮搜索完全指南
java·大数据·运维·elasticsearch·搜索引擎·全文检索
2601_956139423 小时前
集团品牌全案公司哪家专业
大数据·人工智能·python
财经资讯数据_灵砚智能3 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月3日
大数据·人工智能·python·信息可视化·自然语言处理
灵机一物3 小时前
灵机一物AI原生电商小程序、PC端(已上线)-AI产业深度解析:Token供需失衡下的算力战争与产业变革
大数据·人工智能·深度学习