flink1.13(一)

news2024/12/24 3:17:07

1. 初识Flink

1.1 Flink是什么

  Apache Flink 是一个框架和分布式处理引擎, 用于对无界和有界数据流进行状态计算. Flink框架处理流程如下图所示:
在这里插入图片描述
Flink的应用场景:
在这里插入图片描述

1.2 为什么选择Flink

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

1.3 数据处理架构的发展

  1. 事务处理
    在这里插入图片描述
  2. 分析处理, 如大数据框架hadoop和hive 都是离线的, 保存一定的数据量后进行处理
  3. 有状态的流式处理
    在这里插入图片描述
  4. lambda架构的流式处理
    在这里插入图片描述

1.4 新一代流处理器Flink

核心特点:

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

1.5 Flink的应用场景

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

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

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

Flink的分层API

在这里插入图片描述
在Flink1.12版本后, 用DataStreamAPI可以实现批流一体化, 现在基本上不用DataSetAPI了

2. Flink快速上手

2.1 创建一个Java项目

引入maven相关依赖

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <!-- 引入日志管理相关依赖-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.14.0</version>
        </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文件
在这里插入图片描述


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<Tuple2<String,Long>>
        // 代表需要把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();
    }
}

在这里插入图片描述
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<String> 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的方式输出了
在这里插入图片描述

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

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

3.Flink部署

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

3.1 快速启动

3.1.1 环境配置

Flink 是一个分布式的流处理框架,所以实际应用一般都需要搭建集群环境。我们在进行Flink 安装部署的学习时,需要准备 3 台 Linux 机器。具体要求如下:
在这里插入图片描述

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。
    在这里插入图片描述

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

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

  1. 程序打包
    (1)为方便自定义结构和定制依赖,我们可以引入插件 maven-assembly-plugin 进行打包。在 Flink项目的 pom.xml 文件中添加打包插件的配置,具体如下:
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.0.0</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </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”,将任务提交到集群运行。
    在这里插入图片描述

    (3)任务提交成功之后,可点击左侧导航栏的“Running Jobs”查看程序运行列表情况
    在这里插入图片描述
    启动报错了, 在jobManager中查看任务运行日志, 看到是没有启动nc,导致socket连接失败,启动nc后重新执行
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    在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
    在这里插入图片描述
    如果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)

  会话模式其实最符合常规思维。我们需要先启动一个集群,保持一个会话,在这个会话中通过客户端提交作业,如图 所示。集群启动时所有资源就都已经确定,所以所有提交的作业会竞争集群中的资源。

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

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

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

3.3.3 应用模式(Application Mode)

  前面提到的两种模式下,应用代码都是在客户端上执行,然后由客户端提交给 JobManager的。但是这种方式客户端需要占用大量网络带宽,去下载依赖和把二进制数据发送给JobManager;加上很多情况下我们提交作业用的是同一个客户端,就会加重客户端所在节点的资源消耗。
  所以解决办法就是,我们不要客户端了,直接把应用提交到 JobManger 上运行。而这也就代表着,我们需要为每一个提交的应用单独启动一个 JobManager,也就是创建一个集群。这个 JobManager 只为执行这一个应用而存在,执行结束之后 JobManager 也就关闭了,这就是所谓的应用模式,
在这里插入图片描述
  应用模式与单作业模式,都是提交作业之后才创建集群;单作业模式是通过客户端来提交的,客户端解析出的每一个作业对应一个集群;而应用模式下,是直接由 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

在这里插入图片描述

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 或者命令行两种方式提交作业。
    在这里插入图片描述
  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 界面查看运行情况。
    在这里插入图片描述
    从图中可以看到我们创建的 Yarn-Session 实际上是一个 Yarn 的Application,并且有唯一的 Application ID。也可以通过 Flink 的 Web UI 页面查看提交任务的运行情况
    在这里插入图片描述

3.4.3 单作业模式部署

在这里插入图片描述
在这里插入图片描述
这里的 application_XXXX_YY 是当前应用的 ID,是作业的 ID。注意如果取消作业,整个 Flink 集群也会停掉。

3.4.4 应用模式部署

在这里插入图片描述

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-attempts</name>
 <value>4</value>
 <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 整体构成

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

4.1.2 作业管理器JobManager

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

4.1.3 任务管理器TaskManager

在这里插入图片描述

4.2 Flink作业提交流程

4.2.1 高层级抽象视角

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

4.2.2 独立模式(Standalone)

在这里插入图片描述

4.2.3 YARN 集群

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

4.3 一些重要概念

4.3.1 数据流图(DataFlow Graph)

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

4.3.2 并行度(Parallelism)

在这里插入图片描述

在这里插入图片描述

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

4.3.3 算子链(Operator Chain)

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

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

在这里插入图片描述

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

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

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

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 也就没有存在的必要了。

在这里插入图片描述

  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 模式
    在这里插入图片描述

5.1.3 触发程序执行

在这里插入图片描述

5.2 源算子

在这里插入图片描述

5.2.1 准备工作

在这里插入图片描述

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;
}

在这里插入图片描述

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中读取数据的话
在这里插入图片描述

5.2.3 从socket中读取数据

在这里插入图片描述

5.2.4 从Kafka读取数据

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

<dependency>
 	<groupId>org.apache.flink</groupId>
	<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

在这里插入图片描述

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支持的数据类型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
再次强调一下Flink对POJO类的定义, 类必须是公共的(public)和独立的(没有非静态的内部类). 类必须有公共的无参构造方法, 类内部的字段是public且非final的, 或者有公共的getter和setter方法, 且满足Java命名规范
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3 转换算子(Transformation)

在这里插入图片描述

5.3.1 基本转换算子

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

  1. 映射(map)
    在这里插入图片描述
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<String> map = streamSource.map(new MyMapper());
        // 2.使用匿名类实现MapFunction接口
        /*SingleOutputStreamOperator<String> map = streamSource.map(new MapFunction<Event, String>() {
            @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)

在这里插入图片描述

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<Event> filter = streamSource.filter(new MyFilter());
        // 2. 传入一个匿名类
        /*SingleOutputStreamOperator<Event> filter = streamSource.filter(new FilterFunction<Event>() {
            @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)

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

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<String> 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)

在这里插入图片描述

  1. 按键分区(key by)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
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. 简单聚合

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

  1. 规约聚合
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
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)

在这里插入图片描述

  1. 函数类
    在这里插入图片描述
  2. 匿名函数(Lambda)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    这些方法对于其它泛型擦除的场景同样适用。
  3. 富函数类(Rich Function Classes)
    在这里插入图片描述
    在这里插入图片描述
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)

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

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();

在这里插入图片描述
整体来看没有什么规律, 在数据量很多的情况下, 会大概率变成均匀分布

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

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

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)

在这里插入图片描述

5.4.1 连接到外部系统

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

5.4.2 输出到文件

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

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();
    }

在这里插入图片描述
因为设置了4个并行子任务来执行写入, 而且还设置了滚动策略. 所以输出路径如下图所示
在这里插入图片描述

5.4.3 输出到Kafka

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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();

在这里插入图片描述

 // 自定义类实现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();
        }
    }

在这里插入图片描述

5.4.5 输出到ElasticSearch

在这里插入图片描述

  1. 添加ElasticSearch连接器依赖
<dependency>
 	<groupId>org.apache.flink</groupId>
 	<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();
    }

在这里插入图片描述

5.4.6 输出到MYSQL(jdbc)

在这里插入图片描述

<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>

在这里插入图片描述

在这里插入图片描述

5.4.7 自定义Sink输出如HBase

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

<dependency>
 	<groupId>org.apache.hbase</groupId>
 	<artifactId>hbase-client</artifactId>
 	<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中的时间语义

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

6.1.2 哪种时间语义更重要

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

6.2 水位线

6.2.1 事件时间和窗口

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

6.2.2 什么是水位线

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

6.2.3 如何生成水位线

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
水位线的周期性生成间隔时间设置env.getConfig().setAutoWatermarkInterval(100L) 单位是ms表示每隔100ms生成一次水位线, 默认值是200ms
在这里插入图片描述
在这里插入图片描述

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();
    }

在这里插入图片描述

在这里插入图片描述

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));
        }
    }

在这里插入图片描述

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 水位线的传递

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

6.3 窗口

6.3.1 窗口的概念

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

6.3.2 窗口的分类

  1. 按照驱动类型分类
    在这里插入图片描述
    (1)时间窗口(Time Window)
    在这里插入图片描述
    在这里插入图片描述
    (2)计数窗口(Count Window)
    在这里插入图片描述
  2. 按照窗口分配数据的规则分类
    在这里插入图片描述
    (1)滚动窗口(Tumbling Windows)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    (2)滑动窗口(Sliding Windows)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    (3)会话窗口(Session Windows)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    (4)全局窗口(Global Windows)
    在这里插入图片描述
    在这里插入图片描述

6.3.3 窗口API概览

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

6.3.4 窗口分配器(Window Assigners)

在这里插入图片描述

  1. 时间窗口
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 计数窗口
    在这里插入图片描述
    在这里插入图片描述
  3. 全局窗口
    在这里插入图片描述
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窗口函数

在这里插入图片描述
对WindowStream进行窗口函数后, 才能得到一个DataStream
在这里插入图片描述

  1. 增量聚合函数(incremental aggregation functions)流处理思路增量的计算
    在这里插入图片描述
    (1)归约函数(ReduceFunction)
    在这里插入图片描述
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();
    }

在这里插入图片描述
(2)聚合函数(AggregateFunction)重要
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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;
        }
    }

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

  1. 全窗口函数(full window functions)批处理思路攒一波最后计算
    在这里插入图片描述
    (1)窗口函数(WindowFunction)不推荐使用
    在这里插入图片描述
    在这里插入图片描述
    (2)处理窗口函数(ProcessWindowFunction)推荐使用
    在这里插入图片描述
    在这里插入图片描述
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 );
        }
    }

在这里插入图片描述

  1. 增量聚合和全窗口函数的结合使用
    在这里插入图片描述
    在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;
        }
    }

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

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) +
                '}';
    }
}

在这里插入图片描述

6.3.6 其他API

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

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;
        }
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/22202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【第九章】vim程序编辑器

文章目录vi与vimvi的使用范例按键说明一般指令模式可用的按钮说明&#xff1a;光标移动、复制贴上、搜寻取代等一般指令模式切换到编辑模式的可用的按钮说明一般指令模式切换到命令行界面的可用按钮说明vim的暂存盘、救援回复与打开时的警告讯息vim的额外功能区块选择&#xff…

AOP编程

什么是面向切面编程AOP? 在软件业&#xff0c;AOP为Aspect Oriented Programming的缩写&#xff0c;意为&#xff1a;面向切面编程&#xff0c;通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续&#xff0c;是软件开发中的一个热点&#xff…

【个人硬件开源分享】基于ESP32的能量机关主控受控模块,伏虎单手摇杆重力感应加摇杆加LCD屏幕分为左手和右手,基于PD5048A的I2C总线扩展级联器

提示&#xff1a;文章看完后&#xff0c;记得点赞&#xff01; 文章目录前言基于ESP32的能量机关主控受控模块伏虎单手摇杆I2C总线级联器MPU6050陀螺仪WS2812灯珠总结前言 这些是我业余时间做的一些电子小作品&#xff0c;包括基于ESP32的能量机关主控受控模块&#xff0c;伏虎…

在这个技术浮躁的时代也要注重架构的本质,多位大佬联袂推荐小团队构建大网站,让你少走弯路,甚至实现速成,成为优秀架构师!

作为一个技术人&#xff0c;架构师是每个人追寻的目标&#xff0c;也是个人价值和成就的一个重要标志。 那架构师是怎样炼成的呢&#xff1f;或者说&#xff0c;怎样才算是一个优秀的架构师呢&#xff1f; 我们需要几年甚至十几年的一线技术工作经验&#xff0c;我们需要正好遇…

isdigit isdecimal isnumeric 区别

一、代码测试 num "1" #unicode num.isdigit() # True num.isdecimal() # True num.isnumeric() # Truenum "1" # 全角 num.isdigit() # True num.isdecimal() # True num.isnumeric() # Truenum b"1" # byte num.isdigit() # True n…

还在手动电源模块测试吗?ATECLOUD智能云测试解放双手

江苏某生产电源的企业联系到纳米软件Namisoft&#xff0c;了解到他们目前依然使用的是全人工模式进行电源模块参数的测试&#xff0c;经常会因为手动记录数据而导致的一些数据出错&#xff0c;测试的效率也是非常低。通过网络了解到Namisoft的ATECLOUD智能云测试平台&#xff0…

Eruke是什么?怎么搭建?

Eruke是什么&#xff1f;怎么搭建?认识Eureka构成单机构建项目搭建注册中心生产者服务发现自我保护什么是自我保护&#xff1f;为什么会有自我保护&#xff1f;关闭结果演示停更认识Eureka 它是一个基于REST的服务&#xff0c;是实现服务发现与注册的组件。 构成 Eureka采用…

【愚公系列】2022年11月 uniapp专题-运行uniapp的多种方式

文章目录前言一、运行到web平台二、运行到安卓模拟器三、运行到微信开发者工具前言 uniapp本身就是一次开发多端部署&#xff0c;uniapp可以发布成各种应用包括以下几种 web版本 一、运行到web平台 点击工具栏的运行 -> 运行到浏览器 -> 选择浏览器&#xff0c;即可体…

BIOMOD2模型、MaxEnt模型物种分布模拟,生物多样性生境模拟,论文写作

目录 ①基于R语言BIOMOD2模型的物种分布模拟实践技术应用 ②基于R语言、MaxEnt模型融合技术的物种分布模拟、参数优化方法、结果分析制图与论文写作 ③基于MAXENT模型的生物多样性生境模拟与保护优先区甄选、自然保护区布局优化评估 ①基于R语言BIOMOD2模型的物种分布模拟实…

分享Markdown编写文档的技巧

Markdown 是一种轻量级标记语言&#xff0c;使用易读易写的纯文本格式编写文档&#xff0c;然后转换成有效的 XHTML&#xff08;或者HTML&#xff09;文档。Markdown有许多好用简单的用法&#xff0c;下面分享Markdown用来编写文档的技巧。 注&#xff1a;下面通过添加\反斜杠来…

Get To The Point: Summarization with Pointer-Generator Networks

论文题目&#xff1a;《Get To The Point: Summarization with Pointer-Generator Networks》 主要内容&#xff1a;seq2seq&#xff0c;文本摘要 《切中要害&#xff1a;指针生成器网络概述》 摘要 神经序列到序列模型为抽象文本摘要提供了一种可行的新方法&#xff08;这意…

TCP链接异常: SYN_RECV

1. 异常数据包分析&#xff1a; 从数据包分析来看应该是网关这边出问题了&#xff0c;应该是网关的服务程序在1217上出问题了&#xff0c; &#xff08;注意左右量变的数据包的一个syn的Seq都是1358143899&#xff09; 从重发2开始网关这边就一直认为它没有收到client回复给它…

Okhttp连接泄漏警告问题分析

背景 某天在查询生产日志时&#xff0c;发现大量的Okhttp连接泄漏警告日志&#xff0c;但生产上没有收到任何异常反馈。出于好奇心&#xff0c;本地最小化复现问题&#xff0c;并最终解决问题。 分析问题 okhttp官网的demo示例 OkHttpClient client new OkHttpClient();Stri…

【K8S】学习笔记(二)

K8S学习笔记三、Kubernetes 核心概念3.1、kubernetes 集群命令行工具 kubectl3.1.1、kubectl 概述3.1.2、kubectl 命令格式3.1.3、kubectl 基础命令3.1.4、kubectl 部署命令3.1.5、kubectl 集群管理命令3.1.6、kubectl 故障和调试命令3.1.7、kubectl 其它命令3.2、Kubernetes 集…

分组后将成员拼成字符串

【问题】 I have a text file like this: key1 value1 A key1 value2 B key1 value3 A key2 value1 A key2 value2 BI am trying to open it as a dictionary and print the list of keys and values separated by commas so it looks like this in the end: key1 value1,va…

PTA题目 谁先倒

划拳是古老中国酒文化的一个有趣的组成部分。酒桌上两人划拳的方法为&#xff1a;每人口中喊出一个数字&#xff0c;同时用手比划出一个数字。如果谁比划出的数字正好等于两人喊出的数字之和&#xff0c;谁就输了&#xff0c;输家罚一杯酒。两人同赢或两人同输则继续下一轮&…

智云通CRM:越是害怕被客户拒绝,你就越会被拒绝?

你害怕被客户拒绝码&#xff1f;先别急着说“害怕”&#xff0c;先来看看你为什么会被拒绝&#xff1f;因为你认为自己会失败&#xff01; 很多情况霞&#xff0c;我们会被客户拒绝&#xff0c;原因在哪里&#xff1f;原因有很多&#xff0c;最常见的是因为业务员自身的心理障…

SSM整合(四)

SSM整合之redis设置值和获取值三种方式 准备工作(导入一些必要的依赖) <dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>2.4.1</version> </dependency> <…

ES6 入门教程 19 Generator 函数的语法 19.1 简介

ES6 入门教程 ECMAScript 6 入门 作者&#xff1a;阮一峰 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录ES6 入门教程19 Generator 函数的语法19.1 简介19.1.1 基本概念19.1.2 yield 表达式19.1.3 与 Iterator 接口的关系19 Generator 函数的语…

Espressif-IDE ESP32 LED Flash 闪烁工程的创建

前言 Espressif-IDE 类似于 eclipse 开发环境&#xff0c;更新 ESP32的开发环境后&#xff0c;发现可以直接使用 Espressif-IDE 进行ESP32 的工程代码开发、程序下载&#xff0c;非常的方便 本篇开始把 ESP32的开发板的LED 点亮&#xff0c;熟悉下 Espressif-IDE 的开发流程 …