flink1.13(一)

1. 初识Flink

1.1 Flink是什么

  Apache Flink 是一个框架和分布式处理引擎, 用于对无界和有界数据流进行状态计算. Flink框架处理流程如下图所示:
flink1.13(一)_第1张图片
Flink的应用场景:
flink1.13(一)_第2张图片

1.2 为什么选择Flink

  • 批处理和流处理
  • 流数据更真实地反应了我们的生活方式
  • 我们的目标
    • 低延迟
    • 高吞吐
    • 结果的准确性和良好的容错性

1.3 数据处理架构的发展

  1. 事务处理
    flink1.13(一)_第3张图片
  2. 分析处理, 如大数据框架hadoop和hive 都是离线的, 保存一定的数据量后进行处理
  3. 有状态的流式处理
    flink1.13(一)_第4张图片
  4. lambda架构的流式处理
    flink1.13(一)_第5张图片

1.4 新一代流处理器Flink

核心特点:

  1. 高吞吐, 低延迟
  2. 结果的准确性
  3. 精确一次(exactly-once)的状态一致性保证
  4. 可以与众多常用存储系统连接
  5. 高可用, 支持动态扩展

1.5 Flink的应用场景

  1. 事件驱动型应用
    flink1.13(一)_第6张图片
    事件驱动型应用是一类具有状态的应用,它从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。比较典型的就是以 Kafka 为代表的消息队列几乎都是事件驱动型应用。
    这其实跟传统事务处理本质上是一样的,区别在于基于有状态流处理的事件驱动应用,不再需要查询远程数据库,而是在本地访问它们的数据,这样在吞吐量和延迟方面就可以有更好的性能。
    另外远程持久性存储的检查点保证了应用可以从故障中恢复。检查点可以异步和增量地完成,因此对正常计算的影响非常小。

  2. 数据分析型应用
    flink1.13(一)_第7张图片
    所谓的数据分析,就是从原始数据中提取信息和发掘规律。传统上,数据分析一般是先将数据复制到数据仓库(Data Warehouse),然后进行批量查询。如果数据有了更新,必须将最新数据添加到要分析的数据集中,然后重新运行查询或应用程序。
    如今,Apache Hadoop 生态系统的组件,已经是许多企业大数据架构中不可或缺的组成部分。现在的做法一般是将大量数据(如日志文件)写入 Hadoop 的分布式文件系统(HDFS)、S3 或 HBase 等批量存储数据库,以较低的成本进行大容量存储。然后可以通过 SQL-on-Hadoop
    类的引擎查询和处理数据,比如大家熟悉的 Hive。这种处理方式,是典型的批处理,特点是可以处理海量数据,但实时性较差,所以也叫离线分析。
    如果我们有了一个复杂的流处理引擎,数据分析其实也可以实时执行。流式查询或应用程序不是读取有限的数据集,而是接收实时事件流,不断生成和更新结果。结果要么写入外部数据库,要么作为内部状态进行维护。
    Apache Flink 同时支持流式与批处理的数据分析应用。
    与批处理分析相比,流处理分析最大的优势就是低延迟,真正实现了实时。另外,流处理不需要去单独考虑新数据的导入和处理,实时更新本来就是流处理的基本模式。当前企业对流式数据处理的一个热点应用就是实时数仓,很多公司正是基于 Flink 来实现的。

  3. 数据管道型应用
    flink1.13(一)_第8张图片
    ETL 也就是数据的提取、转换、加载,是在存储系统之间转换和移动数据的常用方法。在数据分析的应用中,通常会定期触发 ETL 任务,将数据从事务数据库系统复制到分析数据库或数据仓库。
    所谓数据管道的作用与 ETL 类似。它们可以转换和扩展数据,也可以在存储系统之间移动数据。不过如果我们用流处理架构来搭建数据管道,这些工作就可以连续运行,而不需要再去周期性触发了。比如,数据管道可以用来监控文件系统目录中的新文件,将数据写入事件日志。连续数据管道的明显优势是减少了将数据移动到目的地的延迟,而且更加通用,可以用于更多的场景。如图所示展示了 ETL 与数据管道之间的区别。
    有状态的流处理架构上其实并不复杂,很多用户基于这种思想开发出了自己的流处理系统,这就是第一代流处理器。Apache Storm 就是其中的代表。Storm 可以说是开源流处理的先锋,最早是由 Nathan Marz 和创业公司 BackType 的一个团队开发的,后来才成为 Apache 软
    件基金会下属的项目。Storm 提供了低延迟的流处理,但是它也为实时性付出了代价:很难实现高吞吐,而且无法保证结果的正确性。用更专业的话说,它并不能保证“精确一次”
    (exactly-once);即便是它能够保证的一致性级别,开销也相当大。关于状态一致性和exactly-once,我们会在后续的章节中展开讨论。

Flink的分层API

flink1.13(一)_第9张图片
在Flink1.12版本后, 用DataStreamAPI可以实现批流一体化, 现在基本上不用DataSetAPI了

2. Flink快速上手

2.1 创建一个Java项目

引入maven相关依赖

    <properties>
        <java.version>1.8java.version>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASEspring-boot.version>
        <flink.version>1.13.0flink.version>
        <java.version>1.8java.version>
        <scala.binary.version>2.12scala.binary.version>
        <slf4j.version>1.7.30slf4j.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.apache.flinkgroupId>
            <artifactId>flink-javaartifactId>
            <version>${flink.version}version>
        dependency>
        <dependency>
            <groupId>org.apache.flinkgroupId>
            <artifactId>flink-streaming-java_${scala.binary.version}artifactId>
            <version>${flink.version}version>
        dependency>
        <dependency>
            <groupId>org.apache.flinkgroupId>
            <artifactId>flink-clients_${scala.binary.version}artifactId>
            <version>${flink.version}version>
        dependency>
        
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-apiartifactId>
            <version>${slf4j.version}version>
        dependency>
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-log4j12artifactId>
            <version>${slf4j.version}version>
        dependency>
        <dependency>
            <groupId>org.apache.logging.log4jgroupId>
            <artifactId>log4j-to-slf4jartifactId>
            <version>2.14.0version>
        dependency>
    dependencies>

配置日志管理文件, 在resources下添加log4j.properties

log4j.rootLogger=error, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

2.2 批处理wordCount程序

新建一个Java类, 用来测试批处理WordCount程序. 在根目录下创建一个input文件夹, 内部放一个words.txt文件
flink1.13(一)_第10张图片


package com.yangjunyi.flink.batch.wordCount;

import org.apache.flink.api.common.typeinfo.Types;
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 BatchWordCount {
    public static void main(String[] args) {
        // 1. 创建执行环境, 这里没有连接flink环境, 是因为数据源放在idea本地, 所以不需要连接到集群
        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
        // 2. 获取数据源
        DataSource<String> strDS = env.readTextFile("input/words.txt");
        // 3. 转换数据格式, 需要把一行一行的字符串做扁平化处理 flatMap() 扁平化函数
        // flatMap后接一个函数式变成, String line 代表读到的一行数据, 后面的Collector>
        // 代表需要把line收集成一个tuple(元组类型) 数据结构类似于('hello',1) ('world',1) 这种格式
        // 返回值代表扁平化的算子, 代表对运算的结果的中间算术运算, 第一个泛型String, 代表数据源每一行都是String类型
        // 第二个泛型Tuple是最终算子的运算结果类型
        FlatMapOperator<String, Tuple2<String, Long>> wordAndOne = strDS.flatMap((String line, Collector<Tuple2<String, Long>> out) -> {
            // 在这里写如何对一个line进行操作, 使其变成多个元组
            String[] lineStrs = line.split(" ");
            for (String lineStr : lineStrs) {
                // 使用Collector收集器对循环出来的每一个字符串进行收集,
                // 每获得一个字符串就在元组中记录该字符串的数量为1
                out.collect(Tuple2.of(lineStr, 1L));
            }
            // 在函数式变成的最后, 声明返回值是一个Tuple类型, Tuple的泛型为String 和Long
        }).returns(Types.TUPLE(Types.STRING, Types.LONG));
        // 4. 按照word进行分组, 0这个参数是对元组中哪个索引位置的数据进行分组, 我们对tuple中的第一个位置String类型的word进行分组
        UnsortedGrouping<Tuple2<String, Long>> tuple2UnsortedGrouping = wordAndOne.groupBy(0);
        // 5. 进行聚合, 对tuple中索引为1的地方, 也就是word的count进行聚合, 聚合方式是sum加和
        AggregateOperator<Tuple2<String, Long>> sum = tuple2UnsortedGrouping.sum(1);
        // 该print会抛出异常, 所以需要处理该异常
        try {
            sum.print();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

我在运行这段代码时是报错的. 提示org.apache.flink.runtime.taskexecutor.slot.SlotNotFoundException: Could not find slot for dff66956ba77ffac489f5136cc27364b. 但是通过一步一步debug时 ,我发现是可以打印出来的, 但是输出的结果隐藏在一堆报错中间, 不太方便查看, 这个错误字面意思是不能找到某一个slot, 关于这个的具体原因我会边学边思考.

2.3 流处理wordCount程序

流式处理, 执行结果也是一条一条累加的, 注意:需要使用流式处理环境和最后的执行命令

package com.yangjunyi.flink.stream.wordCount;

import org.apache.flink.api.common.JobExecutionResult;
import org.apache.flink.api.common.typeinfo.Types;
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 StreamWordCount {
    public static void main(String[] args) throws Exception {
        // 创建一个流处理环境
        StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> stringDataStreamSource = executionEnvironment.readTextFile("input/words.txt");
        // 对读出来的数据进行扁平化操作
        SingleOutputStreamOperator<Tuple2<String, Long>> wordAndOne = stringDataStreamSource.flatMap((String line, Collector<Tuple2<String, Long>> wordCount) -> {
            String[] words = line.split(" ");
            for (String word : words) {
                wordCount.collect(Tuple2.of(word, 1L));
            }
        }).returns(Types.TUPLE(Types.STRING, Types.LONG));
        // 设置分组, 以word进行分组
        KeyedStream<Tuple2<String, Long>, String> tuple2StringKeyedStream = wordAndOne.keyBy(data -> data.f0);
        // 对tuple中的count进行求和
        SingleOutputStreamOperator<Tuple2<String, Long>> sum = tuple2StringKeyedStream.sum(1);
        sum.print();
        // 注意流式处理时, 因为是要一直读取的 所以多了一步启动的命令, 这里有异常, 先抛出去
        JobExecutionResult execute = executionEnvironment.execute();
    }
}

flink1.13(一)_第11张图片
flink是并行执行的大数据框架, 不能保证对每一行的计算都是顺序执行的, 每一个数据是不断变化的, 可以看到依次叠加的过程, 因为流式处理, 数据是一条一条来的, 一共3个hello, 会被flink分配到一个执行单元去执行, 所以才会出现hello,1 hello,2 hello,3的现象. 在开发环境时, Idea用多线程来模拟flink集群, 并行的处理数据. 结果前面的数字, 代表本地以哪个线程来执行并输出任务的, flink中把最小的任务单位叫做任务槽(task slot)

2.4 无界流处理WordCount

正常情况下, 流应该是源源不断的状态过来的, 所以要保持一个监听, 时刻监听需要处理的流数据. 可以用Linux自带的netcat来模拟socket, 实时发送流数据. 在linux服务器中, 使用命令nc -lk 7777启动netcat 并使用7777端口来保持持续发送的功能, 如果没有nc命令的话, 使用yum install nc 来安装.

package com.yangjunyi.flink;

import org.apache.flink.api.common.JobExecutionResult;
import org.apache.flink.api.common.typeinfo.Types;
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 StreamWordCount {
    public static void main(String[] args) throws Exception {
        // 创建一个流处理环境
        StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
//        DataStreamSource stringDataStreamSource = executionEnvironment.readTextFile("input/words.txt");
        // 读取一个socket文本流, 配置主机名, 端口号
        DataStreamSource<String> stringDataStreamSource = executionEnvironment.socketTextStream("192.168.0.105", 7777);
        // 对读出来的数据进行扁平化操作
        SingleOutputStreamOperator<Tuple2<String, Long>> wordAndOne = stringDataStreamSource.flatMap((String line, Collector<Tuple2<String, Long>> wordCount) -> {
            String[] words = line.split(" ");
            for (String word : words) {
                wordCount.collect(Tuple2.of(word, 1L));
            }
        }).returns(Types.TUPLE(Types.STRING, Types.LONG));
        // 设置分组, 以word进行分组
        KeyedStream<Tuple2<String, Long>, String> tuple2StringKeyedStream = wordAndOne.keyBy(data -> data.f0);
        // 对tuple中的count进行求和
        SingleOutputStreamOperator<Tuple2<String, Long>> sum = tuple2StringKeyedStream.sum(1);
        sum.print();
        // 注意流式处理时, 因为是要一直读取的 所以多了一步启动的命令, 这里有异常, 先抛出去
        JobExecutionResult execute = executionEnvironment.execute();
    }
}

把直接读取文本流, 换成监听socket端口,其余操作不变
在socket中输入hello world
在这里插入图片描述
可以看到在idea中已经读取到了输入的行, 并按wordcount的方式输出了
flink1.13(一)_第12张图片

2.5 通过运行时参数配置监听ip和端口

flink提供的通过main方法的args参数来提取参数的工具ParameterTool.formArgs()
把前面写死的ip和端口号通过参数的配置来读取出来
flink1.13(一)_第13张图片
配置idea的启动命令参数
flink1.13(一)_第14张图片
在netcat中输入文本后, 在idea中可以看到结果
flink1.13(一)_第15张图片

3.Flink部署

在idea中启动flink流处理时, 不会已启动就执行相关代码的, 在启动时, 首先要把计算机模拟成一个flink集群, 然后把中间的代码打包成一个job发布到flink集群上, 在数据进来后才会依次执行.
  这里需要提到 Flink 中的几个关键组件:客户端(Client)、作业管理器(JobManager)和任务管理器(TaskManager)。我们的代码,实际上是由客户端获取并做转换,之后提交给JobManger 的。所以 JobManager 就是 Flink 集群里的“管事人”,对作业进行中央调度管理;而它获取到要执行的作业后,会进一步处理转换,然后分发任务给众多的 TaskManager。这里的 TaskManager,就是真正“干活的人”,数据的处理操作都是它们来做的.
flink1.13(一)_第16张图片

3.1 快速启动

3.1.1 环境配置

Flink 是一个分布式的流处理框架,所以实际应用一般都需要搭建集群环境。我们在进行Flink 安装部署的学习时,需要准备 3 台 Linux 机器。具体要求如下:
flink1.13(一)_第17张图片

3.1.2 本地启动

最简单的启动方式,其实是不搭建集群,直接本地启动。本地部署非常简单,直接解压安装包就可以使用,不用进行任何配置;一般用来做一些简单的测试。

  1. 下载安装包
    进入 Flink 官网,下载 1.13.0 版本安装包 flink-1.13.0-bin-scala_2.12.tgz,注意此处选用对
    应 scala 版本为 scala 2.12 的安装包。
  2. 解压
    在 hadoop102 节点服务器上创建安装目录/opt/module,将 flink 安装包放在该目录下,并
    执行解压命令,解压至当前目录。
    tar -zxvf flink-1.13.0-bin-scala_2.12.tgz -C /opt/module/
  3. 启动
    进入解压后的目录,执行启动命令,并查看进程。
$ cd flink-1.13.0/
$ bin/start-cluster.sh 
Starting cluster.
Starting standalonesession daemon on host hadoop102.
Starting taskexecutor daemon on host hadoop102.
$ jps
10369 StandaloneSessionClusterEntrypoint
10680 TaskManagerRunner
10717 Jps
  1. 访问 Web UI
    启动成功后,访问 http://hadoop102:8081,可以对 flink 集群和任务进行监控管理,如图 3-2
    所示。
  2. 关闭集群
    如果想要让 Flink 集群停止运行,可以执行以下命令:
    bin/stop-cluster.sh

3.1.3 集群启动

  Flink 是典型的 Master-Slave 架构的分布式数据处理框架,其中 Master 角色对应着JobManager,Slave 角色则对应 TaskManager。我们对三台节点服务器的角色分配如下所示。
在这里插入图片描述

  1. 下载并解压安装包
    具体操作与上节相同。
  2. 修改集群配置
    进入 conf 目录下,修改 flink-conf.yaml 文件,修改 jobmanager.rpc.address 参数为
    hadoop102,如下所示:
cd conf/
vim flink-conf.yaml
# JobManager 节点地址.
jobmanager.rpc.address: hadoop102

这就指定了 hadoop102 节点服务器为 JobManager 节点。修改 workers 文件,将另外两台节点服务器添加为本 Flink 集群的 TaskManager 节点,具体修改如下:

vim workers 
hadoop103
hadoop104

这样就指定了 hadoop103 和 hadoop104 为 TaskManager 节点。
另外,在 flink-conf.yaml 文件中还可以对集群中的 JobManager 和 TaskManager 组件
进行优化配置,主要配置项如下:

jobmanager.memory.process.size:对 JobManager 进程可使用到的全部内存进行配置,
包括 JVM 元空间和其他开销,默认为 1600M,可以根据集群规模进行适当调整。
taskmanager.memory.process.size:对 TaskManager 进程可使用到的全部内存进行配置,
包括 JVM 元空间和其他开销,默认为 1600M,可以根据集群规模进行适当调整。
taskmanager.numberOfTaskSlots:对每个 TaskManager 能够分配的 Slot 数量进行配置,
默认为 1,可根据 TaskManager 所在的机器能够提供给 Flink 的 CPU 数量决定。所谓
Slot 就是 TaskManager 中具体运行一个任务所分配的计算资源。
parallelism.default:Flink 任务执行的默认并行度,优先级低于代码中进行的并行度配
置和任务提交时使用参数指定的并行度数量。

  1. 分发安装目录
    配置修改完毕后,将 Flink 安装目录发给另外两个节点服务器。
$ scp -r ./flink-1.13.0 atguigu@hadoop103:/opt/module
$ scp -r ./flink-1.13.0 atguigu@hadoop104:/opt/module
  1. 启动集群
    在 hadoop102 节点服务器上执行 start-cluster.sh 启动 Flink 集群:
    bin/start-cluster.sh 使用jps查看进程启动情况

  2. 访问 Web UI
    启动成功后,同样可以访问 http://hadoop102:8081 对 flink 集群和任务进行监控管理,如图
    所示。
    这里可以明显看到,当前集群的 TaskManager 数量为 2;由于默认每个 TaskManager 的 Slot
    数量为 1,所以总 Slot 数和可用 Slot 数都为 2。
    flink1.13(一)_第18张图片

  3. 关闭集群
    如果想要让 Flink 集群停止运行,可以执行以下命令:bin/stop-cluster.sh

3.1.4 向集群提交作业和取消作业

  1. 程序打包
    (1)为方便自定义结构和定制依赖,我们可以引入插件 maven-assembly-plugin 进行打包。在 Flink项目的 pom.xml 文件中添加打包插件的配置,具体如下:
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-assembly-pluginartifactId>
                <version>3.0.0version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependenciesdescriptorRef>
                    descriptorRefs>
                configuration>
                <executions>
                    <execution>
                        <id>make-assemblyid>
                        <phase>packagephase>
                        <goals>
                            <goal>singlegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>

(2)插件配置完毕后,可以使用 IDEA 的 Maven 工具执行 package 命令,出现如下提示即
表示打包成功。
  打 包 完 成 后 , 在 target 目 录 下 即 可 找 到 所 需 JAR 包 , JAR 包 会 有 两 个 ,
FlinkTutorial-1.0-SNAPSHOT.jar 和 FlinkTutorial-1.0-SNAPSHOT-jar-with-dependencies.jar,因
为集群中已经具备任务运行所需的所有依赖,所以建议使用 Flink-1.0-SNAPSHOT.jar

  1. 在 Web UI 上提交作业
    (1)任务打包完成后,我们打开 Flink 的 WEB UI 页面,在右侧导航栏点击“Submit New
    Job”,然后点击按钮“+ Add New”,选择要上传运行的 JAR 包.
    (2)点击该 JAR 包,出现任务配置页面,进行相应配置。主要配置程序入口主类的全类名,任务运行的并行度,任务运行所需的配置参数和保存点路径等,配置完成后,即可点击按钮“Submit”,将任务提交到集群运行。
    flink1.13(一)_第19张图片

    (3)任务提交成功之后,可点击左侧导航栏的“Running Jobs”查看程序运行列表情况
    flink1.13(一)_第20张图片
    启动报错了, 在jobManager中查看任务运行日志, 看到是没有启动nc,导致socket连接失败,启动nc后重新执行
    flink1.13(一)_第21张图片
    flink1.13(一)_第22张图片
    flink1.13(一)_第23张图片

    在taskManager的控制台, 可以看到输出结果, 我们有两个taskManager并且任务的并行度是2, 所以这两个会同时输出.
    (4)点击该任务,可以查看任务运行的具体情况,也可以通过点击“Cancel Job”结束任
    务运行

  2. 使用命令行提交作业
      除了通过 WEB UI 界面提交任务之外,也可以直接通过命令行来提交任务。这里为方便
    起见,我们可以先把 jar 包直接上传到目录 flink-1.13.0 下. 先把控制台执行的task停止
    再进入到 Flink 的安装路径下,在命令行使用 flink run 命令提交作业.
    ./bin/flink run -m TEST-BIGDATA-05:8081 -c com.yangjunyi.flink.StreamWordCount -p 2 ./flink-1.0-SNAPSHOT.jar --hostname 192.168.0.105 --port 7777
    在命令行中ctrl+c退出后, 对正在运行的task没有影响
    这里的参数 –m 指定了提交到的 JobManager,-c 指定了入口类, 如果想指定运行时参数, 在jar包后面添加相应的即可, 再去flink-ui中也可以看到用命令行提交应用的执行情况.
    运行后, 当前的task slot变成了0
    flink1.13(一)_第24张图片
    如果slot为0时,再次提交任务, 则提交时会直接报错Could not acquire the minimum required resources获取不到最小运行需要的资源

在命令行中取消正在执行的任务
./bin/flink cancel jobId jobId是命令行提交后输出的值
在这里插入图片描述

3.2 部署模式

  在一些应用场景中,对于集群资源分配和占用的方式,可能会有特定的需求。Flink 为各种场景提供了不同的部署模式,主要有以下三种:

  • 会话模式(Session Mode)
  • 单作业模式(Per-Job Mode)
  • 应用模式(Application Mode)
      它们的区别主要在于:集群的生命周期以及资源的分配方式;以及应用的 main 方法到底在哪里执行是客户端(Client)还是 JobManager。接下来我们就做一个展开说明。

3.2.1 会话模式(Session Mode)

  会话模式其实最符合常规思维。我们需要先启动一个集群,保持一个会话,在这个会话中通过客户端提交作业,如图 所示。集群启动时所有资源就都已经确定,所以所有提交的作业会竞争集群中的资源。
flink1.13(一)_第25张图片
  这样的好处很明显,我们只需要一个集群,就像一个大箱子,所有的作业提交之后都塞进去;集群的生命周期是超越于作业之上的,铁打的营盘流水的兵,作业结束了就释放资源,集群依然正常运行。当然缺点也是显而易见的:因为资源是共享的,所以资源不够了,提交新的作业就会失败。另外,同一个 TaskManager 上可能运行了很多作业,如果其中一个发生故障导致 TaskManager 宕机,那么所有作业都会受到影响。之前先启动集群再提交作业, 这种方式就是会话模式. 会话模式比较适合于单个规模小, 执行时间短的大量作业

3.2.2 单作业模式(Per-Job Mode)

  会话模式因为资源共享会导致很多问题,所以为了更好地隔离资源,我们可以考虑为每个
提交的作业启动一个集群,这就是所谓的单作业(Per-Job)模式,
flink1.13(一)_第26张图片
  单作业模式也很好理解,就是严格的一对一,集群只为这个作业而生。同样由客户端运行应用程序,然后启动集群,作业被提交给 JobManager,进而分发给 TaskManager 执行。作业作业完成后,集群就会关闭,所有资源也会释放。这样一来,每个作业都有它自己的 JobManager管理,占用独享的资源,即使发生故障,它的 TaskManager 宕机也不会影响其他作业。
  这些特性使得单作业模式在生产环境运行更加稳定,所以是实际应用的首选模式。
  需要注意的是,Flink 本身无法直接这样运行,所以单作业模式一般需要借助一些资源管理框架来启动集群,比如 YARN、Kubernetes。

3.3.3 应用模式(Application Mode)

  前面提到的两种模式下,应用代码都是在客户端上执行,然后由客户端提交给 JobManager的。但是这种方式客户端需要占用大量网络带宽,去下载依赖和把二进制数据发送给JobManager;加上很多情况下我们提交作业用的是同一个客户端,就会加重客户端所在节点的资源消耗。
  所以解决办法就是,我们不要客户端了,直接把应用提交到 JobManger 上运行。而这也就代表着,我们需要为每一个提交的应用单独启动一个 JobManager,也就是创建一个集群。这个 JobManager 只为执行这一个应用而存在,执行结束之后 JobManager 也就关闭了,这就是所谓的应用模式,
flink1.13(一)_第27张图片
  应用模式与单作业模式,都是提交作业之后才创建集群;单作业模式是通过客户端来提交的,客户端解析出的每一个作业对应一个集群;而应用模式下,是直接由 JobManager 执行应用程序的,并且即使应用包含了多个作业,也只创建一个集群。
  总结一下,在会话模式下,集群的生命周期独立于集群上运行的任何作业的生命周期,并且提交的所有作业共享资源。而单作业模式为每个提交的作业创建一个集群,带来了更好的资源隔离,这时集群的生命周期与作业的生命周期绑定。最后,应用模式为每个应用程序创建一个会话集群,在 JobManager 上直接调用应用程序的 main()方法。
  我们所讲到的部署模式,相对是比较抽象的概念。实际应用时,一般需要和资源管理平台结合起来,选择特定的模式来分配资源、部署应用。接下来,我们就针对不同的资源提供者(Resource Provider)的场景,具体介绍 Flink 的部署方式。

3.3 独立模式部署

独立模式部署使用的很少, 不再赘述了.

3.4 YARN模式部署

  独立(Standalone)模式由 Flink 自身提供资源,无需其他框架,这种方式降低了和其他第三方资源框架的耦合性,独立性非常强。但我们知道,Flink 是大数据计算框架,不是资源调度框架,这并不是它的强项;所以还是应该让专业的框架做专业的事,和其他资源调度框架集成更靠谱。而在目前大数据生态中,国内应用最为广泛的资源管理平台就是 YARN 了。所以接下来我们就将学习,在强大的 YARN 平台上 Flink 是如何集成部署的。
  整体来说,YARN 上部署的过程是:客户端把 Flink 应用提交给 Yarn 的 ResourceManager, Yarn 的ResourceManager 会向 Yarn 的 NodeManager 申请容器。在这些容器上,Flink 会部署JobManager 和 TaskManager 的实例,从而启动集群。Flink 会根据运行在 JobManger 上的作业所需要的 Slot 数量动态分配 TaskManager 资源。

3.4.1 相关准备和配置

  在 Flink1.8.0 之前的版本,想要以 YARN 模式部署 Flink 任务时,需要 Flink 是有 Hadoop支持的。从 Flink 1.8 版本开始,不再提供基于 Hadoop 编译的安装包,若需要 Hadoop 的环境支持,需要自行在官网下载 Hadoop 相关版本的组件 flink-shaded-hadoop-2-uber-2.7.5-10.0.jar,并将该组件上传至 Flink 的 lib 目录下。在 Flink 1.11.0 版本之后,增加了很多重要新特性,其中就包括增加了对Hadoop3.0.0以及更高版本Hadoop的支持,不再提供“flink-shaded-hadoop-*”jar 包,而是通过配置环境变量完成与 YARN 集群的对接。
  在将 Flink 任务部署至 YARN 集群之前,需要确认集群是否安装有 Hadoop,保证 Hadoop版本至少在 2.2 以上,并且集群中安装有 HDFS 服务。
具体配置步骤如下:
(1)按照 3.1 节所述,下载并解压安装包,并将解压后的安装包重命名为 flink-1.13.0-yarn,
本节的相关操作都将默认在此安装路径下执行。
(2)配置环境变量,增加环境变量配置如下:

 vim /etc/profile.d/my_env.sh
HADOOP_HOME=/opt/module/hadoop-2.7.5
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
export HADOOP_CONF_DIR=${HADOOP_HOME}/etc/hadoop
export HADOOP_CLASSPATH=`hadoop classpath`

这里必须保证设置了环境变量 HADOOP_CLASSPATH
(3)启动 Hadoop 集群,包括 HDFS 和 YARN。

 start-dfs.sh
 start-yarn.sh

分别在 3 台节点服务器查看进程启动情况。
(4)进入 flink的conf 目录,修改 flink-conf.yaml 文件,修改以下配置,这些配置项的含义在进
行 Standalone 模式配置的时候进行过讲解,若在提交命令中不特定指明,这些配置将作为默认
配置。

 cd /opt/module/flink-1.13.0-yarn/conf/
$ vim flink-conf.yaml
jobmanager.memory.process.size: 1600m
taskmanager.memory.process.size: 1728m
taskmanager.numberOfTaskSlots: 8
parallelism.default: 1

flink1.13(一)_第28张图片

3.4.2 会话模式部署(常用)

  YARN 的会话模式与独立集群略有不同,需要首先申请一个 YARN 会话(YARN session)来启动 Flink 集群。具体步骤如下:

  1. 启动集群
    (1)启动 hadoop 集群(HDFS, YARN)。
    (2)执行脚本命令向 YARN 集群申请资源,开启一个 YARN 会话,启动 Flink 集群。 bin/yarn-session.sh -nm test -d
    可用参数解读:
    -d:分离模式,如果你不想让 Flink YARN 客户端一直前台运行,可以使用这个参数,即使关掉当前对话窗口,YARN session 也可以后台运行。
    -jm(–jobManagerMemory):配置 JobManager 所需内存,默认单位 MB。
    -nm(–name):配置在 YARN UI 界面上显示的任务名。
    -qu(–queue):指定 YARN 队列名。
    -tm(–taskManager):配置每个 TaskManager 所使用内存。
      注意:Flink1.11.0 版本不再使用-n 参数和-s 参数分别指定 TaskManager 数量和 slot 数量,
    YARN 会按照需求动态分配 TaskManager 和 slot。所以从这个意义上讲,YARN 的会话模式也
    不会把集群资源固定,同样是动态分配的。
      YARN Session 启动之后会给出一个 web UI 地址以及一个 YARN application ID,如下所示,
    用户可以通过 web UI 或者命令行两种方式提交作业。
    flink1.13(一)_第29张图片
  2. 提交作业
    (1)通过 Web UI 提交作业 这种方式比较简单,与上文所述 Standalone 部署模式基本相同。
    (2)通过命令行提交作业, 将 Standalone 模式讲解中打包好的任务运行 JAR 包上传至集群,执行以下命令将该任务提交到已经开启的 Yarn-Session 中运行。 bin/flink run -c com.atguigu.wc.StreamWordCount FlinkTutorial-1.0-SNAPSHOT.jar
      客户端可以自行确定 JobManager 的地址,也可以通过-m 或者-jobmanager 参数指定
    JobManager 的地址,JobManager 的地址在 YARN Session 的启动页面中可以找到。
    任务提交成功后,可在 YARN 的 Web UI 界面查看运行情况。
    flink1.13(一)_第30张图片
    从图中可以看到我们创建的 Yarn-Session 实际上是一个 Yarn 的Application,并且有唯一的 Application ID。也可以通过 Flink 的 Web UI 页面查看提交任务的运行情况
    flink1.13(一)_第31张图片

3.4.3 单作业模式部署

flink1.13(一)_第32张图片
flink1.13(一)_第33张图片
这里的 application_XXXX_YY 是当前应用的 ID,是作业的 ID。注意如果取消作业,整个 Flink 集群也会停掉。

3.4.4 应用模式部署

flink1.13(一)_第34张图片

3.4.5 高可用

  YARN 模式的高可用和独立模式(Standalone)的高可用原理不一样。
  Standalone 模式中, 同时启动多个 JobManager, 一个为“领导者”(leader),其他为“后备”(standby), 当 leader 挂了, 其他的才会有一个成为 leader。
  而 YARN 的高可用是只启动一个 Jobmanager, 当这个 Jobmanager 挂了之后, YARN 会再次启动一个, 所以其实是利用的 YARN 的重试次数来实现的高可用。
(1)在 yarn-site.xml 中配置。

<property>
 <name>yarn.resourcemanager.am.max-attemptsname>
 <value>4value>
 <description>
 The maximum number of application master execution attempts.
 description>
property>

注意: 配置完不要忘记分发, 和重启 YARN。
(2)在 flink-conf.yaml 中配置。

yarn.application-attempts: 3
high-availability: zookeeper
high-availability.storageDir: hdfs://hadoop102:9820/flink/yarn/ha
high-availability.zookeeper.quorum: 
hadoop102:2181,hadoop103:2181,hadoop104:2181
high-availability.zookeeper.path.root: /flink-yarn

(3)启动 yarn-session。
(4)杀死 JobManager, 查看复活情况。
注意: yarn-site.xml 中配置的是 JobManager 重启次数的上限, flink-conf.xml 中的次数应该小于这个值。

4. Flink运行时架构

4.1 Flink系统架构

4.1.1 整体构成

flink1.13(一)_第35张图片
flink1.13(一)_第36张图片
flink1.13(一)_第37张图片

4.1.2 作业管理器JobManager

flink1.13(一)_第38张图片
flink1.13(一)_第39张图片
flink1.13(一)_第40张图片
flink1.13(一)_第41张图片

4.1.3 任务管理器TaskManager

flink1.13(一)_第42张图片

4.2 Flink作业提交流程

4.2.1 高层级抽象视角

flink1.13(一)_第43张图片
flink1.13(一)_第44张图片

4.2.2 独立模式(Standalone)

flink1.13(一)_第45张图片

4.2.3 YARN 集群

flink1.13(一)_第46张图片
flink1.13(一)_第47张图片
flink1.13(一)_第48张图片
在这里插入图片描述
flink1.13(一)_第49张图片
flink1.13(一)_第50张图片
flink1.13(一)_第51张图片

4.3 一些重要概念

4.3.1 数据流图(DataFlow Graph)

flink1.13(一)_第52张图片
flink1.13(一)_第53张图片
flink1.13(一)_第54张图片
flink1.13(一)_第55张图片

4.3.2 并行度(Parallelism)

flink1.13(一)_第56张图片

flink1.13(一)_第57张图片

flink1.13(一)_第58张图片
flink1.13(一)_第59张图片
flink1.13(一)_第60张图片

4.3.3 算子链(Operator Chain)

flink1.13(一)_第61张图片
flink1.13(一)_第62张图片
flink1.13(一)_第63张图片
在这里插入图片描述
flink1.13(一)_第64张图片
flink1.13(一)_第65张图片

4.3.4 作业图(JobGraph)和执行图(ExecutionGraph)

flink1.13(一)_第66张图片

flink1.13(一)_第67张图片
flink1.13(一)_第68张图片
flink1.13(一)_第69张图片
flink1.13(一)_第70张图片
flink1.13(一)_第71张图片

4.3.5 任务(Task)和任务槽(Task Slots)

flink1.13(一)_第72张图片
flink1.13(一)_第73张图片
flink1.13(一)_第74张图片
flink1.13(一)_第75张图片

5. DataStreamAPI(基础篇)

5.1 执行环境(Execution Environment)

5.1.1 创建执行环境

  1. getExecutionEnvironment
      最简单的方式,就是直接调用 getExecutionEnvironment 方法。它会根据当前运行的上下文直接得到正确的结果:如果程序是独立运行的,就返回一个本地执行环境;如果是创建了 jar包,然后从命令行调用它并提交到集群执行,那么就返回集群的执行环境。也就是说,这个方法会根据当前运行的方式,自行决定该返回什么样的运行环境。
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
      这种“智能”的方式不需要我们额外做判断,用起来简单高效,是最常用的一种创建执行环境的方式.

  2. createLocalEnvironment
      这个方法返回一个本地执行环境。可以在调用时传入一个参数,指定默认的并行度;如果
    不传入,则默认并行度就是本地的 CPU 核心数
    StreamExecutionEnvironment localEnv = StreamExecutionEnvironment.createLocalEnvironment();

  3. createRemoteEnvironment
       这个方法返回集群执行环境。需要在调用时指定 JobManager 的主机名和端口号,并指定
    要在集群中运行的 Jar 包。

StreamExecutionEnvironment remoteEnv = StreamExecutionEnvironment
 			.createRemoteEnvironment(
 				"host", // JobManager 主机名
 				1234, // JobManager 进程端口号
 				"path/to/jarFile.jar" // 提交给 JobManager 的 JAR 包
);

  在获取到程序执行环境后,我们还可以对执行环境进行灵活的设置。比如可以全局设置程
序的并行度、禁用算子链,还可以定义程序的时间语义、配置容错机制。关于时间语义和容错
机制,我们会在后续的章节介绍。

5.1.2 执行模式(Execution Mode)

   上节中我们获取到的执行环境,是一个 StreamExecutionEnvironment,顾名思义它应该是做流处理的。那对于批处理,又应该怎么获取执行环境呢?
  在之前的 Flink 版本中,批处理的执行环境与流处理类似,是调用类 ExecutionEnvironment的静态方法,返回它的对象:

// 批处理环境
ExecutionEnvironment batchEnv = ExecutionEnvironment.getExecutionEnvironment();
// 流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

  基于 ExecutionEnvironment 读入数据创建的数据集合,就是 DataSet;对应的调用的一整套转换方法,就是 DataSet API。这些我们在第二章的批处理 word count 程序中已经有了基本了解。
  而从 1.12.0 版本起,Flink 实现了 API 上的流批统一。DataStream API 新增了一个重要特性:可以支持不同的“执行模式”(execution mode),通过简单的设置就可以让一段 Flink 程序在流处理和批处理之间切换。这样一来,DataSet API 也就没有存在的必要了。

flink1.13(一)_第76张图片

  1. BATCH 模式的配置方法
      由于 Flink 程序默认是 STREAMING 模式,我们这里重点介绍一下 BATCH 模式的配置。
    主要有两种方式:
    (1)通过命令行配置 bin/flink run -Dexecution.runtime-mode=BATCH ... 在提交作业时,增加 execution.runtime-mode 参数,指定值为 BATCH。
    (2)通过代码配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);

  在代码中,直接基于执行环境调用 setRuntimeMode 方法,传入 BATCH 模式。
  建议: 不要在代码中配置,而是使用命令行。这同设置并行度是类似的:在提交作业时指定参数可以更加灵活,同一段应用程序写好之后,既可以用于批处理也可以用于流处理。而在代码中硬编码(hard code)的方式可扩展性比较差,一般都不推荐。

  1. 什么时候选择 BATCH 模式
    flink1.13(一)_第77张图片

5.1.3 触发程序执行

flink1.13(一)_第78张图片

5.2 源算子

flink1.13(一)_第79张图片

5.2.1 准备工作

flink1.13(一)_第80张图片

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
// 链式赋值
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class Event {
    private String user;
    private String url;
    private Long timestamp;
}

flink1.13(一)_第81张图片

5.2.2 从集合/元素/文件中获取数据

最简单的读取数据的方式,就是在代码中直接创建一个 Java 集合,然后调用执行环境的fromCollection 方法进行读取。这相当于将数据临时存储到内存中,形成特殊的数据结构后,作为数据源使用,一般用于测试。

import com.yangjunyi.flink.entity.Event;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import java.util.ArrayList;
import java.util.List;

public class SourceTest {
    public static void main(String[] args) throws Exception {
        // 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 1. 从文件中读取数据
        DataStreamSource<String> stream1 = env.readTextFile("input/clicks.txt");
        // 2.从集合中读取数据
        List<Integer> nums = new ArrayList<Integer>();
        nums.add(2);
        nums.add(5);
        DataStreamSource<Integer> numStream = env.fromCollection(nums);

        ArrayList<Event> events = new ArrayList<>();
        events.add(new Event("Mary","./home",1000L));
        events.add(new Event("Bob","./cart",2000L));
        DataStreamSource<Event> stream2 = env.fromCollection(events);

        // 3. 从元素读取数据
        DataStreamSource<Event> elementStream = env.fromElements(
                new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L)
        );
        stream1.print("stream1");
        numStream.print("numStream");
        stream2.print("stream2");
        elementStream.print("elementStream");
        env.execute();

    }
}

如果要从hdfs中读取数据的话
flink1.13(一)_第82张图片

5.2.3 从socket中读取数据

flink1.13(一)_第83张图片

5.2.4 从Kafka读取数据

flink1.13(一)_第84张图片
flink1.13(一)_第85张图片

<dependency>
 	<groupId>org.apache.flinkgroupId>
	<artifactId>flink-connector-kafka_${scala.binary.version}artifactId>
 	<version>${flink.version}version>
dependency>
    // 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        Properties properties = new Properties();
        // kafka连接节点
        properties.setProperty("bootstrap.servers","192.168.0.75:9092");
        // groupId
        properties.setProperty("group.id","flinktest");
        // 消费者key序列化
        properties.setProperty("key.deserializer","org.apache.kafka.common.serialization.StringSerializer");
        // 消费者value序列化
        properties.setProperty("value.deserializer","org.apache.kafka.common.serialization.StringSerializer");
        // 偏移量设置
        properties.setProperty("auto.offset.reset","latest");
        // 从kafka中读取数据 第一个参数:topic 第二个参数: value序列化模式, 第三个参数:kafka连接properties参数
        DataStreamSource<String> kafkaStream = env.addSource(new FlinkKafkaConsumer<String>("X429Deplane", new SimpleStringSchema(), properties));
        kafkaStream.print();
        env.execute();

5.2.5 自定义Source

flink1.13(一)_第86张图片

public class ClickSource implements SourceFunction<Event> {
    // 声明一个标志位,在下面的run中不停的生成数据, 直到running为false. running什么时候才能置成false呢, 就是在调用cancel方法的时候
    private boolean running = true;
    @Override
    public void run(SourceContext<Event> sourceContext) throws Exception {
        // 随机生成数据
        Random random = new Random();
        // 定义字段选取数据范围
        String[] users = {"Mary", "Alice", "Bob", "Cary"};
        String[] urls = {"./home", "./cart", "./fav", "./prod?id=1", "./prod?id=2"};
        while (running) {
            String user = users[random.nextInt(users.length)];
            String url = urls[random.nextInt(users.length)];
            Long timestamp = Calendar.getInstance().getTimeInMillis();
            sourceContext.collect(new Event(user,url,timestamp));
            Thread.sleep(1000);
        }
    }

    @Override
    public void cancel() {
        this.running = false;
    }
}

link使用自定义source作为源算子

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        DataStreamSource<Event> objectDataStreamSource = env.addSource(new ClickSource());
        objectDataStreamSource.print();
        env.execute();
    }

这里要注意的是 SourceFunction 接口定义的数据源,并行度只能设置为 1,如果数据源设置为大于 1 的并行度,则会抛出异常。如果想要设置并行的数据源, 需要实现ParallelSourceFunction接口

public class ParallelCustomSource implements ParallelSourceFunction<Integer> {

    private boolean running = true;
    Random random = new Random();
    @Override
    public void run(SourceContext<Integer> sourceContext) throws Exception {
        while(running) {
            sourceContext.collect(random.nextInt());
            Thread.sleep(1000);
        }
    }

    @Override
    public void cancel() {
        this.running= false;
    }
}

接下来再把并行度调高就可以了

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        DataStreamSource<Integer> objectDataStreamSource = env.addSource(new ParallelCustomSource()).setParallelism(2);
        objectDataStreamSource.print();
        env.execute();
    }

5.2.6 Flink支持的数据类型

flink1.13(一)_第87张图片
flink1.13(一)_第88张图片
flink1.13(一)_第89张图片
再次强调一下Flink对POJO类的定义, 类必须是公共的(public)和独立的(没有非静态的内部类). 类必须有公共的无参构造方法, 类内部的字段是public且非final的, 或者有公共的getter和setter方法, 且满足Java命名规范
flink1.13(一)_第90张图片
flink1.13(一)_第91张图片
flink1.13(一)_第92张图片

5.3 转换算子(Transformation)

flink1.13(一)_第93张图片

5.3.1 基本转换算子

在基本转换算子后, 如果lambda中使用了泛型, 需要在后面跟一个returns方法

  1. 映射(map)
    flink1.13(一)_第94张图片
public class TransformMapTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
        // 进行map转换计算, 提取user字段
        // 1.使用自定义类实现MapFunction接口
        //SingleOutputStreamOperator map = streamSource.map(new MyMapper());
        // 2.使用匿名类实现MapFunction接口
        /*SingleOutputStreamOperator map = streamSource.map(new MapFunction() {
            @Override
            public String map(Event event) throws Exception {
                return event.getUser();
            }
        });*/
        // 3.使用lambda表达式
        SingleOutputStreamOperator<String> map = streamSource.map(data -> data.getUser());
        map.print();
        env.execute();

    }
    // 自定义mapFunction
    public static class MyMapper implements MapFunction<Event,String>{
        @Override
        public String map(Event event) throws Exception {
            return event.getUser();
        }
    }
}
  1. 过滤(filter)

flink1.13(一)_第95张图片

public class TransformFliterTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
        // 1. 传入一个实现了filterFunction的自定义类对象
        //SingleOutputStreamOperator filter = streamSource.filter(new MyFilter());
        // 2. 传入一个匿名类
        /*SingleOutputStreamOperator filter = streamSource.filter(new FilterFunction() {
            @Override
            public boolean filter(Event event) throws Exception {
                return "Mary".equals(event.getUser());
            }
        });*/
        // 3. 传入lambda表达式
        SingleOutputStreamOperator<Event> filter = streamSource.filter(data -> "Mary".equals(data.getUser()));
        filter.print();
        env.execute();
    }
    public static class MyFilter implements FilterFunction<Event>{
        @Override
        public boolean filter(Event event) throws Exception {
            return "Mary".equals(event.getUser());
        }
    }
}
  1. 扁平映射(flatMap)

flink1.13(一)_第96张图片
flink1.13(一)_第97张图片

public class TransformFlatMapTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
        // 1. 传入一个实现了flatMapFunction的自定义类对象
//        SingleOutputStreamOperator flatMap = streamSource.flatMap(new MyFlatMap());
        // 3. 传入lambda表达式
        SingleOutputStreamOperator<String> flatMap = streamSource.flatMap((Event value, Collector<String> out) -> {
            if("Mary".equals(value.getUser())) {
                out.collect(value.getUser());
            }else if("Bob".equals(value.getUrl())){
                out.collect(value.getUser());
                out.collect(value.getUrl());
                out.collect(value.getTimestamp().toString());
            }
            // 不是Mary和Bob以外的其他数据, 这里不做任何处理, 相当于是被过滤掉了
             // 这里还是会出现Java Lambda的泛型擦除. 因为在Lambda里使用了泛型, 当输出时Java会把泛型擦除, 所以这里需要手动添加return
        }).returns(new TypeHint<String>() {});
        flatMap.print();
        env.execute();
    }
    public static class MyFlatMap implements FlatMapFunction<Event, String> {
        @Override
        public void flatMap(Event event, Collector<String> collector) throws Exception {
            collector.collect(event.getUser());
            collector.collect(event.getUrl());
            collector.collect(event.getTimestamp().toString());
        }
    }
}

如果不加returns方法指定返回类型的话, 这里会报错
在这里插入图片描述

5.3.2 聚合算子(Aggregation)

flink1.13(一)_第98张图片

  1. 按键分区(key by)
    在这里插入图片描述
    flink1.13(一)_第99张图片
    flink1.13(一)_第100张图片
public class AggregationKeyByTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.fromElements(new Event("Mary","./home",1000L),
                new Event("Bob","./cart",2000L),
                new Event("Alice","./prod?id=100",3000L),
                new Event("Bob","./prod?id=200",3400L)
        );
        // 按键分析后进行聚合
        KeyedStream<Event, String> keyedStream = streamSource.keyBy(( event -> event.getUser()));
        SingleOutputStreamOperator<Event> operatorMax = keyedStream.max("timestamp");
        SingleOutputStreamOperator<Event> operatorMaxBy = keyedStream.maxBy("timestamp");
        // max方法除了修改了timestamp的值以外, 对其他的参数还是使用最初的值, 而maxby除了对timestamp修改了以外, 也对其他的值取最新的数据
        operatorMax.print("max");
        operatorMaxBy.print("maxBy");
        env.execute();
    }
}
  1. 简单聚合

flink1.13(一)_第101张图片
在这里插入图片描述
flink1.13(一)_第102张图片

  1. 规约聚合
    flink1.13(一)_第103张图片
    flink1.13(一)_第104张图片
    在这里插入图片描述
public class AggregationReduceTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.fromElements(new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L),
                new Event("Alice", "./prod?id=2", 3200L),
                new Event("Bob", "./prod?id=200", 3400L),
                new Event("Bob", "./home", 3300L),
                new Event("Bob", "./prod?id=2", 3800L),
                new Event("Bob", "./prod?id=3", 4200L)
        );
        // 统计访问量最大的用户和访问量
        // 1. 统计每个用户的访问频次
        // 在这里的map使用匿名内部类的形式不会出现泛型擦除, 如果使用lambda会出现泛型擦除, 所以在转换操作后, 尽量跟一个returns方法
        SingleOutputStreamOperator<Tuple2<String, Long>> reduce = streamSource.map( event -> Tuple2.of(event.getUser(), 1L))
                .returns(new TypeHint<Tuple2<String, Long>>() {})
                .keyBy(data -> data.f0)
                .reduce(new ReduceFunction<Tuple2<String, Long>>() {
                    // 规约时, 第一个参数value1是前面规约后得到的数据, 后面的value2是新进来的数据,
                    // 所以我们要做的操作是,tuple的f0是user,而user名字都是相同的, 因为之前keyBy就是根据名字进行的
                    // tuple的f1是访问次数, 所以需要把value1的数字和value2的数字相加, 就是之前规约得到的数字加上新进来的数据的数字
                    @Override
                    public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
                        return Tuple2.of(value1.f0, value1.f1 + value2.f1);
                    }
                });
        // 2. 根据当前的个数, 选取当前最活跃的用户
        // 这里keyBy给定了一个写死的字符串, 代表所有的数据都分配到相同的key中, 都会被分配到同一个分区
        reduce.keyBy(data -> "key")
                .reduce(new ReduceFunction<Tuple2<String, Long>>() {
                    @Override
                    public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
                        // value1的数量和value2的数量谁大就返回谁
                        return value1.f1 > value2.f1 ? value1 : value2;
                    }
                }).print();

        env.execute();
    }

}

5.3.3 用户自定义函数(UDF)

flink1.13(一)_第105张图片

  1. 函数类
    flink1.13(一)_第106张图片
  2. 匿名函数(Lambda)
    在这里插入图片描述
    flink1.13(一)_第107张图片
    flink1.13(一)_第108张图片
    flink1.13(一)_第109张图片
    这些方法对于其它泛型擦除的场景同样适用。
  3. 富函数类(Rich Function Classes)
    flink1.13(一)_第110张图片
    flink1.13(一)_第111张图片
public class TransformRichFunction {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.fromElements(new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L),
                new Event("Alice", "./prod?id=2", 3200L),
                new Event("Bob", "./prod?id=200", 3400L),
                new Event("Bob", "./home", 3300L),
                new Event("Bob", "./prod?id=2", 3800L),
                new Event("Bob", "./prod?id=3", 4200L)
        );
        streamSource.map(new MyRichMapFunction()).print();

        env.execute();
    }
    public static class MyRichMapFunction extends RichMapFunction<Event,Integer>{
        @Override
        public void open(Configuration parameters) throws Exception {
            super.open(parameters);
            System.out.println("open生命周期" + getRuntimeContext().getIndexOfThisSubtask() + "号任务");
        }

        @Override
        public void close() throws Exception {
            super.close();
            System.out.println("close生命周期" + getRuntimeContext().getIndexOfThisSubtask() + "号任务结束");

        }

        @Override
        public Integer map(Event event) throws Exception {
            return event.getUrl().length();
        }
    }
}

5.3.4 物理分区(Physical Partitioning)

flink1.13(一)_第112张图片
flink1.13(一)_第113张图片
在这里插入图片描述
flink1.13(一)_第114张图片

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.fromElements(new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L),
                new Event("Alice", "./prod?id=2", 3200L),
                new Event("Bob", "./prod?id=200", 3400L),
                new Event("Bob", "./home", 3300L),
                new Event("Bob", "./prod?id=2", 3800L),
                new Event("Bob", "./prod?id=3", 4200L)
        );
        streamSource.shuffle().print().setParallelism(4);
        env.execute();

flink1.13(一)_第115张图片
整体来看没有什么规律, 在数据量很多的情况下, 会大概率变成均匀分布

flink1.13(一)_第116张图片
streamSource.rebalance().print().setParallelism(4);
flink1.13(一)_第117张图片
可以看到8条数据被均匀的分给了四个并行算子上,每个slot上都输出了2条数据, 如果不指定物理分区操作, Flink底层默认会使用rebalance分区策略

flink1.13(一)_第118张图片
flink1.13(一)_第119张图片
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
flink1.13(一)_第120张图片

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 从自定义流中读取数据
        env.fromElements(1,2,3,4,5,6,7,8)
                .partitionCustom(new Partitioner<Integer>() {
                    @Override
                    public int partition(Integer o, int i) {
                        return  o % 2;
                    }
                }, new KeySelector<Integer, Integer>() {
                    @Override
                    public Integer getKey(Integer integer) throws Exception {
                        return integer;
                    }
                }).print().setParallelism(4);
        // 自定义重分区
        env.execute();

5.4 输出算子(Sink)

flink1.13(一)_第121张图片

5.4.1 连接到外部系统

flink1.13(一)_第122张图片
flink1.13(一)_第123张图片
flink1.13(一)_第124张图片
flink1.13(一)_第125张图片
flink1.13(一)_第126张图片

5.4.2 输出到文件

flink1.13(一)_第127张图片
flink1.13(一)_第128张图片

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(4);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.fromElements(new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L),
                new Event("Alice", "./prod?id=2", 3200L),
                new Event("Bob", "./prod?id=200", 3400L),
                new Event("Bob", "./home", 3300L),
                new Event("Bob", "./prod?id=2", 3800L),
                new Event("Bob", "./prod?id=3", 4200L)
        );
        StreamingFileSink<String> streamingFileSink = StreamingFileSink.<String>forRowFormat(new Path("./output"),
                new SimpleStringEncoder<>("UTF-8"))
                // 设置文件归档策略, 使用默认的滚动归档策略即可
                .withRollingPolicy(DefaultRollingPolicy
                        .builder()
                        // 文件的最大大小单位字节 此处为1G
                        .withMaxPartSize(1024*1024*1024)
                        // 设置每隔多长时间进行归档 单位ms 此处为15分钟
                        .withRolloverInterval(TimeUnit.MINUTES.toMillis(15))
                        // 设置无操作(不活跃)后多长时间进行归档 此处为5分钟
                        .withInactivityInterval(TimeUnit.MINUTES.toMillis(5))
                        .build())
                .build();
        // 因为streamSource的泛型为Event, 我们这里要转换成String进行归档写入文件, 所以要加一个map转换的操作
        streamSource.map(data -> data.toString()).addSink(streamingFileSink);
        env.execute();
    }

flink1.13(一)_第129张图片
因为设置了4个并行子任务来执行写入, 而且还设置了滚动策略. 所以输出路径如下图所示
flink1.13(一)_第130张图片

5.4.3 输出到Kafka

在这里插入图片描述
在这里插入图片描述

flink1.13(一)_第131张图片

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        // 读取Kafka数据
        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers","hadoop102:9092");
        DataStreamSource<String> kafkaStream = env.addSource(new FlinkKafkaConsumer<String>("clicks", new SimpleStringSchema(), properties));

        // 2. 用flink进行转换处理
        SingleOutputStreamOperator<String> map = kafkaStream.map(new MapFunction<String, String>() {
            @Override
            public String map(String s) throws Exception {
                String[] fields = s.split(",");
                return new Event(fields[0].trim(), fields[1].trim(), Long.valueOf(fields[2].trim())).toString();
            }
        });
        // 3.输出到kafka
        map.addSink(new FlinkKafkaProducer<String>("hadoop102:9092","events",new SimpleStringSchema()));
        env.execute();
    }

5.4.4 输出到Redis

flink1.13(一)_第132张图片

  1. 导入redis连接器依赖
<dependency>
 	<groupId>org.apache.bahirgroupId>
 	<artifactId>flink-connector-redis_2.11artifactId>
 	<version>1.0version>
dependency>

flink1.13(一)_第133张图片

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        DataStreamSource<Event> stream = env.addSource(new ClickSource());

        // 创建一个jedis连接配置
        FlinkJedisPoolConfig config = new FlinkJedisPoolConfig.Builder()
                .setHost("hadoop102")
                .build();
        stream.addSink(new RedisSink<>(config, new MyRedisMapper()));
        env.execute();

flink1.13(一)_第134张图片

 // 自定义类实现RedisMapper接口
    public static class MyRedisMapper implements RedisMapper<Event> {
        @Override
        public RedisCommandDescription getCommandDescription() {
            return new RedisCommandDescription(RedisCommand.HSET,"clicks");
        }

        @Override
        public String getKeyFromData(Event event) {
            return event.getUser();
        }

        @Override
        public String getValueFromData(Event event) {
            return event.getUrl();
        }
    }

flink1.13(一)_第135张图片

5.4.5 输出到ElasticSearch

flink1.13(一)_第136张图片

  1. 添加ElasticSearch连接器依赖
<dependency>
 	<groupId>org.apache.flinkgroupId>
 	<artifactId>flink-connector-elasticsearch7_${scala.binary.version}artifactId>
 	<version>${flink.version}version>
dependency>

在这里插入图片描述

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.fromElements(new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L),
                new Event("Alice", "./prod?id=2", 3200L),
                new Event("Bob", "./prod?id=200", 3400L),
                new Event("Bob", "./home", 3300L),
                new Event("Bob", "./prod?id=2", 3800L),
                new Event("Bob", "./prod?id=3", 4200L)
        );
        // 定义hosts的列表
        ArrayList<HttpHost> httpHosts = new ArrayList<>();
        httpHosts.add(new HttpHost("hadoop102",9200));
        // 定义ElasticSearchSinkFunction 处理数据
        ElasticsearchSinkFunction<Event> elasticsearchSinkFunction = new ElasticsearchSinkFunction<Event>() {
            @Override
            public void process(Event event, RuntimeContext runtimeContext, RequestIndexer requestIndexer) {
                HashMap<String, String> map = new HashMap<>();
                map.put(event.getUser(),event.getUrl());

                // 构建一个IndexRequest
                IndexRequest clicks = Requests.indexRequest()
                        .index("clicks")
                        .source(map);
                
                requestIndexer.add(clicks);
            }
        };
        streamSource.addSink(new ElasticsearchSink.Builder<>(httpHosts,elasticsearchSinkFunction ).build());
        env.execute();
    }

flink1.13(一)_第137张图片

5.4.6 输出到MYSQL(jdbc)

flink1.13(一)_第138张图片

<dependency>
 	<groupId>org.apache.flink</groupId>
 	<artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId>
 	<version>${flink.version}</version>
</dependency>
<dependency>
 	<groupId>mysql</groupId>
 	<artifactId>mysql-connector-java</artifactId>
 	<version>5.1.47</version>
</dependency>

flink1.13(一)_第139张图片

flink1.13(一)_第140张图片

5.4.7 自定义Sink输出如HBase

flink1.13(一)_第141张图片
flink1.13(一)_第142张图片

<dependency>
 	<groupId>org.apache.hbasegroupId>
 	<artifactId>hbase-clientartifactId>
 	<version>${hbase.version}version>
dependency>

(2)编写输出到 HBase 的示例代码

package com.yangjunyi.flink.sink;

package com.yangjunyi.flink.sink;

import com.yangjunyi.flink.entity.Event;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.jdbc.JdbcConnectionOptions;
import org.apache.flink.connector.jdbc.JdbcSink;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Table;

import java.nio.charset.StandardCharsets;
import java.sql.PreparedStatement;

public class SinkToHBase {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env =
                StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.fromElements("hello", "world")
                .addSink(
                        new RichSinkFunction<String>() {
                            public org.apache.hadoop.conf.Configuration configuration; // 管理 Hbase 的配置信息,这里因为 Configuration 的重名问题,将类以完整路径导入
                            public Connection connection; // 管理 Hbase 连接
                            @Override
                            public void open(Configuration parameters) throws Exception {
                                super.open(parameters);
                                configuration = HBaseConfiguration.create();
                                configuration.set("hbase.zookeeper.quorum", "hadoop102:2181");
                                connection = ConnectionFactory.createConnection(configuration);
                            }

                            @Override
                            public void invoke(String value, Context context) throws Exception {
                                Table table = connection.getTable(TableName.valueOf("test")); // 表名为 test
                                Put put = new Put("rowkey".getBytes(StandardCharsets.UTF_8)); // 指定 rowkey
                                put.addColumn("info".getBytes(StandardCharsets.UTF_8) // 指定列名
                                        , value.getBytes(StandardCharsets.UTF_8) // 写入的数据
                                        , "1".getBytes(StandardCharsets.UTF_8)); // 写入的数据
                                table.put(put); // 执行 put 操作
                                table.close(); // 将表关闭
                            }
                            @Override
                            public void close() throws Exception {
                                super.close();
                                connection.close(); // 关闭连接
                            }
                        }
                );
        env.execute();
    }
}



public class SinkToHBase {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env =
                StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.fromElements("hello", "world")
                .addSink(
                        new RichSinkFunction<String>() {
                            public org.apache.hadoop.conf.Configuration configuration; // 管理 Hbase 的配置信息,这里因为 Configuration 的重名问题,将类以完整路径导入
                            public Connection connection; // 管理 Hbase 连接
                            @Override
                            public void open(Configuration parameters) throws Exception {
                                super.open(parameters);
                                configuration = HBaseConfiguration.create();
                                configuration.set("hbase.zookeeper.quorum", "hadoop102:2181");
                                connection = ConnectionFactory.createConnection(configuration);
                            }

                            @Override
                            public void invoke(String value, Context context) throws Exception {
                                Table table = connection.getTable(TableName.valueOf("test")); // 表名为 test
                                Put put = new Put("rowkey".getBytes(StandardCharsets.UTF_8)); // 指定 rowkey
                                put.addColumn("info".getBytes(StandardCharsets.UTF_8) // 指定列名
                                        , value.getBytes(StandardCharsets.UTF_8) // 写入的数据
                                        , "1".getBytes(StandardCharsets.UTF_8)); // 写入的数据
                                table.put(put); // 执行 put 操作
                                table.close(); // 将表关闭
                            }
                            @Override
                            public void close() throws Exception {
                                super.close();
                                connection.close(); // 关闭连接
                            }
                        }
                );
        env.execute();
    }
}

6.Flink中的时间和窗口

6.1 时间语义

6.1.1 Flink中的时间语义

flink1.13(一)_第143张图片
flink1.13(一)_第144张图片
flink1.13(一)_第145张图片
flink1.13(一)_第146张图片
flink1.13(一)_第147张图片

6.1.2 哪种时间语义更重要

flink1.13(一)_第148张图片
flink1.13(一)_第149张图片
flink1.13(一)_第150张图片

6.2 水位线

6.2.1 事件时间和窗口

在这里插入图片描述
flink1.13(一)_第151张图片
flink1.13(一)_第152张图片
flink1.13(一)_第153张图片
flink1.13(一)_第154张图片
flink1.13(一)_第155张图片

6.2.2 什么是水位线

flink1.13(一)_第156张图片
flink1.13(一)_第157张图片
flink1.13(一)_第158张图片
flink1.13(一)_第159张图片
flink1.13(一)_第160张图片

6.2.3 如何生成水位线

flink1.13(一)_第161张图片
flink1.13(一)_第162张图片
flink1.13(一)_第163张图片
flink1.13(一)_第164张图片
flink1.13(一)_第165张图片
flink1.13(一)_第166张图片
flink1.13(一)_第167张图片
水位线的周期性生成间隔时间设置env.getConfig().setAutoWatermarkInterval(100L) 单位是ms表示每隔100ms生成一次水位线, 默认值是200ms
flink1.13(一)_第168张图片
flink1.13(一)_第169张图片

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 设置水位线自动生成时间间隔为100ms 默认值是200ms
        env.getConfig().setAutoWatermarkInterval(100L);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.fromElements(new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L),
                new Event("Alice", "./prod?id=2", 3200L),
                new Event("Bob", "./prod?id=200", 3400L),
                new Event("Bob", "./home", 3300L),
                new Event("Bob", "./prod?id=2", 3800L),
                new Event("Bob", "./prod?id=3", 4200L)
        );
        // 水位线生成策略建议离source算子越近越好, 因为在source处设置了水位线, 后续的其他算子都可以用到
        // 有序流额watermark生成,有序流就是基于自增长的时间戳作为水位线即可
        SingleOutputStreamOperator<Event> stream1 = streamSource.assignTimestampsAndWatermarks(
                WatermarkStrategy.<Event>forMonotonousTimestamps()
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long recordTimestamp) {
                                return event.getTimestamp();
                            }
                        })
        );

        // 乱序流watermark生成
        SingleOutputStreamOperator<Event> stream2 = streamSource.assignTimestampsAndWatermarks(
                // 设置水位线延时2秒
                WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2L))
                        // 设置水位线基于时间戳数据
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long recordTimestamp) {
                                return event.getTimestamp();
                            }
                        })
        );

        env.execute();
    }

flink1.13(一)_第170张图片

flink1.13(一)_第171张图片

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(new CustomWatermarkStrategy())
                .print();
        env.execute();
    }

    public static class CustomWatermarkStrategy implements WatermarkStrategy<Event> {
        @Override
        public TimestampAssigner<Event> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
            return new SerializableTimestampAssigner<Event>() {
                @Override
                public long extractTimestamp(Event element, long recordTimestamp) {
                    return element.getTimestamp(); // 告诉程序数据源里的时间戳是哪一个字段
                }
            };
        }

        @Override
        public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            return new CustomPeriodicGenerator();
        }
    }

    public static class CustomPeriodicGenerator implements WatermarkGenerator<Event> {
        private Long delayTime = 5000L; // 延迟时间
        private Long maxTs = Long.MIN_VALUE + delayTime + 1L; // 观察到的最大时间戳

        @Override
        public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) {
            // 每来一条数据就调用一次
            maxTs = Math.max(event.getTimestamp(), maxTs); // 更新最大时间戳
        }

        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            // 发射水位线,默认 200ms 调用一次
            output.emitWatermark(new Watermark(maxTs - delayTime - 1L));
        }
    }

flink1.13(一)_第172张图片

public class CustomPunctuatedGenerator implements WatermarkGenerator<Event> {
 	@Override
 	public void onEvent(Event r, long eventTimestamp, WatermarkOutput output) {
		// 只有在遇到特定的 itemId 时,才发出水位线
 		if (r.user.equals("Mary")) {
 			output.emitWatermark(new Watermark(r.timestamp - 1));
 		}
 }
 	@Override
 	public void onPeriodicEmit(WatermarkOutput output) {
 	// 不需要做任何事情,因为我们在 onEvent 方法中发射了水位线
 	}
}

在这里插入图片描述

6.2.4 水位线的传递

flink1.13(一)_第173张图片
flink1.13(一)_第174张图片
flink1.13(一)_第175张图片

6.3 窗口

6.3.1 窗口的概念

flink1.13(一)_第176张图片
flink1.13(一)_第177张图片
flink1.13(一)_第178张图片
flink1.13(一)_第179张图片
flink1.13(一)_第180张图片
flink1.13(一)_第181张图片

6.3.2 窗口的分类

  1. 按照驱动类型分类
    flink1.13(一)_第182张图片
    (1)时间窗口(Time Window)
    flink1.13(一)_第183张图片
    flink1.13(一)_第184张图片
    (2)计数窗口(Count Window)
    flink1.13(一)_第185张图片
  2. 按照窗口分配数据的规则分类
    flink1.13(一)_第186张图片
    (1)滚动窗口(Tumbling Windows)
    flink1.13(一)_第187张图片
    flink1.13(一)_第188张图片
    flink1.13(一)_第189张图片
    (2)滑动窗口(Sliding Windows)
    flink1.13(一)_第190张图片
    flink1.13(一)_第191张图片
    flink1.13(一)_第192张图片
    (3)会话窗口(Session Windows)
    flink1.13(一)_第193张图片
    flink1.13(一)_第194张图片
    flink1.13(一)_第195张图片
    flink1.13(一)_第196张图片
    (4)全局窗口(Global Windows)
    flink1.13(一)_第197张图片
    flink1.13(一)_第198张图片

6.3.3 窗口API概览

flink1.13(一)_第199张图片
在这里插入图片描述
flink1.13(一)_第200张图片

6.3.4 窗口分配器(Window Assigners)

flink1.13(一)_第201张图片

  1. 时间窗口
    flink1.13(一)_第202张图片
    flink1.13(一)_第203张图片
    flink1.13(一)_第204张图片
    flink1.13(一)_第205张图片
    flink1.13(一)_第206张图片
    flink1.13(一)_第207张图片
    flink1.13(一)_第208张图片
  2. 计数窗口
    flink1.13(一)_第209张图片
    flink1.13(一)_第210张图片
  3. 全局窗口
    flink1.13(一)_第211张图片
public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 设置水位线自动生成时间间隔为100ms 默认值是200ms
        env.getConfig().setAutoWatermarkInterval(100L);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.fromElements(new Event("Mary", "./home", 1000L),
                new Event("Bob", "./cart", 2000L),
                new Event("Alice", "./prod?id=100", 3000L),
                new Event("Alice", "./prod?id=2", 3200L),
                new Event("Bob", "./prod?id=200", 3400L),
                new Event("Bob", "./home", 3300L),
                new Event("Bob", "./prod?id=2", 3800L),
                new Event("Bob", "./prod?id=3", 4200L)
        );
        // 乱序流watermark生成
        SingleOutputStreamOperator<Event> stream = streamSource.assignTimestampsAndWatermarks(
                // 设置水位线延时2秒
                WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2L))
                        // 设置水位线基于时间戳数据
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long recordTimestamp) {
                                return event.getTimestamp();
                            }
                        })
        );
        // 注意开窗前必须指定水位线
        stream.keyBy(data -> data.getUser())
                // 滚动事件时间窗口, 指定事件时间1小时的窗口
                //.window(TumblingEventTimeWindows.of(Time.hours(1)))
                // 时间窗口都有一个参数是offset偏移量,我们时区为东8区, 代码获取的时间应该减去8小时才是我们的时间
                // 这样才可以得到北京时间每天 0 点开启的滚动窗口
                //.window(TumblingEventTimeWindows.of(Time.days(1),Time.hours(-8)))
                // 滑动事件时间窗口, 指定事件时间窗口大小为1小时, 每5分钟滑动一次
                //.window(SlidingEventTimeWindows.of(Time.hours(1),Time.minutes(5)))
                // 事件时间会话窗口
                //.window(EventTimeSessionWindows.withGap(Time.seconds(2)))
                // countWindow计数窗口, 一个参数的就是滚动窗口, 2个参数的就是滑动窗口
                .countWindow(10L);
        // 注意所有的开窗都不是一个完整的算子, 只能算是一个中间步骤返回值是WindowedStream, 只有开窗函数+窗口函数完成后才返回的是完整的算子


        env.execute();
    }

6.3.5窗口函数

flink1.13(一)_第212张图片
对WindowStream进行窗口函数后, 才能得到一个DataStream
flink1.13(一)_第213张图片

  1. 增量聚合函数(incremental aggregation functions)流处理思路增量的计算
    flink1.13(一)_第214张图片
    (1)归约函数(ReduceFunction)
    flink1.13(一)_第215张图片
public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 设置水位线自动生成时间间隔为100ms 默认值是200ms
        env.getConfig().setAutoWatermarkInterval(100L);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
        // 乱序流watermark生成
        SingleOutputStreamOperator<Event> stream = streamSource.assignTimestampsAndWatermarks(
                // 设置水位线延时2秒
                WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2L))
                        // 设置水位线基于时间戳数据
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long recordTimestamp) {
                                return event.getTimestamp();
                            }
                        })
        );
        // 把点击数据转换成二元组 用户名和点击次数1次
        stream.map(data -> Tuple2.of(data.getUser(), 1L))
                // 指定返回值二元组
                .returns(new TypeHint<Tuple2<String, Long>>() {})
                // 对user进行分组
                .keyBy(data -> data.f0)
                // 开窗函数 指定滚动事件时间窗口, 窗口大小为10秒
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                // 窗口函数 增量聚合函数 实现规约操作, 把每个人的点击数字相加, 得到每个人最终的点击次数
                .reduce(new ReduceFunction<Tuple2<String, Long>>() {
                    @Override
                    public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
                        return Tuple2.of(value1.f0,value1.f1 + value2.f1);
                    }
                }).print();
        env.execute();
    }

flink1.13(一)_第216张图片
(2)聚合函数(AggregateFunction)重要
flink1.13(一)_第217张图片
flink1.13(一)_第218张图片
flink1.13(一)_第219张图片
flink1.13(一)_第220张图片

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);
        // 设置水位线自动生成时间间隔为100ms 默认值是200ms
        env.getConfig().setAutoWatermarkInterval(100L);
        // 从自定义流中读取数据
        DataStreamSource<Event> streamSource = env.addSource(new ClickSource());
        // 乱序流watermark生成
        SingleOutputStreamOperator<Event> stream = streamSource.assignTimestampsAndWatermarks(
                // 设置水位线延时2秒
                WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2L))
                        // 设置水位线基于时间戳数据
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long recordTimestamp) {
                                return event.getTimestamp();
                            }
                        })
        );
        // keyBy
        stream.keyBy(date -> date.getUser())
                // 开窗
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                // 三个参数, 第一个参数输入对象,
                // 第二个参数累加器我们这里要求timestamp的平均, 所以一个是Long类型的timestamp的和  另一个是count,最后保存user
                // 第三个参数,以什么样的形式返回, 我们需要user,平均数 这样的二元组返回
                .aggregate(new AggregateFunction<Event, Tuple3<Long,Integer,String>, Tuple2<String,String>>() {
                    @Override
                    public Tuple3<Long, Integer,String> createAccumulator() {
                        // 创建一个累加器, 这里给一个初始值就行
                        return Tuple3.of(0L,0,"");
                    }

                    @Override
                    public Tuple3<Long, Integer,String> add(Event event, Tuple3<Long, Integer,String> accumulator) {
                        // 每来一个数据就要调用当前add方法, event当前到来的数据, accumulator当前累加器的状态
                        // 累加器add时, 我们需要把timestamp累加, 并把count + 1
                        return Tuple3.of(accumulator.f0 + event.getTimestamp(),accumulator.f1 + 1,event.getUser());
                    }

                    @Override
                    public Tuple2<String, String> getResult(Tuple3<Long, Integer,String> accumulator) {
                        // 获取最终结果, 把当前累加器中得到的数据计算成想要的结果
                        return Tuple2.of(accumulator.f2,new Timestamp(accumulator.f0/accumulator.f1).toString());
                    }

                    @Override
                    public Tuple3<Long, Integer,String> merge(Tuple3<Long, Integer,String> accumulator1, Tuple3<Long, Integer,String> accumulator2) {
                        // 合并两个累加器, 在会话窗口有实际意义, 这里是滚动窗口, 不实现也可以
                        return Tuple3.of(accumulator1.f0+accumulator2.f0, accumulator2.f1 + accumulator2.f1, accumulator1.f2);
                    }
                }).print();
        env.execute();
    }
public static void main(String[] args) throws Exception {
        // 开窗统计pv和uv, 两者相除得到平均用户活跃度
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 设置全局并行度
        env.setParallelism(1);
        // 设置自动水位线生成时间间隔为100ms
        env.getConfig().setAutoWatermarkInterval(100);
        DataStreamSource<Event> stream = env.addSource(new ClickSource());
        // 设置串行流的时间戳
        SingleOutputStreamOperator<Event> operator = stream.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                    @Override
                    public long extractTimestamp(Event event, long recordTime) {
                        return event.getTimestamp();
                    }
                }));
        operator.print();
        // 所有数据放在一起统计
        operator.keyBy(data -> true)
                // 设置一个基于事件时间的滑动窗口, 窗口大小为10s 偏移量为2s , 每隔2s统计一次上10s的数据情况
                .window(SlidingEventTimeWindows.of(Time.seconds(10),Time.seconds(2)))
                .aggregate(new MyAggregate())
                .print();

        env.execute();
    }
    // 自定义一个AggregateFunction, 用long保存pv个数, 用hashset做uv去重
    public static class MyAggregate implements AggregateFunction<Event, Tuple2<Long, HashSet<String>>,Double>{

        @Override
        public Tuple2<Long, HashSet<String>> createAccumulator() {
            return Tuple2.of(0L,new HashSet<>());
        }

        @Override
        public Tuple2<Long, HashSet<String>> add(Event event, Tuple2<Long, HashSet<String>> accumulator) {
            // 每来一条数据, pv个数+1 将user放入HashSet中
            accumulator.f1.add(event.getUser());
            return Tuple2.of(accumulator.f0 + 1,accumulator.f1);
        }

        @Override
        public Double getResult(Tuple2<Long, HashSet<String>> accumulator) {
            // 窗口结束时., 输出pv和uv的比值, 这里有可能会有除0的风险, 实际开发中需要判断
            return (double)accumulator.f0/accumulator.f1.size();
        }

        @Override
        public Tuple2<Long, HashSet<String>> merge(Tuple2<Long, HashSet<String>> longHashSetTuple2, Tuple2<Long, HashSet<String>> acc1) {
            return null;
        }
    }

flink1.13(一)_第221张图片
在这里插入图片描述

  1. 全窗口函数(full window functions)批处理思路攒一波最后计算
    flink1.13(一)_第222张图片
    (1)窗口函数(WindowFunction)不推荐使用
    flink1.13(一)_第223张图片
    flink1.13(一)_第224张图片
    (2)处理窗口函数(ProcessWindowFunction)推荐使用
    flink1.13(一)_第225张图片
    flink1.13(一)_第226张图片
public static void main(String[] args) throws Exception {
        // 开窗统计pv和uv, 两者相除得到平均用户活跃度
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 设置全局并行度
        env.setParallelism(1);
        // 设置自动水位线生成时间间隔为100ms
        env.getConfig().setAutoWatermarkInterval(100);
        DataStreamSource<Event> stream = env.addSource(new ClickSource());
        // 设置串行流的时间戳
        SingleOutputStreamOperator<Event> operator = stream.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                    @Override
                    public long extractTimestamp(Event event, long recordTime) {
                        return event.getTimestamp();
                    }
                }));
        operator.print();
        // 使用processWindowFunction计算uv
        operator.keyBy(data -> true)
                // 设置一个基于事件时间的滑动窗口, 窗口大小为10s 偏移量为2s , 每隔2s统计一次上10s的数据情况
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .process(new MyProcessWindowFunction())
                .print();

        env.execute();
    }

    public static class MyProcessWindowFunction extends ProcessWindowFunction<Event, String,Boolean, TimeWindow> {
        @Override
        public void process(Boolean key, Context context, Iterable<Event> in, Collector<String> out) throws Exception {
            // 用一个HashSet保存user
            HashSet<String> userSet = new HashSet<>();
            // 从in中遍历数据, 放入userSet
            for (Event event : in) {
                userSet.add(event.getUser());
            }
            Long uv = (long) userSet.size();
            // 结合窗口信息输出
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            out.collect("窗口"+new Timestamp(start) + "~" + new Timestamp(end) + "uv值为" + uv );
        }
    }

flink1.13(一)_第227张图片

  1. 增量聚合和全窗口函数的结合使用
    flink1.13(一)_第228张图片
    在aggregate和processWindowFunction一起使用时, aggregate的返回值会传递processWindowFunction的输入Iterable中
public static void main(String[] args) throws Exception {
        // 开窗统计pv和uv, 两者相除得到平均用户活跃度
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 设置全局并行度
        env.setParallelism(1);
        // 设置自动水位线生成时间间隔为100ms
        env.getConfig().setAutoWatermarkInterval(100);
        DataStreamSource<Event> stream = env.addSource(new ClickSource());
        // 设置串行流的时间戳
        SingleOutputStreamOperator<Event> operator = stream.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                    @Override
                    public long extractTimestamp(Event event, long recordTime) {
                        return event.getTimestamp();
                    }
                }));
        operator.print();
        // 使用aggregate和processWindowFunction结合计算uv
        operator.keyBy(data -> true)
                // 设置一个基于事件时间的滑动窗口, 窗口大小为10s 偏移量为2s , 每隔2s统计一次上10s的数据情况
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .aggregate(new MyAggregate(),new MyProcessWindowFunction())
                .print();

        env.execute();
    }
    // 自定义实现processWindowFunction, 包装窗口信息输出
    public static class MyProcessWindowFunction extends ProcessWindowFunction<Integer, String,Boolean, TimeWindow> {
        @Override
        public void process(Boolean key, Context context, Iterable<Integer> in, Collector<String> out) throws Exception {
            Integer uv =in.iterator().next();
            // 结合窗口信息输出
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            out.collect("窗口"+new Timestamp(start) + "~" + new Timestamp(end) + "uv值为" + uv );
        }
    }
    // 自定义一个AggregateFunction, 增量聚合计算uv值
    public static class MyAggregate implements AggregateFunction<Event,  HashSet<String>,Integer> {
        @Override
        public HashSet<String> createAccumulator() {
            return new HashSet<>();
        }

        @Override
        public HashSet<String> add(Event event, HashSet<String> accumulator) {
            accumulator.add(event.getUser());
            return accumulator;
        }

        @Override
        public Integer getResult(HashSet<String> accumulator) {
            return accumulator.size();
        }

        @Override
        public HashSet<String> merge(HashSet<String> strings, HashSet<String> acc1) {
            return null;
        }
    }

flink1.13(一)_第229张图片
flink1.13(一)_第230张图片

public static void main(String[] args) throws Exception {
        // 开窗统计pv和uv, 两者相除得到平均用户活跃度
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 设置全局并行度
        env.setParallelism(1);
        // 设置自动水位线生成时间间隔为100ms
        env.getConfig().setAutoWatermarkInterval(100);
        DataStreamSource<Event> stream = env.addSource(new ClickSource());
        // 设置串行流的时间戳
        SingleOutputStreamOperator<Event> operator = stream.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                    @Override
                    public long extractTimestamp(Event event, long recordTime) {
                        return event.getTimestamp();
                    }
                }));
        operator.print();
        // 统计每个url的访问量
        operator.keyBy(data -> data.getUrl())
                // 设置一个基于事件时间的滑动窗口, 窗口大小为10s 偏移量为2s , 每隔2s统计一次上10s的数据情况
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .aggregate(new UrlViewCountAgg(),new UrlViewCountResult())
                .print();

        env.execute();
    }
    // 泛型 Long是输入是aggregate的结果, UrlViewCount是输出,我们需要输出一个自定义类,
    // String是key的类型, 因为是根据url进行keyBy的, 所以是String类型, TimeWindow是当前开窗类型, 按时间进行开窗
    public static class UrlViewCountResult extends ProcessWindowFunction<Long, UrlViewCount,String, TimeWindow> {
        @Override
        public void process(String key, Context context, Iterable<Long> in, Collector<UrlViewCount> out) throws Exception {
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            out.collect(new UrlViewCount(key,in.iterator().next(),start,end));
        }
    }
    // 泛型Event对象 累加器计算个数所以是Long 结果是Long类型
    public static class UrlViewCountAgg implements AggregateFunction<Event, Long,Long> {
        @Override
        public Long createAccumulator() {
            return 0L;
        }

        @Override
        public Long add(Event event, Long accumulator) {
            return accumulator + 1;
        }

        @Override
        public Long getResult(Long accumulator) {
            return accumulator;
        }

        @Override
        public Long merge(Long aLong, Long acc1) {
            return null;
        }
    }
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UrlViewCount {
    private String url;
    private Long count;
    private Long windowStart;
    private Long windowEnd;

    @Override
    public String toString() {
        return "UrlViewCount{" +
                "url='" + url + '\'' +
                ", count=" + count +
                ", windowStart=" + new Timestamp(windowStart) +
                ", windowEnd=" + new Timestamp(windowEnd) +
                '}';
    }
}

flink1.13(一)_第231张图片

6.3.6 其他API

flink1.13(一)_第232张图片
flink1.13(一)_第233张图片
flink1.13(一)_第234张图片
flink1.13(一)_第235张图片
flink1.13(一)_第236张图片
flink1.13(一)_第237张图片
flink1.13(一)_第238张图片
flink1.13(一)_第239张图片
flink1.13(一)_第240张图片
在这里插入图片描述

6.3.7 Flink处理迟到数据

方式1: 设置 watermark 延迟时间,2 秒钟
方式2: 允许窗口处理迟到数据,设置 1 分钟的等待时间
方式3: 定义侧输出流并将最后的迟到数据输出到侧输出流

public static void main(String[] args) throws Exception {
        // 开窗统计pv和uv, 两者相除得到平均用户活跃度
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 设置全局并行度
        env.setParallelism(1);
        // 设置自动水位线生成时间间隔为100ms
        env.getConfig().setAutoWatermarkInterval(100);
        DataStreamSource<String> stream = env.socketTextStream("hadoop102", 7777);
        SingleOutputStreamOperator<Event> operator = stream.map(new MapFunction<String, Event>() {
            @Override
            public Event map(String event) throws Exception {
                String[] split = event.split(",");
                return new Event(split[0].trim(), split[1].trim(), Long.valueOf(split[2].trim()));
            }
        });

        // 设置无序流的时间戳并给定waterMarks的延迟时间为2秒
        // 处理迟到数据方式一:设置 watermark 延迟时间,2 秒钟
        SingleOutputStreamOperator<Event> watermarks = operator
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2L))
                .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                    @Override
                    public long extractTimestamp(Event event, long recordTime) {
                        return event.getTimestamp();
                    }
                }));
        operator.print();

        // 定义测输出流标签,注意这个地方需要指定一个匿名内部类, 否则会有泛型擦除问题
        OutputTag<Event> late = new OutputTag<Event>("late"){};
        // 统计每个url的访问量
        SingleOutputStreamOperator<UrlViewCount> result = operator.keyBy(data -> data.getUrl())
                // 设置一个基于事件时间的滑动窗口, 窗口大小为10s 偏移量为2s , 每隔2s统计一次上10s的数据情况
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                //  方式二:允许窗口处理迟到数据,设置 1 分钟的等待时间
                .allowedLateness(Time.minutes(1))
                // 方式三:将最后的迟到数据输出到侧输出流 注意需要定义侧输出流标签
                .sideOutputLateData(late)
                .aggregate(new UrlCountExample.UrlViewCountAgg(), new UrlCountExample.UrlViewCountResult());

        result.print("result");
        // 最后要把侧输出流的执行的操作绑定到窗口函数算子上
        result.getSideOutput(late).print("late");
        env.execute();
    }
    // 泛型 Long是输入是aggregate的结果, UrlViewCount是输出,我们需要输出一个自定义类,
    // String是key的类型, 因为是根据url进行keyBy的, 所以是String类型, TimeWindow是当前开窗类型, 按时间进行开窗
    public static class UrlViewCountResult extends ProcessWindowFunction<Long, UrlViewCount,String, TimeWindow> {
        @Override
        public void process(String key, Context context, Iterable<Long> in, Collector<UrlViewCount> out) throws Exception {
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            out.collect(new UrlViewCount(key,in.iterator().next(),start,end));
        }
    }
    // 泛型Event对象 累加器计算个数所以是Long 结果是Long类型
    public static class UrlViewCountAgg implements AggregateFunction<Event, Long,Long> {
        @Override
        public Long createAccumulator() {
            return 0L;
        }

        @Override
        public Long add(Event event, Long accumulator) {
            return accumulator + 1;
        }

        @Override
        public Long getResult(Long accumulator) {
            return accumulator;
        }

        @Override
        public Long merge(Long aLong, Long acc1) {
            return null;
        }
    }

你可能感兴趣的:(flink)