一.Flink概述
Apache Flink是一个分布式流处理和批处理框架,被设计用于处理大规模的数据, 并用与对无界和有界数据流进行有状态计算。
Flink官网地址:https://flink.apache.org/
在德语中,"flink"一词表示"快速,灵巧".项目logo是一只彩色的松鼠.
1.无界数据流:
比如瀑布
- 有定义流的开始,但没有定义流的结束;
- 它们会无休止的产生数据;
- 无界流的数据必须持续处理,也就是数据被摄取后需要立刻处理.我们不能等到所有数据都到达再处理,以为输入时是无限的
2.有界数据流
比如水龙头
- 有定义流的开始,也有定义流的结束;
- 有界流可以在摄取所有数据后再进行计算;
- 有界流所有数据可以被排序,所以并不需要有序摄取;
- 有界流处理通常被称为批处理
3.Flink应用场景
Flink在国内各个企业中大量使用.一些行业中的典型应用有:
1.电商和市场营销
举例: 实时数据报表、广告投放、实时推荐
2.物联网(IOT)
举例: 传感器实时数据采集和显示、实时报警,交通运输业
3.物流配送和服务业
举例: 订单状态实时更新、通知信息推送
4.银行和金融业
举例: 实时结算和通知推送,实时检测异常行为
4.Flink分层API
1.有状态流处理:通过底层API(处理函数),对最原始数据加工处理。底层APT与DataStream API相集成,可以处理复杂的计算。
2.DataStream APl(流处理)和DataSet API(批处理)封装了底层处理函数,提供了通用的模块,比如转换(transformations,包括map、flatmap等),连接(joins),聚合(aggregations),窗口(windows)操作等。注意:Flink1.12以后,DataStream API已经实现真正的流批一体,所以DataSet API已经过时。
3.Table API是以表为中心的声明式编程,其中表可能会动态变化。Table API遵循关系模型:表有二维数据结构,类似于关系数据库中的表,同时API提供可比较的操作,例如select、project、join、group-by、aggregate等。我们可以在表与DataStream/DataSet之间无缝切换,以允许程序将Table API 与 DataStream以及 DataSet混合使用。
4.SQL这一层在语法与表达能力上与Table API类似,但是是以SQL查询表达式的形式表现程序。SQL抽象与Table API交互密切,同时SOL查询可以直接在Table API定义的表上执行。
5.特点
(1).内置支持流处理和批处理:
Flink能够无缝地处理无界的数据流以及有界的数据集。这使得它非常适合处理实时数据和离线数据。批流统一,同一套代码或者SQL,可以跑流也可以跑批.
(2).高性能和可伸缩性:
Flink通过将数据处理任务分布在多个节点上并利用流水线执行模型,实现了高性能和可伸缩性。它能够处理大规模数据集并在故障发生时进行容错处理。具有高吞吐和低延迟的特征.
(3).状态管理:
Flink提供强大的状态管理功能,可以在流处理过程中跟踪和管理状态。这使得Flink能够处理有状态的流处理任务,例如按键分组、窗口计算和模式匹配。支持水平扩展架构, 支持超大状态与增量检测点机制. 大公司使用情况: 每天处理数万亿的事件,应用为维护几TB大小的状态,应用在数千个CPU核心上运行.
(4).支持多种数据源和数据接收器:
Flink能够连接到各种数据源,包括文件系统、消息队列、分布式数据库等。同时,它也可以将处理结果发送到不同的数据接收器,如数据库、消息队列等。
(5).exactly-once语义:
Flink支持精确一次处理语义,即保证每条输入数据只被处理一次,从而确保结果的准确性。这通过在系统级别跟踪和管理状态来实现。
(6).丰富的API和库:
Flink提供丰富的API和库,使开发人员能够使用Java、Scala或Python编写复杂的流处理和批处理应用。此外,Flink还集成了许多常用的第三方库,如Apache Kafka、Apache Cassandra等。
6.时间语义
Flink提供了三种时间语义:Event Time、Ingestion Time和Processing Time。
-
Event Time(事件时间):事件时间是事件实际发生的时间。在流处理中,事件通常会带有时间戳,表示事件发生的时间。使用事件时间进行处理时,Flink会根据事件的时间戳对事件进行排序和处理,以保证结果的正确性。事件时间处理非常适用于处理乱序事件或具有延迟的事件流。
-
Ingestion Time(摄入时间):摄入时间是事件进入Flink系统的时间。当事件到达Flink的数据源时,Flink会为事件分配一个摄入时间作为时间戳。使用摄入时间进行处理时,Flink会按照事件进入系统的顺序对事件进行处理。摄入时间处理适用于实时数据流,并且不受事件乱序或延迟的影响。
-
Processing Time(处理时间):处理时间是Flink系统内部的时间,即处理事件的时间。使用处理时间进行处理时,Flink会使用系统的本地时间作为时间戳,按照事件到达处理器的顺序进行处理。处理时间处理速度非常快,适用于对实时性要求较高的场景。
在Flink中,可以通过指定时间语义来定义事件流的处理方式。可以使用时间窗口、水印(Watermark)等机制来处理不同时间语义下的事件流。时间窗口可以根据事件时间或处理时间对事件进行分组和聚合,而水印用于解决事件乱序和延迟问题,以确保结果的准确性。
总结一下,Flink的时间语义包括事件时间、摄入时间和处理时间,每种时间语义都适用于不同的场景和需求。选择合适的时间语义可以提高处理结果的准确性和实时性。
7.窗口
Flink中的窗口是用于对数据流进行分组和聚合操作的一种机制。窗口可以将数据流划分为不同的时间段,并在每个时间段内对数据进行处理。
Flink提供了多种类型的窗口,包括滚动窗口(Tumbling Window)、滑动窗口(Sliding Window)、会话窗口(Session Window)和全局窗口(Global Window)等。
-
滚动窗口(Tumbling Window):滚动窗口将数据流划分为固定大小的、不重叠的时间段。例如,如果将数据流按照5秒的滚动窗口进行划分,则每个窗口包含5秒内的数据。滚动窗口适用于需要对数据进行周期性聚合的场景。
-
滑动窗口(Sliding Window):滑动窗口将数据流划分为固定大小的、可能重叠的时间段。与滚动窗口不同,滑动窗口可以在不同的时间段内包含相同的数据。例如,如果将数据流按照5秒的滑动窗口进行划分,且滑动步长为3秒,则每个窗口之间会有2秒的重叠部分。滑动窗口适用于需要对数据进行连续聚合的场景。
-
会话窗口(Session Window):会话窗口根据数据流中的活动间隔来划分窗口。当数据流中的两个事件之间的时间间隔超过指定的阈值时,会话窗口会结束并触发聚合操作。会话窗口适用于处理具有不确定持续时间的事件流。
-
全局窗口(Global Window):全局窗口将整个数据流作为一个窗口进行处理,不对数据进行划分。全局窗口适用于需要对整个数据流进行聚合的场景。
在Flink中,可以通过使用窗口函数来定义窗口的计算逻辑,例如对窗口内的数据进行求和、计数、平均值等聚合操作。窗口函数可以在窗口关闭时被触发执行,并输出计算结果。
总结一下,Flink的窗口是用于对数据流进行分组和聚合操作的机制,包括滚动窗口、滑动窗口、会话窗口和全局窗口等类型。选择合适的窗口类型和窗口函数可以根据业务需求对数据进行有效的处理和分析。
8.水位线
Flink中的水位线(Watermark)是用于处理事件时间乱序和延迟的机制。水位线用于告知系统当前事件时间的进展情况,以便在窗口操作中确定哪些事件已经到达,哪些事件还可能会到达。在Flink中,水印(Watermark)是用于处理事件时间乱序和延迟的机制,而水位线(Watermark)是水印的具体实现方式。
水位线是一个特殊的事件,它带有一个时间戳,并且代表了事件时间的进展。在流处理中,事件的时间戳表示事件实际发生的时间。水位线的时间戳通常比实际事件时间要晚一些,以确保所有具有较早时间戳的事件都已经到达系统。
Flink中的水位线可以通过以下方式生成:
-
周期性水位线生成器(Periodic Watermark Generator):周期性水位线生成器根据固定的时间间隔生成水位线。例如,每隔1秒生成一个水位线,表示系统认为事件时间已经推进了1秒。
-
指定延迟的水位线生成器(Bounded Out-of-Orderness Watermark Generator):指定延迟的水位线生成器根据事件的时间戳和指定的延迟值来生成水位线。例如,如果指定的延迟值为3秒,则水位线的时间戳将设置为事件时间减去3秒。
在Flink中,水位线的作用主要体现在窗口操作中,用于确定何时关闭窗口并触发计算。当一个窗口的结束时间小于等于当前水位线时,该窗口将被关闭,并触发相应的聚合操作。
水位线的使用可以解决事件时间乱序和延迟带来的问题,确保结果的准确性。通过设置适当的水位线策略和延迟值,可以根据实际场景来处理不同程度的乱序和延迟。
总结一下,Flink中的水位线是用于处理事件时间乱序和延迟的机制,用于告知系统当前事件时间的进展情况。水位线在窗口操作中起到重要作用,用于确定何时关闭窗口并触发计算。通过设置适当的水位线策略和延迟值,可以确保结果的准确性和实时性。
9.时间语义、窗口和水位线流程
-
定义事件时间属性:在数据流中,每个事件都应该包含一个事件时间属性,表示事件实际发生的时间。可以通过调用
assignTimestampsAndWatermarks()
方法来指定事件时间属性。 -
生成水位线:根据业务需求选择合适的水位线生成策略,可以使用周期性水位线生成器或自定义水位线生成器。水位线的生成方式通常是根据事件时间属性计算得出,比如根据事件时间戳加上一个固定的延迟值。
-
分配水位线:通过调用
assignTimestampsAndWatermarks()
方法将生成的水位线分配给数据流。 -
定义窗口:使用
window()
方法定义窗口的类型和大小。Flink提供了多种窗口类型,如滚动窗口、滑动窗口和会话窗口。可以根据业务需求选择合适的窗口类型,并指定窗口的大小。 -
分组和聚合:通过
keyBy()
方法对数据流进行分组,然后使用聚合函数(如sum()
、count()
等)对每个窗口内的数据进行聚合操作。 -
触发计算:当水位线超过窗口的结束时间时,该窗口将被关闭,并触发相应的计算和输出结果。
-
处理延迟数据:由于水位线的存在,可能会有一些延迟的事件到达系统。在窗口计算过程中,Flink会根据水位线判断是否等待更多的事件到达,以处理延迟数据。
总结一下,时间语义配合水位线和窗口的过程包括定义事件时间属性、生成水位线、分配水位线、定义窗口、分组和聚合以及触发计算。通过这个过程,Flink可以实现基于事件时间的窗口计算,并处理事件乱序和延迟的情况。
二.Flink快速上手
1.创建项目
在IDEA中创建一个Maven工程
2.引入依赖
<properties>
<flink.version>1.17.0</flink.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>
3.编写代码
需求:统计一段文字中,每个单词出现的频次.
3.1批处理
批处理基本思路:先逐行读入文件数据,然后将每一行文字拆分成单词;接着按照单词分组,统计每组数据的个数,就是对应单词的频次.
首先创建word.txt文件
在项目的根目录下创建input软件包,创建word.txt文件,写入一些单词.
然后创建一个BatchDemo类编写代码
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.AggregateOperator;
import org.apache.flink.api.java.operators.DataSource;
import org.apache.flink.api.java.operators.FlatMapOperator;
import org.apache.flink.api.java.operators.UnsortedGrouping;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.Collector;
public class BatchDemo {
public static void main(String[] args) throws Exception {
//1.创建执行环境
ExecutionEnvironment environment = ExecutionEnvironment.getExecutionEnvironment();
//2.从文件中读取数据
DataSource<String> dataSource = environment.readTextFile("input/word.txt");
//3.切分转换成(word,1)格式
FlatMapOperator<String, Tuple2<String, Integer>> flatMap = dataSource.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
//3.1 按照空格切分单词
String[] words = s.split(" ");
//3.2 将单词转换为(word,1)
for (String word : words) {
Tuple2<String, Integer> tuple2 = Tuple2.of(word, 1);
//3.3 使用Collector向下游发送数据
collector.collect(tuple2);
}
}
});
//4.按照word分组
UnsortedGrouping<Tuple2<String, Integer>> wordGroupBy = flatMap.groupBy(0);
//5.各分组内聚合 1代表索引位置
AggregateOperator<Tuple2<String, Integer>> sum = wordGroupBy.sum(1);
//6.输出结果
sum.print();
}
}
输出结果
需要注意的是,这种代码的实现方式,是基于DataSet API的,也就是我们对数据的处理转换,是看作数据集来进行操作的。事实上Flink本身是流批统一的处理架构,批量的数据集本质上也是流,没有必要用两套不同的API来实现。所以从Flink 1.12开始,官方推荐的做法是直接使用DataStream API,在提交任务时通过将执行模式设为BATCH来进行批处理:$ bin/flink run -Dexecution.runtime-mode=BATCH BatchWordCount.jar
这样,DataSet API就没什么用了,在实际应用中我们只要维护一套 DataStream API就可以。
3.2流处理
创建一个StreamDemo类编写代码
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class StreamDemo {
public static void main(String[] args) throws Exception {
//1.创建执行环境
StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
//2.从文件中读取数据
DataStreamSource<String> streamSource = environment.readTextFile("input/word.txt");
//3.切分转换成(word,1)格式
SingleOutputStreamOperator<Tuple2<String, Integer>> flatMap = streamSource.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
//3.1 按照空格切分单词
String[] words = s.split(" ");
//3.2 将单词转换为(word,1)
for (String word : words) {
Tuple2<String, Integer> tuple2 = Tuple2.of(word, 1);
//3.3 使用Collector向下游发送数据
collector.collect(tuple2);
}
}
});
//4.按照word分组
KeyedStream<Tuple2<String, Integer>, String> wordAndOneKs = flatMap.keyBy(new KeySelector<Tuple2<String, Integer>, String>() {
@Override
public String getKey(Tuple2<String, Integer> value) throws Exception {
return value.f0;
}
});
//5.各分组内聚合 1代表索引位置
SingleOutputStreamOperator<Tuple2<String, Integer>> sum = wordAndOneKs.sum(1);
//6.输出结果
sum.print();
//7.执行
environment.execute();
}
}
输出结果
3.3批处理和流处理的区别
- 执行环境不同
- 分组使用的方法不同
- 流处理最后必须有个执行的方法,而批处理则不需要
最后安利大家看一个very wonderful 的电视剧,那就是少年歌行,特别好看...(♥∀♥)