flink学习

概念

flink是开源大数据流处理框架,分布式处理引擎,内存执行(为了保存读取状态的时候很快[在本地读取])。
批处理spark+流处理storm—》流批一体lambda(合并两套)----》流处理flink

不同级别API:
最高级最简单(好用):flinksql
声明式sql(老bug):table Api
核心api(不会):DataStream、DataSet API
底层(最难):有状态的流

spark底层是RDD sparkStream是DataStream,一组组小批数据RDD,有阶段的进行
flink底层是DataFlow和Event事件序列

第一道面试题,使用flink的原因:

⚫ Flink 的延迟是毫秒级别,而 Spark Streaming 的延迟是秒级延迟。
⚫ Flink 提供了严格的精确一次性语义保证。
⚫ Flink 的窗口 API 更加灵活、语义更丰富。
⚫ Flink 提供事件时间语义,可以正确处理延迟数据。
⚫ Flink 提供了更加灵活的可对状态编程的 API。

(延迟低 精准一次写入 api灵活 提供事件时间 可对状态变成)

flink配置

第二道面试题:flink的部署模式:

在一些应用场景中,对于集群资源分配和占用的方式,可能会有特定的需求。Flink 为各
种场景提供了不同的部署模式,主要有以下三种:
⚫ 会话模式(Session Mode) 启动集群保持会话并在客户端提交作业,适合规模小执行时间短的大量作业,集群一直在,作业抢集群的资源
⚫ 单作业模式(Per-Job Mode)yarn和k8s用但作业模式隔离资源,为每一个提交的作业启动一个集群,客户端执行交给jobmanager,提交任务才创建集群,客户端提交,一个任务一个集群
⚫ 应用模式(Application Mode)不像前两种需要客户端,我们不浪费贷款和下载依赖,直接给jobmanager,就是创建一个集群,任务结束jm关闭,提交任务创建集群,直接jobmanager执行,多个作业也只创建一个集群
它们的区别主要在于:集群的生命周期以及资源的分配方式;以及应用的 main 方法到底
在哪里执行——客户端(Client)还是 JobManager。

单机+cluster

解压之后直接start-cluster.sh,8081端口访问

集群+cluster
vi flink-conf.yaml
jobmanager.rpc.address: master
vi workders
slave1
slave2

分发flink后start-cluster.sh,8081端口访问

提交任务使用flink run -m master:8081 -c 主类 jar包

不借助yarn的standalone实现三个部署
1会话模式:

设置flink-conf.yaml的jobmanager然后start-cluster.sh
flink run的时候指定-m 指定jobmanager即可

2单作业模式

standalone不支持哦,因为需要借助外部资源yarn或者k8s

3应用模式
  1. 提前把jar包放到lib目录下
    cp my.jar $FLINK_HOME/lib
  2. 开启一个任务来启动jobmanager
    standalone-job.sh start --job-classname 主类
    standalone-job.sh会自动搜索
  3. 启动taskmanager
    taskmanager.sh start
  4. 关闭集群
    standalone-job.sh stop
    taskmanager.sh stop
借助yarn的yarn模式实现三个部署

客户端把flink提交给yarn的resourcemanager,yarn的resourcemanager向yarn的nodemanager申请容器,flink部署jobmanager和taskmanager来启动集群,根据jobmanager上的作业所需要的slots数量动态分配taskmanager资源

#配置yarn环境变量
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
#必须要配置HADOOP_CLASSPATH,而且要在HADOOP_HOME放到PATH里面之后
export HADOOP_CLASSPATH=`hadoop classpath

#配置jobmanager和taskmanager资源大小
vi flink-conf.yaml
jobmanager.memory.process.size: 1600m
taskmanager.memory.process.size: 1728m
taskmanager.numberOfTaskSlots: 8
parallelism.default: 1

启动hadoop

1会话模式

首先启动hadoop,然后yarn-session向yarn申请资源,开启会话,启动flink
yarn-session.sh -nm test
-d后台
-jmjobmanager所需内存单位MB
-nm name yarn ui的人物名
-ququeen yarn队列名
-tm每个taskmanager 所需内存
运行使用flink run -c 主类 jar包
-m / -jobmanager确定jonmanager地址,地址在yarn8088端口可以看到

2单作业模式

有了yarn环境,直接提交一个单独作业不提前开启yarn -session就是单作业模式
flink run -d -t yarn-per-job -c 主类 jar包
早期:
flink run -m yarn-cluster -c 主类 jar包
flink list -t yarn-per-job -Dyarn.application.id=application_xx_yy查看
flink cancel -t yarn-per-job -Dyarn.application.id=application.xxx_yy jobid取消当前应用的作业
取消作业,整个flink集群也会停掉

3应用模式

flink run-application -t yarn-application -c 主类 jar包运行
flink lit -t yarn-application -Dyarn.application.id=application_xxx_yy查看
flink cancel -t yarn-application -Dyarn.application.id=application_xx_yy取消

框架的结构

jobmanager:master管理者,负责管理调度,只有一个
taskmanager:worker工作者,执行任务处理数据,一个或多个flink框架可以看到flink任务到jobmanager,jobmanager把任务交给taskmanager做。

第三道面试题:jm和tm的启动方式

JobManager 和 TaskManagers 可以以不同的方式启动:
⚫ 作为独立(Standalone)集群的进程,直接在机器上启动
⚫ 在容器中启动
⚫ 由资源管理平台调度启动,比如 YARN、K8S

第四道面试题:作业提交流程

(1)一般情况下,由客户端(App)通过分发器提供的REST接口,将作业提交给JobManager。

(2)由分发器启动 JobMaster,并将作业(包含 JobGraph)提交给 JobMaster。

(3)JobMaster 将 JobGraph 解析为可执行的 ExecutionGraph,得到所需的资源数量,然后
向资源管理器请求任务槽资源(slots)。

(4)资源管理器判断当前是否由足够的可用资源;如果没有,启动新的 TaskManager。

(5)TaskManager 启动之后,向 ResourceManager 注册自己的可用任务槽(slots)。

(6)资源管理器通知 TaskManager 为新的作业提供 slots。

(7)TaskManager 连接到对应的 JobMaster,提供 slots。
(8)JobMaster 将需要执行的任务分发给 TaskManager。
(9)TaskManager 执行任务,互相之间可以交换数据。

如果部署模式不同,或者集群环境不同(例如 Standalone、YARN、K8S 等),其中一些步骤可能会不同或被省略,也可能有些组件会运行在同一个 JVM 进程中。比如我们在上一章践过的独立集群环境的会话模式,就是需要先启动集群,如果资源不够,只能等待资源释放,而不会直接启动新的TaskManager。

逻辑图-作业图-流程图-物理图
客户端完成-交给jobmanager的-jobmanger的优化的-jobmanager完成-taskmanager完成

任务和任务槽

任务槽是task slots
taskmanager.numberOfTaskSlots: 8设置任务槽数量,一个任务槽表示一个线程,多设几个增加并行度
默认flink子任务共享slot
.map().slogSharingGroup("1")设置为一个组的才会被共享

并行度parallelism是动态的,是实际的并行度
parallelism.default设置,会在多个taskmanager上平均分配在这里插入图片描述

依赖

pom.xml (比赛提供pom.xml文件)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>work</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <flink.version>1.14.0</flink.version>
        <scala.version>2.12</scala.version>
        <hive.version>3.1.2</hive.version>
        <mysqlconnect.version>5.1.47</mysqlconnect.version>
        <clickhouse.version>0.3.2</clickhouse.version>
        <hdfs.version>3.1.3</hdfs.version>
        <spark.version>3.1.1</spark.version>
        <hbase.version>2.2.3</hbase.version>
        <kafka.version>2.4.1</kafka.version>
        <lang3.version>3.9</lang3.version>
        <flink-connector-redis.verion>1.1.5</flink-connector-redis.verion>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-reflect</artifactId>
            <version>${scala.version}.16</version>
        </dependency>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-compiler</artifactId>
            <version>${scala.version}.16</version>
        </dependency>
        <dependency>
            <groupId>ru.yandex.clickhouse</groupId>
            <artifactId>clickhouse-jdbc</artifactId>
            <version>0.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}.16</version>
        </dependency>

        <!--kafka-->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_${scala.version}</artifactId>
            <version>${kafka.version}</version>
        </dependency>

        <!--flink 实时处理-->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-runtime-web_${scala.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_${scala.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-scala_${scala.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka_${scala.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.json4s</groupId>
            <artifactId>json4s-core_2.12</artifactId>
            <version>3.6.6</version>
        </dependency>
        <dependency>
            <groupId>org.json4s</groupId>
            <artifactId>json4s-jackson_2.12</artifactId>
            <version>3.6.6</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner_${scala.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>com.clickhouse</groupId>
            <artifactId>clickhouse-http-client</artifactId>
            <version>0.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-json</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-scala-bridge_${scala.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-redis_2.11</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.flink</groupId>
                    <artifactId>flink-shaded-hadoop2</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.commons</groupId>
                    <artifactId>commons-lang3</artifactId>
                </exclusion>
            </exclusions>
            <version>${flink-connector-redis.verion}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${lang3.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-hive_${scala.version}
            </artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-hbase-2.2_${scala.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>


        <!--mysql连接器-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysqlconnect.version}</version>
        </dependency>


        <!--spark处理离线-->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_${scala.version}</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.hive</groupId>
                    <artifactId>hive-exec</artifactId>
                </exclusion>
            </exclusions>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql_${scala.version}</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.hive</groupId>
                    <artifactId>hive-exec</artifactId>
                </exclusion>
            </exclusions>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-hive_${scala.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>



        <!-- hadoop相关-->
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>${hdfs.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-auth</artifactId>
            <version>${hdfs.version}</version>
        </dependency>



        <!--hbase 相关-->
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-mapreduce</artifactId>
            <version>${hbase.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>${hbase.version}</version>
        </dependency>
        <!--jdbc-->
        <!-- 连接mysql需要驱动包-->
	<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>

        <!--clickhouse-->
        <!-- 连接ClickHouse需要驱动包-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>

        <dependency>
            <groupId>ru.yandex.clickhouse</groupId>
            <artifactId>clickhouse-jdbc</artifactId>
            <version>${clickhouse.version}</version>
            <!-- 去除与Spark 冲突的包 -->
            <exclusions>
                <exclusion>
                    <groupId>com.fasterxml.jackson.core</groupId>
                    <artifactId>jackson-databind</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>net.jpountz.lz4</groupId>
                    <artifactId>lz4</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>


    <build>
    <sourceDirectory>src/main/scala</sourceDirectory>
    <testSourceDirectory>src/test/java</testSourceDirectory>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <recompileMode>incremental</recompileMode>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
   <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.1.6</version>
                <configuration>
                    <scalaCompatVersion>2.11</scalaCompatVersion>
                    <scalaVersion>2.11.12</scalaVersion>
                    <encoding>UTF-8</encoding>
                </configuration>
                <executions>
                    <execution>
                        <id>compile-scala</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>test-compile-scala</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>
</project>

初始化

//批处理,读文件
val env=ExecutionEnvironment.getExecutionEnvironment
//本地执行环境,不给参数的话并行度默认cpu核心数
val localEnvironment=StreamExecutionEnvironment.createLocalEnvironment()
//流处理,kafka,socket,
val env = StreamExecutionEnvironment.getExecutionEnvironment
//流处理需要execute,不然不会运行
//streaming转batch
env.setRuntimeMode(RuntimeExecutionMode.BATCH)
//触发执行
env.execute()

flink run -Dexecution.runtime-mode=BATCH默认就是STREAMING模式

事件时间和处理时间

处理时间是机器的系统时间
事件时间是每个设备在对应的设备上发生的时间,就是数据生成的时间,就是Timestamp时间戳

获取数据

#批处理
val data=env.readTextFile("input/works.txt")

//流处理
 env.readTextFile("")
 env.addSource(new FlinkKafkaConsumer[String](topic, new MykafkaDeserializationSchemaConsumer, names.getProp())())
 env.socketTextStream()
 env.socketTextStream("master",7777)
 

source

方法意思案例
socketTextStream获取端口流数据env.socketTextStream(“host”,port)
fromCollection获取列表数据env.fromCollection(List(Event(‘’),…))
fromElements获取元素数据env.fromElements(Event()…)
readTestFile获取文件数据env.readTextFile(“path”)
env.addSource从类库获取数据env.addSource(new FlinkKafkaConsumer[](‘topic’,new SimpleStringSchema(),prop))
kafka源
val prop=new Properties()
val BOOTSTRAP_SERVERS="master:9092,slave1:9092,slave2:9092"
prop.setProperty("bootstrap.servers",BOOTSTRAP_SERVERS)
prop.setProperty("key.deserializer","org.apache.kakfa.common.serialization.StringDeserializer")
prop.setProperty("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer")
prop.setProperty("auto.offset.reset","latest")
//topic可以是一个也可以是多个,还可以匹配正则
//DeserializationSchema或KeyedDeserializationSchema,进行反序列化,可以自定义
//prop是kafka客户端属性bootstrap.servers key/value.deserializer...
val data=env.addSource(new FlinkKafkaConsumer[String]("topic",new SimpleStringSchema(),prop))
data.print("from kafka:")
env.execute()

自定义源

class Clicksource extends SourceFunction[Event]{
	var running=true
	def run=={
		while(running){
		val random=new Random
		random.nextInt() //随机数
		//Calender.getInstance.getTimeInMillis 获取当前时间戳
			collect.collect(data)
			Thread.sleep(1000)
		}
	}
	def  cancel=={
		running=false
	}
}

//在主类里面
env.addSource(new ClickSource).print()
env.execute()


=-------------
.addSource(new RichParallelSourceFunction[Int] {
	 override def run(sourceContext: SourceFunction.SourceContext[Int]):Unit= {
	 for (i <- 0 to 7) {
 	// 将偶数发送到下游索引为 0 的并行子任务中去
	 // 将奇数发送到下游索引为 1 的并行子任务中去
	 	if ((i + 1) % 2 == getRuntimeContext.getIndexOfThisSubtask) {
			 sourceContext.collect(i + 1)
		 }
	 }
 }
 // 这里???是 Scala 中的占位符
 override def cancel(): Unit = ???
 })

处理数据

并行度设置:

  1. setParallelism(2)设置并行度
  2. flink run -p 2 -c 主类 jar包设置并行度
  3. parallelism.default : 2flink-conf.yaml设置并行度
一对一:map filter flatMap
重分区:keyBy window apply
// 禁用算子链 disableChaining
.map((_,1)).disableChaining()
// 从当前算子开始新链 startNewChain
.map((_,1)).startNewChain()
//算子
data.flatMap(_.split(" ")).map((_,1)).groupBy(0).sum(1)
//批处理groupBy 流处理keyBy
//流处理没有groupBy

算子意思案例
flatMap平坦化data.flatMap(_.split(" "))
mapforeach对每个数据处理.map((_,1))
keyBy分组.keyBy(_._1)批groupBy同理,flink没有直接聚合的API,必须要先分区。为了提高效率,先分区才能聚合keyBy,tuple用一个多个位置组合,pojo用字段,lambda表达式或keySelector提取 .keyBy(_=>true)放到一个分区
sum累加.sum(1)指定按照元祖第2个加
filter过滤.filter(_.user.equals(“Mary”))
sum求和keyBy后才能聚合
min最小这些聚合操作只要指定字段就行
max最大元祖是_1,_2
minBy最小值所在记录可以指定位置或指定名称
maxBy最大值所在记录
reduce聚合,将keyedStream转为DataStream,不改变类型.redis(+)
udf自定义有filterFunction MapFunction ReduceFunction
rich udf富函数RichMapFunction RichFilterFunction RichReduceFunction 有上下文对象getRuntimeContext.getState,自带open和close方法

自定义map


class UserExtractor extends MapFunction[Event,String]{
	map=={
	value.user}
}

自定义filter

class UserFilter extends FilterFunction[Event]{
	filter==>{value.user.equals("Mary")}
}

自定义flatMap

//自定义map很强大,可以过滤,因为有一个out输出,很灵活!
class MyFlatMap extends FlatMapFunction[Event,String]{
	flatMap==>{
		if(value.user.equals("Mary")){
			out.collect(value.user)
		}else if(value.user.equals("Bob")){
			out.collect(value.user)
		}
	}
}

自定义RichMap

richFlatMap有out参数,

.map(new RichMapFunction[Event,Long](){
	open==>{
	//数据库链接,配置文件读取,IO流创建
		getRuntime.Context.getIndexOfThisSubtask //获取任务索引
		//程序执行的并行度,任务名称,以及状态
(state)。
	}
	//任务结束执行,关闭数据库连接
	close==>{}
	//任务的真正执行,来一条处理一条
	map=>{}
})

自定义RichFlatMap

class MyFlatMap extends RichFlatMapFunction[IN,OUT]{
 
 override def open(parameters: Configuration): Unit = {
 // 做一些初始化工作
 // 例如建立一个和 MySQL 的连接
 }
 override def flatMap(value: IN, out: Collector[OUT]): Unit = {
 // 对数据库进行读写
 }
 override def close(): Unit = {
 // 清理工作,关闭和 MySQL 数据库的连接。
 
 } }

第五道面试题:物理分区策略有哪些

常见的物理分区策略有随机分区、轮询分区、重缩放和广播,还有一种特殊的分区策略— —全局分区,并且 Flink 还支持用户自定义分区策略.
随机分区:数据随机地分配到下游算子的并行任务与中去。随机分布服从均匀分布,data.shuffle.print("shuffle").setParallelism(4)
轮询分区:按照先后顺序将数据一次分发data.rebalance.print("rebalance").setParallelism(4)
重缩放分区:和轮询类似。底层是轮询,但rebalance是所有人依次发牌,recale是分成小团体依次轮流发牌,接收方是发送方整倍数时效率更高data.setParallelism(2).rescale.setParallelism(4).print("rescale")
广播:将输入数据复制并发送到下游算子的所有并行任务中去data.broadcast.print("broadcast").setPraallelism(4)
全局分区:将所有的输入流都发送到下游算子的第一个并行子任务中区,相当于强行让下游任务的并行度变为1data.global().print("global")
自定义分区:data.partitionCustom(new Partitioner[Int]{partition==>{key%2},data=>data}).print()参数是一个partitioner对象,第二个是分区器的字段

保存数据

sink

//到kafka
data.addSink(new FlinkKafkaProducer[String]("dim_customer_login_log", new SimpleStringSchema(), one.getProp()))
//打印
data.print

sink

算子意思案例
addSInk保存到指定类型

sink到文件系统

val stream = env.fromElements(
 Event("Mary", "./home", 1000L),
 Event("Bob", "./cart", 2000L),
 Event("Alice", "./prod?id=100", 3000L),
 Event("Alice", "./prod?id=200", 3500L),
 Event("Bob", "./prod?id=2", 2500L),
 Event("Alice", "./prod?id=300", 3600L),
 Event("Bob", "./home", 3000L),
 Event("Bob", "./prod?id=1", 2300L),
 Event("Bob", "./prod?id=3", 3300L)
 )
 val fileSink = StreamingFileSink
 .forRowFormat(
 new Path("./output"),
 new SimpleStringEncoder[String]("UTF-8")
 )
 //通过.withRollingPolicy()方法指定“滚动策略”
 .withRollingPolicy(
 DefaultRollingPolicy.builder()
 //15分钟数据
 .withRolloverInterval(TimeUnit.MINUTES.toMillis(15))
 //最近5分钟没有收到新的数据
 .withInactivityInterval(TimeUnit.MINUTES.toMillis(5))
// 文件大小达到1G
 .withMaxPartSize(1024 * 1024 * 1024)
 //达到这三个就写
 .build()
 )
 .build
 stream.map(_.toString).addSink(fileSink)
 env.execute()

sink到kafka

val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val properties = new Properties()
properties.put("bootstrap.servers", "hadoop102:9092")
val stream = env.readTextFile("input/clicks.csv")
stream
.addSink(new FlinkKafkaProducer[String](
"clicks",
new SimpleStringSchema(),
properties
))
env.execute()
///kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic clicks查看

sink到redis

使用FlinkJedisPoolConfig.Builder().build和new RedisSink

 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 val conf = new FlinkJedisPoolConfig.Builder()
 	.setHost("redis")
 	.setPassword("123456")
 	.build()
 env.addSource(new ClickSource)
 .addSink(new RedisSink[Event](conf, new MyRedisMapper()))
 env.execute()
 //redisMapper是redis映射类接口,说明怎样将数据转换成可以写入redis的类型
 //有三个类,getKeyFrom获取redis的key getValue获取写入redis的value getCommand获取写入redis的命令
 class MyRedisMapper extends RedisMapper[Event] {
	 override def getKeyFromData(t: Event): String = t.user
 	override def getValueFromData(t: Event): String = t.url
	 override def getCommandDescription: RedisCommandDescription = new RedisCommandDescription(RedisCommand.HSET, "clicks")
 }

sink到ElasticsearchSink

val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 val stream = env.fromElements(
 Event("Mary", "./home", 1000L),
 Event("Bob", "./cart", 2000L),
 Event("Alice", "./prod?id=100", 3000L),
 Event("Alice", "./prod?id=200", 3500L),
 Event("Bob", "./prod?id=2", 2500L),
 Event("Alice", "./prod?id=300", 3600L),
 Event("Bob", "./home", 3000L),
 Event("Bob", "./prod?id=1", 2300L),
 Event("Bob", "./prod?id=3", 3300L)
 )
 val httpHosts = new util.ArrayList[HttpHost]()
 httpHosts.add(new HttpHost("hadoop102", 9200, "http"))
 val esBuilder = new ElasticsearchSink.Builder[Event](
 httpHosts,
 new ElasticsearchSinkFunction[Event] {
 override def process(t: Event, runtimeContext: RuntimeContext, 
requestIndexer: RequestIndexer): Unit = {
 val data = new java.util.HashMap[String, String]()
 data.put(t.user, t.url)
 val indexRequest = Requests
 .indexRequest()
 .index("clicks")
 .type("type")
 .source(data)
 requestIndexer.add(indexRequest)
 }
 }
 )
 stream.addSink(esBuilder.build())
 env.execute()

sink到mysql

pom依赖


stream.addSink(
	JdbcSink.sink(
 		"INSERT INTO clicks (user, url) VALUES (?, ?)",
 		new JdbcStatementBuilder[Event] {
 			override def accept(t: PreparedStatement, u: Event): Unit = {
				 t.setString(1, u.user)
 				t.setString(2, u.url)
 			}
 		},
	 new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
 	.withUrl("jdbc:mysql://localhost:3306/test")
 	.withDriverName("com.mysql.jdbc.Driver")
	 .withUsername("username")
	 .withPassword("password")
	 .build()
	 )
 )
 env.execute ()

自定义sink

集成sinkFunction和对应的RichSinkFunction抽象类,被addSink调用即可
//hbase
class MyHbaseSink() extends RichSinkFunction[work3Result]{
  var conn:client.Connection=_
  override def open(parameters: Configuration): Unit = {
    val conf = HBaseConfiguration.create()
    conf.set("hbase.zookeeper.quorum","bigdata1,bigdata2,bigdata3")
    conf.set("hbase.zookeeper.property.clientPort","2181")
    conf.set("hbase.master","bigdata1")
    conf.set("hbase.rootdir","/hbase")
    conn = ConnectionFactory.createConnection(conf)
  }

  override def invoke(value: work3Result, context: SinkFunction.Context): Unit = {
    //Exists? no useful
//    val admin = conn.getAdmin
//    val bool: Boolean = admin.tableExists(TableName.valueOf("threemin_warning_state_agg"))
//    if(!bool){
//      val threemin_warning_state_agg = new HTableDescriptor(TableName.valueOf("threemin_warning_state_agg"))
//      threemin_warning_state_agg.addFamily(new HColumnDescriptor("info"))
//      admin.createTable(threemin_warning_state_agg)
//    }
//    admin.close()

    val table = conn.getTable(TableName.valueOf("threemin_warning_state_agg"))
    val put=new Put(Bytes.toBytes(value.change_machine_id))
    put.addColumn("info".getBytes(),"change_machine_id".getBytes(),(value.change_machine_id).toString.getBytes())
    put.addColumn("info".getBytes(),"totalwarning".getBytes(),(value.totalwarning).toString.getBytes())
    put.addColumn("info".getBytes(),"windoow_end_time".getBytes(),(value.window_end_time).getBytes())
    table.put(put)
  }

  override def close(): Unit = {
    if(conn!=null){
      conn.close()
    }
  }

}
================================================
//mysql
class MyMysqlSink extends RichSinkFunction[work3Result]{
  var conn:Connection=_
  var statement:PreparedStatement=_
  override def open(parameters: Configuration): Unit = {
    Class.forName("com.mysql.jdbc.Driver")
    conn = DriverManager.getConnection("jdbc:mysql://bigdata1:3306/shtd_industry", "root", "123456")
  }
  override def invoke(value: work3Result, context: SinkFunction.Context): Unit = {
    println(value.totalwarning)
    statement = conn.prepareStatement("insert into threemin_warning_state_agg values(?,?,?)")
    statement.setInt(1,value.change_machine_id)
    statement.setInt(2,value.totalwarning)
    statement.setString(3,value.window_end_time)
    statement.executeUpdate()
  }

  override def close(): Unit = {
    if(conn!=null) {
      conn.close()
    }
    if(statement!=null) {
      statement.close()
    }
  }

}

水位线

WatermarkStrategy.forMonotonousTimestamps()有序
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(0))乱序
在事件时间下,基于数据自带的时间戳定义时钟,只有数据的时间戳比当前时钟大,才推动时钟前进(插入时间戳)
对于迟到数据,等2秒,就是当前已有数据的最大时间戳减去2秒。就是要插入的水位线的时间戳。

面试题:水位线的特性:

水位线就代表了当前的事件时间时钟,而且可以在数据的时间戳基础
上加一些延迟来保证不丢数据,这一点对于乱序流的正确处理非常重要。
我们可以总结一下水位线的特性:
⚫ 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据
⚫ 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展
⚫ 水位线是基于数据的时间戳生成的
⚫ 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
⚫ 水位线可以通过设置延迟,来保证正确处理乱序数据
⚫ 一个水位线 Watermark(t),表示在当前流中事件时间已经达到了时间戳 t, 这代表 t 之前的所有数据都到齐了,之后流中不会出现时间戳 t’ ≤ t 的数据水位线是 Flink 流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成对乱序数据的正确处理

水位线延迟越高,数据结果越准确,但是实时性降低。相反同理。可以设置处理迟到处理,让实时性高一点。
因为难以选择的特性,flink把水位线控制权交给程序员

//1.自定义水位线
//flink内置水位线能满足应用需求,不需要自定义
//timestampAssigner是提取时间戳元素
//WatermarkGenerator是按照既定的方法生成水位线
//既定有onEvent每个时间到来调用
//obPeriodicEmit周期性调用,默认200ms
//env.getConfig.setAutoWatermarkInterval(60*1000L)

//2.flink内置水位线生成器:有序流forMontonousTimestamps
//只要当前最大的时间戳作为水位线就可
//withTimestampAssigner将timestamp字段提取出来
stream.assignTimestampsAndWatermarks(
 	WatermarkStrategy.forMonotonousTimestamps[Event]()
 	.withTimestampAssigner(
 	new SerializableTimestampAssigner[Event] {
		 override def extractTimestamp(element: Event,recordTimestamp: Long): 
			Long = element.timestamp
 }
 )
 )

//3.乱序流 forBoundOutOfOrderness
//设置一个固定量的延迟时间,最大的时间戳减去延迟的结果。
//需要一个maxOutOfOrdemess参数表示最大乱序程序
.assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10))
        .withTimestampAssigner(
          new SerializableTimestampAssigner[MyOrderMaster] {
            override def extractTimestamp(element: MyOrderMaster, recordTimestamp: Long): Long = {
              val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
              val timeStr = element.modified_time
              format.parse(timeStr.toString).getTime
            }
          }
        )
      )
//在自定义的数据源中抽取事件时间,然后发送水位线后,就不能再在程序中使用assignTimestampsAndWatermarks了。只能二选一
env.fromSource(
      KafkaSource.builder()
        .setTopics("ChangeRecord")
        .setBootstrapServers(name.bootstrapServer)
        .setValueOnlyDeserializer(new SimpleStringSchema())
        .setProperty("auto.offset.reset","latest").build()
      ,WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(2))
      .withTimestampAssigner(new SerializableTimestampAssigner[String] {
        override def extractTimestamp(element: String, recordTimestamp: Long): Long = {
          val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
          format.parse(element.split(",")(5)).getTime
        }
      }),"work2"
    )

水位线的传递在重分区(多个task分区)下是按照最慢的那个时钟,也就是最小的那个水位线为准,水位线在上下游的传递避免了分布式系统没有统一时钟的问题,每次都以处理完之前的所有数据为标准确定自己的时钟,保证窗口处理的结果总是正确的。

窗口

窗口把无界数据切割成有限的数据块。
窗口分为时间窗口和计数窗口,或者滑动窗口,滚动窗口,会话窗口和全局窗口
全局窗口全局有效,会把相同key的所有数据分配到同一个窗口,因为无界流数据永无止尽,所以窗口没有结束的时候,也不会触发计算,需要添加自定义触发器;
flink计数窗口底层就是全局窗口。

在一定窗口之前,要先确定是否基于按键分区(window之前有没有keyBy),不keyBy就是windowAll并行度为1,手动调大并行度无效哦,不推荐

1.时间窗口
//1.滚动+处理时间窗口
stream.keyBy()
	.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
	.aggregate()
//2.滑动+处理时间窗口
stream.keyBy(...)
	.window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(5)))
	.aggregate(...)

//3.会话+处理时间
stream.keyBy(...)
	.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
	.aggregate(...)
	//动态间隔
.window(ProcessingTimeSessionWindows.withDynamicGap(new 
SessionWindowTimeGapExtractor[(String, Long)] {
	 override def extract(element: (String, Long)) { 
		// 提取 session gap 值返回, 单位毫秒
		 element._1.length * 1000
 }
}))
//4.滚动+事件时间窗口
stream.keyBy(...)
	.window(TumblingEventTimeWindows.of(Time.seconds(5)))
	.aggregate(...)

//5.滑动+事件时间
stream.keyBy(...)
	.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
	.aggregate(...)
//6.会话+事件时间窗口
stream.keyBy(...)
	.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
	.aggregate(...)


flink的窗口分配器:
在这里插入图片描述Time参数的类库为:Time

2.计数窗口
//1.滚动计数窗口
stream.keyBy()
	.countWindow(10)

//2.滑动计数窗口
stream.keyBy()
	.countWindow(10,3)
全局窗口
//3.全局窗口
stream.keyBy()
	.window(GlobalWindows.create())

//必须自定义触发器才能实现窗口计算,否则起不到任何作用

窗口函数

windowFunction
由图上可知,

  1. 按键流的窗口或全局窗口之后,需要用窗口函数reduce,aggreagte,apply,process
  2. 窗口window/windowAll后才能用窗口函数,窗口函数运行完后会变成dataStream
  3. dataStream进行keyBy后才能用sum,max,min,reduce的聚合函数
    api由上图可知,按键和非按键窗口函数的api由触发器,移除器,延迟数据,聚合函数,窗口函数,侧输出流。
    .window.windowAll的区别在于有没有.keyBy
    .reduce/aggregate/apply/process给出了窗口函数reduceFunction/aggregateFunction/ProcessWindowFunction结果为dataStream
    .trigger自定义触发器
    .evictor定义移除器
    .allowedLateness指定延迟时间
    .sideOutputLateData定义侧输出流
    .getSideOutput获取侧输出流
    Flink 处理迟到数据,对于结果的正确性有三重保障:水位线的延迟,口允许迟到数据,以及将迟到数据放入窗口侧输出流。
增量聚合函数

reduceFunctionaggregateFunction

val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 env
 .addSource(new ClickSource)
 // 数据源中的时间戳是单调递增的,所以使用下面的方法,只需要抽取时间戳就好了
 // 等同于最大延迟时间是 0 毫秒
 .assignAscendingTimestamps(_.timestamp)
 .map(r => (r.user, 1L))
 // 使用用户名对数据流进行分组
 .keyBy(_._1)
 // 设置 5 秒钟的滚动事件时间窗口
 .window(TumblingEventTimeWindows.of(Time.seconds(5)))
 // 保留第一个字段,针对第二个字段进行聚合
 .reduce((r1, r2) => (r1._1, r1._2 + r2._2))
 //把reduce换成.aggregate(new Myaggr)
 //.aggregate(new Myaggr)
 .print()
 env.execute()

//自定义窗口处理函数
 class AvgPv extends AggregateFunction[Event, (Set[String], Double), Double] {
 // 创建空累加器,类型是元组,元组的第一个元素类型为 Set 数据结构,用来对用户名进行去重
 // 第二个元素用来累加 pv 操作,也就是每来一条数据就加一
 override def createAccumulator(): (Set[String], Double) = (Set[String](), 0L)
 // 累加规则,每条数据都调用。
 override def add(value: Event, accumulator: (Set[String], Double)): 
(Set[String], Double) = (accumulator._1 + value.user, accumulator._2 + 1L)
 // 获取窗口关闭时向下游发送的结果
 override def getResult(accumulator: (Set[String], Double)): Double = 
accumulator._2 / accumulator._1.size
//两个窗口聚合
 // merge 方法只有在事件时间的会话窗口时,才需要实现,这里无需实现。
 override def merge(a: (Set[String], Double), b: (Set[String], Double)):
 (Set[String], Double) = ???
 }

 class UvCountByWindow extends ProcessWindowFunction[Event, String, String, 
TimeWindow] {
 override def process(key: String, context: Context, elements: Iterable[Event], 
out: Collector[String]): Unit = {
 // 初始化一个 Set 数据结构,用来对用户名进行去重
 var userSet = Set[String]()
 // 将所有用户名进行去重
 elements.foreach(userSet += _.user)
 // 结合窗口信息,包装输出内容
 val windowStart = context.window.getStart
 val windowEnd = context.window.getEnd
 out.collect(" 窗 口 : " + new Timestamp(windowStart) + "~" + new 
Timestamp(windowEnd) + "的独立访客数量是:" + userSet.size)
 }
 }

当窗口到达结束时间需要触发计算时,就会调用这里的 apply 方法。我们可以从 input 集合中取出窗口收集的数据,结合 key 和 window 信息,通过收集器(Collector)输出结果。这里 Collector 的用法,与 FlatMapFunction 中相同。
不过我们也看到了,WindowFunction 能提供的上下文信息较少,也没有更高级的功能。事实上,它的作用可以被 ProcessWindowFunction 全覆盖,所以之后可能会逐渐弃用。一般在实际应用,直接使用 ProcessWindowFunction就可以了。
ProcessWindowFunction 是 Window API 中最底层的通用窗口函数接口。之所以说它“最底层”,是因为除了可以拿到窗口中的所有数据之外,ProcessWindowFunction 还可以获取到一个“上下文对象”(Context)。这个上下文对象非常强大,不仅能够获取窗口信息,还可以访问当前的时间和状态信息。这里的时间就包括了处理时间(processing time)和事件时间水位线(event time watermark)。这就使得 ProcessWindowFunction 更加灵活、功能更加丰富,可以认为是一个增强版的 WindowFunction。

增量聚合和全窗口函数的结合使用

基于第一个参数(增量聚合函数)来处理窗口数据,每来一个数
据就做一次聚合;等到窗口需要触发计算时,则调用第二个参数(全窗口函数)的处理逻辑输出结果。需要注意的是,这里的全窗口函数就不再缓存所有数据了,而是直接将增量聚合函数
的结果拿来当作了 Iterable 类型的输入。一般情况下,这时的可迭代集合中就只有一个元素了。


//全窗口函数

//先收集数据到内部缓存起来。等到窗口要输出结果的时候再取出数据进行计算。
 env
	 .addSource(new ClickSource)
	 .assignAscendingTimestamps(_.timestamp)
	 // 为所有数据都指定同一个 key,可以将所有数据都发送到同一个分区
	 .keyBy(_ => "key")
	 .window(TumblingEventTimeWindows.of(Time.seconds(10)))
	 .process(new UvCountByWindow)
	 .print()
 env.execute()
 }
 // 自定义窗口处理函数
 class UvCountByWindow extends ProcessWindowFunction[Event, String, String, TimeWindow] {
	 override def process(key: String, context: Context, elements: Iterable[Event], 
out: Collector[String]): Unit = {
		 // 初始化一个 Set 数据结构,用来对用户名进行去重
		 var userSet = Set[String]()
		 // 将所有用户名进行去重
		 elements.foreach(userSet += _.user)
		 // 结合窗口信息,包装输出内容
		 val windowStart = context.window.getStart
		 val windowEnd = context.window.getEnd
		 out.collect(" 窗 口 : " + new Timestamp(windowStart) + "~" + new Timestamp(windowEnd) + "的独立访客数量是:" + userSet.size)
}

//滑动
val env = StreamExecutionEnvironment.getExecutionEnvironment
	 env.setParallelism(1)
	 env
	 .addSource(new ClickSource)
	 .assignAscendingTimestamps(_.timestamp)
	 // 使用 url 作为 key 对数据进行分区
	 .keyBy(_.url)
	 .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
	 // 注意这里调用的是 aggregate 方法
	 // 增量聚合函数和全窗口聚合函数结合使用
	 .aggregate(new UrlViewCountAgg, new UrlViewCountResult)
	 .print()
 env.execute()
 }
 class UrlViewCountAgg extends AggregateFunction[Event, Long, Long] {
	 override def createAccumulator(): Long = 0L
	 // 每来一个事件就加一
	 override def add(value: Event, accumulator: Long): Long = accumulator + 1L
	 // 窗口闭合时发送的计算结果
	 override def getResult(accumulator: Long): Long = accumulator
	 override def merge(a: Long, b: Long): Long = ???
 }
 class UrlViewCountResult extends ProcessWindowFunction[Long, UrlViewCount, String, TimeWindow] {
 // 迭代器中只有一个元素,是增量聚合函数在窗口闭合时发送过来的计算结果
 	override def process(key: String, context: Context, elements: Iterable[Long],out: Collector[UrlViewCount]): Unit = {
 	out.collect(UrlViewCount(
		 key,
 		elements.iterator.next(),
		 context.window.getStart,
		 context.window.getEnd
	 ))
 }
 }
 case class UrlViewCount(url: String, count: Long, windowStart: Long, windowEnd: Long)
}

触发器

触发器用来控制窗口什么时候触发执行窗口函数
onElement表示每一个元素到来都调用
onEventTime表示触发事件定时器时才调用
onProcessingTime表示触发处理时间定时器才调用
clear表示窗口关闭销毁调用
窗口进行操作的类型:CONTINUE啥也不干 FIRE触发 PURGE清除 FIRE_AND_PURGE触发并清除窗口

stream.keyBy(...)
 .window(...)
 .trigger(new MyTrigger())

移除器

移除器用来定义移除某些数据的逻辑
evictBefore表示执行窗口函数之前的移除数据操作,默认都是窗口函数之前移除
evictAfter表示执行窗口函数之后的数据操作

stream.keyBy(...)
 .window(...)
 .evictor(new MyEvictor())

允许延迟

解决迟到问题设置的允许的最大延迟。水位线会变成窗口结束时间+延迟时间后才真正窗口内容清空和正式关闭窗口

stream.keyBy(...)
 .window(TumblingEventTimeWindows.of(Time.hours(1)))
 .allowedLateness(Time.minutes(1))

侧输出流

处理迟到数据,将迟到数据放入侧输出流进行另外处理

//把迟到数据放到输出标签outputTag里面
//sideOutputLateData(标签名)
val stream = env.addSource(new ClickSource)
val outputTag = new OutputTag[Event]("late")
stream.keyBy("user")
 .window(TumblingEventTimeWindows.of(Time.hours(1)))
.sideOutputLateData(outputTag)

//把迟到数据拿出来 getSideOutput
val winAggStream = stream.keyBy(...)
 .window(TumblingEventTimeWindows.of(Time.hours(1)))
.sideOutputLateData(outputTag)
.aggregate(new MyAggregateFunction)
val lateStream = winAggStream.getSideOutput(outputTag)

总结

乱序数据定义水位线延迟时间—允许迟到数据–迟到了放到侧输出流里面

 // 为了方便测试,读取 socket 文本流进行处理
 val stream = env
	.socketTextStream("localhost", 7777)
	.map(data => {
 val fields = data.split(",")
 Event(fields(0).trim, fields(1).trim, fields(2).trim.toLong)
 })
 // 方式一:设置 watermark 延迟时间,2 秒钟
	.assignTimestampsAndWatermarks(WatermarkStrategy
 // 最大延迟时间设置为 5 秒钟,forBoundedOutputOrderness处理乱序数据
	.forBoundedOutOfOrderness[Event](Duration.ofSeconds(2))
	.withTimestampAssigner(new SerializableTimestampAssigner[Event] {
 // 指定时间戳是哪个字段
		override def extractTimestamp(element: Event, recordTimestamp: Long): Long = element.timestamp
 })
 )
 // 定义侧输出流标签
 val outputTag = OutputTag[Event]("late")
 val result = stream
 	.keyBy(_.url)
	 .window(TumblingEventTimeWindows.of(Time.seconds(10)))
 // 方式二:允许窗口处理迟到数据,设置 1 分钟的等待时间
	 .allowedLateness(Time.minutes(1))
 // 方式三:将最后的迟到数据输出到侧输出流
	 .sideOutputLateData(outputTag)
	 .aggregate(new UrlViewCountAgg, new UrlViewCountResult)
 // 打印输出
	 result.print("result")
	 result.getSideOutput(outputTag).print("late")
 // 为方便观察,可以将原始数据也输出
	 stream.print("input")
	 env.execute()
 }
 class UrlViewCountAgg extends AggregateFunction[Event, Long, Long] {
	 override def createAccumulator(): Long = 0L
 // 每来一个事件就加一
	 override def add(value: Event, accumulator: Long): Long = accumulator + 1L
 // 窗口闭合时发送的计算结果
	 override def getResult(accumulator: Long): Long = accumulator
	 override def merge(a: Long, b: Long): Long = ???
 }
 class UrlViewCountResult extends ProcessWindowFunction[Long,UrlViewCount, String, TimeWindow] {
 // 迭代器中只有一个元素,是增量聚合函数在窗口闭合时发送过来的计算结果
	 override def process(key: String, context: Context, elements: Iterable[Long], 
	out: Collector[UrlViewCount]): Unit = {
	 out.collect(UrlViewCount(
		 key,
		 elements.iterator.next(),
		 context.window.getStart,
		 context.window.getEnd
 	))
 }
 }

ProcessWinowFunction<IN,OUT,KEY,W> 、ProcessAllWindowFunction类似
W表示窗口类型比如TImeWindow或ProcessWindws
process(key,context,elemetn,out):process函数所需参数
processWindowFunction只有prcess和clear方法,没有onTImer

topN

val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 val eventStream = env
 .addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 // 只需要 url 就可以统计数量,所以抽取 url 转换成 String,直接开窗统计
 eventStream.map(_.url)
 // 开窗口
 .windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
 .process(new ProcessAllWindowFunction[String, String, TimeWindow] {
 override def process(context: Context, elements: Iterable[String], out: 
Collector[String]): Unit = {
126
 // 初始化一个 Map,key 为 url,value 为 url 的 pv 数据
 val urlCountMap = Map[String, Long]()
 // 将 url 和 pv 数据写入 Map 中
 elements.foreach(
 r => urlCountMap.get(r) match {
 case Some(count) => urlCountMap.put(r, count + 1L)
 case None => urlCountMap.put(r, 1L)
 }
 )
 // 将 Map 中的 KV 键值对转换成列表数据结构
 // 列表中的元素是(K,V)元组
 var mapList = new ListBuffer[(String, Long)]()
 urlCountMap.keys.foreach(
 k => urlCountMap.get(k) match {
 case Some(count) => mapList += ((k, count))
 case None => mapList
 }
 )
 // 按照浏览量数据进行降序排列
 mapList.sortBy(-_._2)
 // 拼接字符串并输出
 val result = new StringBuilder
 result.append("==================================\n")
 for (i <- 0 to 1) {
 val temp = mapList(i)
 result
 .append("浏览量 No." + (i + 1) + " ")
 .append("url: " + temp._1 + " ")
 .append("浏览量是:" + temp._2 + " ")
 .append("窗口结束时间是:" + new Timestamp(context.window.getEnd) + 
"\n")
 }
 result.append("===================================\n")
 out.collect(result.toString())
 }
 })
 .print()
 env.execute()

KeyedProcessFunction

们没有进行按键分区,直接将所有数据放在一个分区上进行
了开窗操作。这相当于将并行度强行设置为 1,在实际应用中是要尽量避免的,所以 Flink 官方也并不推荐使用 AllWindowedStream 进行处理。如果我们可以利用增量聚合函数的特性,每来一条数据就更新一次对应 url 的浏览量,那么到窗口触发计算时只需要做排序输出就可以了。
基于这样的想法,我们可以从两个方面去做优化:一是对数据进行按键分区,分别统计浏览量;二是进行增量聚合,得到结果最后再做排序输出。所以,我们可以使用增量聚合函数AggregateFunction 进行浏览量的统计,然后结合 ProcessWindowFunction 排序输出来实现 Top N
的需求。

 env.setParallelism(1)
 val eventStream = env
 .addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 // 需要按照 url 分组,求出每个 url 的访问量
 val urlCountStream = eventStream
 .keyBy(_.url)
 // 开窗口
 .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
 // 增量聚合函数和全窗口聚合函数结合使用
 // 计算结果是每个窗口中每个 url 的浏览次数
 .aggregate(new UrlViewCountAgg, new UrlViewCountResult)
 // 对结果中同一个窗口的统计数据,进行排序处理
 val result = urlCountStream
 .keyBy(_.windowEnd)
 .process(new TopN(2))
 result.print()
 env.execute()
 }
 class TopN(n: Int) extends KeyedProcessFunction[Long, UrlViewCount, String] {
 // 定义列表状态,存储 UrlViewCount 数据
 var urlViewCountListState: ListState[UrlViewCount] = _
 override def open(parameters: Configuration): Unit = {
 urlViewCountListState = getRuntimeContext.getListState(
 new ListStateDescriptor[UrlViewCount]("list-state", 
classOf[UrlViewCount]))
 }
 override def processElement(i: UrlViewCount, context: 
KeyedProcessFunction[Long, UrlViewCount, String]#Context, collector: 
collector[String]): Unit = {
 // 每来一条数据就添加到列表状态变量中
 urlViewCountListState.add(i)
 // 注册一个定时器,由于来的数据的 windowEnd 是相同的,所以只会注册一个定时器
 context.timerService.registerEventTimeTimer(i.windowEnd + 1)
 }
 override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Long, 
UrlViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
 // 导入隐式类型转换
 import scala.collection.JavaConversions._
 // 下面的代码将列表状态变量里的元素取出,然后放入 List 中,方便排序
 val urlViewCountList = urlViewCountListState.get().toList
 // 由于数据已经放入 List 中,所以可以将状态变量手动清空了
 urlViewCountListState.clear()
 // 按照浏览次数降序排列
 urlViewCountList.sortBy(-_.count)
 // 拼接要输出的字符串
 val result = new StringBuilder
 result.append("=========================\n")
 for (i <- 0 until n) {
 val urlViewCount = urlViewCountList(i)
 result
 .append("浏览量 No." + (i + 1) + " ")
 .append("url: " + urlViewCount.url + " ")
 .append("浏览量:" + urlViewCount.count + " ")
 .append("窗口结束时间:" + new Timestamp(timestamp - 1L) + "\n")
 }
 result.append("=========================\n")
 out.collect(result.toString())
 }
 }
 class UrlViewCountAgg extends AggregateFunction[Event, Long, Long] {
 override def createAccumulator(): Long = 0L
 override def add(value: Event, accumulator: Long): Long = accumulator + 1L
 override def getResult(accumulator: Long): Long = accumulator
 override def merge(a: Long, b: Long): Long = ???
 }
 class UrlViewCountResult extends ProcessWindowFunction[Long, UrlViewCount, 
String, TimeWindow] {
 override def process(key: String, context: Context, elements: Iterable[Long], 
out: Collector[UrlViewCount]): Unit = {
130
 // 迭代器中只有一条元素,就是增量聚合函数发送过来的聚合结果
 out.collect(UrlViewCount(
 key, elements.iterator.next(), context.window.getStart, 
context.window.getEnd
 ))
 }
 }
 case class UrlViewCount(url: String, count: Long, windowStart: Long, windowEnd: 
Long)

侧输出流

val stream = env.addSource(new ClickSource)
val outputTag: OutputTag[String] = OutputTag[String]("user")
val longStream = stream.process(new ProcessFunction[Event, Long] {
 override def processElement(value: Event, ctx: ProcessFunction[Event, 
Long]#Context, out: Collector[Long]) = {
 //将时间戳输出到主流中
 out.collect(value.timestamp)
 //将用户名输出到侧输出流中
 ctx.output(outputTag, "side-output: " + value.user)
 }
 
 val stringStream = longStream.getSideOutput(outputTag)

多流转换

分流

侧输出流/filter/

1.filter
val stream = env.addSource(new ClickSource)
 val maryStream = stream.filter(_.user == "Mary")
 val bobStream = stream.filter(_.user == "Bob")
 val elseStream = stream.filter(r => !(r.user == "Mary") && !(r.user == "Bob"))
 maryStream.print("Mary pv")
 bobStream.print("Bob pv")
 elseStream.print("else pv")
2.侧输出流
val maryTag = OutputTag[(String, String, Long)]("Mary-pv")
 val bobTag = OutputTag[(String, String, Long)]("Bob-pv")
 def main(args: Array[String]): Unit = {
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 val stream = env.addSource(new ClickSource)
 val processedStream = stream
 .process(new ProcessFunction[Event, Event] {
 override def processElement(value: Event, ctx: ProcessFunction[Event, 
Event]#Context, out: Collector[Event]): Unit = {
 // 将不同的数据发送到不同的侧输出流
 if (value.user == "Mary") {
 ctx.output(maryTag, (value.user, value.url, value.timestamp))
 } else if (value.user == "Bob") {
 ctx.output(bobTag, (value.user, value.url, value.timestamp))
 } else {
 out.collect(value)
 }
 }
 })
 
 // 打印各输出流中的数据
 processedStream.getSideOutput(maryTag).print("Mary pv")
 processedStream.getSideOutput(bobTag).print("Bob pv")
 processedStream.print("else pv")

合流

union、connect、broadcast
stream1.union(stream2,stream3...)
stream1.connect(stream2),只能两条流==>需要map/flatMap/process
stream1.broadcast(new MapStateDescriptor)

connect
val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 val stream1 = env.fromElements(1,2,3)
 val stream2 = env.fromElements(1L,2L,3L)
 val connectedStreams = stream1.connect(stream2)
 val result = connectedStreams
 .map(new CoMapFunction[Int, Long, String] {
 // 处理来自第一条流的事件
 override def map1(in1: Int): String = "Int: " + in1
 // 处理来自第二条流的事件
 override def map2(in2: Long): String = "Long: " + in2
 })
 result.print()
 env.execute()
 }

//coMapFunction \CoProcesFunction  \CoFlatMapFunction
 // 自定义实现 CoProcessFunction
 class OrderMatchResult extends CoProcessFunction[(String, String, Long), 
(String, String, String, Long), String] {
 // 定义状态变量,用来保存已经到达的事件;使用 lazy 定义是一种简洁的写法
 lazy val appEvent = getRuntimeContext.getState(
 new ValueStateDescriptor[(String, String, Long)]("app", classOf[(String, 
String, Long)])
 )
 lazy val thirdPartyEvent = getRuntimeContext.getState(
 new ValueStateDescriptor[(String, String, String, Long)]("third-party", 
classOf[(String, String, String, Long)])
 )
 override def processElement1(value: (String, String, Long), ctx: 
CoProcessFunction[(String, String, Long), (String, String, String, Long), 
String]#Context, out: Collector[String]): Unit = {
 if (thirdPartyEvent.value() != null) {
 // 如果对应的第三方支付事件的状态变量不为空,则说明第三方支付事件先到达,对账成功
 out.collect(value._1 + " 对账成功")
 // 清空保存第三方支付事件的状态变量
 thirdPartyEvent.clear()
 } else {
 // 如果是 app 支付事件先到达,就把它保存在状态中
 appEvent.update(value)
 // 注册 5 秒之后的定时器,也就是等待第三方支付事件 5 秒钟
 ctx.timerService.registerEventTimeTimer(value._3 + 5000L)
 }
 }
 // 和上面的逻辑是对称的关系
 override def processElement2(value: (String, String, String, Long), ctx: 
CoProcessFunction[(String, String, Long), (String, String, String, Long), 
String]#Context, out: Collector[String]): Unit = {
 if (appEvent.value() != null) {
 out.collect(value._1 + " 对账成功")
 appEvent.clear()
 } else {
 thirdPartyEvent.update(value)
 ctx.timerService.registerEventTimeTimer(value._4 + 5000L)
 }
 }
 override def onTimer(timestamp: Long, ctx: CoProcessFunction[(String, String, 
Long), (String, String, String, Long), String]#OnTimerContext, out: 
Collector[String]): Unit = {
 // 如果 app 事件的状态变量不为空,说明等待了 5 秒钟,第三方支付事件没有到达
 if (appEvent.value() != null) {
 out.collect(appEvent.value()._1 + " 对账失败,订单的第三方支付信息未到")
 appEvent.clear()
 }
 // 如果第三方支付事件没有到达,说明等待了 5 秒钟,app 事件没有到达
 if (thirdPartyEvent.value() != null) {
 out.collect(thirdPartyEvent.value()._1 + " 对账失败,订单的 app 支付信息未到")
 thirdPartyEvent.clear()
 }
 }
 }

broadcast

DataStream 调用.connect()方法时,传入的参数也可以不是一个 DataStream,而是一个“广播流”(BroadcastStream),这时合并两条流得到的就变成了一个“广播连接流”.
这种连接方式往往用在需要动态定义某些规则或配置的场景。因为规则是实时变动的,所以我们可以用一个单独的流来获取规则数据;而这些规则或配置是对整个应用全局有效的,所以不能只把这数据传递给一个下游并行子任务处理,而是要“广播”(broadcast)给所有的并行子任务。而下游子任务收到广播出来的规则,会把它保存成一个状态,这就是所谓的“广播状态”(broadcast state)。
广播状态底层是用一个“映射”(map)结构来保存的。在代码实现上,可以直接调用DataStream 的 broadcast()方法,传入一个“映射状态描述器”(MapStateDescriptor)说明状态的名称和类型,就可以得到规则数据的“广播流”(BroadcastStream):(BroadcastConnectedStream)

val ruleStateDescriptor = new MapStateDescriptor[](...);
val ruleBroadcastStream = ruleStream
 .broadcast(ruleStateDescriptor)

接下来我们就可以将要处理的数据流,与这条广播流进行连接(connect),得到的就是所谓的“广播连接流"(BroadcastConnectedStream)。基于BroadcastConnectedStream 调用 process()方法,就可以同时获取规则和数据,进行动态处理了。
这里既然调用了 process()方法,当然传入的参数也应该是处理函数大家族中一员——如果对数据流调用过 keyBy()进行了按键分区,那么要传入的就是 KeyedBroadcastProcessFunction;
如果没有按键分区,就传入 BroadcastProcessFunction。

val output = stream
 .connect(ruleBroadcastStream)
 .process( new BroadcastProcessFunction[]() {...} )
public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends 
BaseBroadcastProcessFunction {
...
 public abstract void processElement(IN1 value, ReadOnlyContext ctx, 
Collector<OUT> out) throws Exception;
 public abstract void processBroadcastElement(IN2 value, Context ctx, 
Collector<OUT> out) throws Exception;
...
}

双流联结Join

connect为联结 join为联结
join是将两条流根据某个字段的值进行配对处理,connect还需要设置定时器和自定义触发逻辑来实现

//相当于sql的内连接,只输出共有的部分
stream1.join(stream2)
 .where(<KeySelector>) //第一个流的key
 .equalTo(<KeySelector>) //第二个流的key
 .window(<WindowAssigner>) //窗口分配器:滚动tumbling,滑动sliding,会话session
 .apply(<JoinFunction>) //只能是apply,没有其他替代的

//JoinFunction
public interface JoinFunction<IN1, IN2, OUT> extends Function, Serializable {
 OUT join(IN1 first, IN2 second) throws Exception;
}

===============================
val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 val stream1 = env.fromElements(
 ("a", 1000L),
 ("b", 1000L),
 ("a", 2000L),
 ("b", 2000L)
 ).assignAscendingTimestamps(_._2)
 val stream2 = env.fromElements(
 ("a", 3000L),
 ("b", 3000L),
 ("a", 4000L),
143
 ("b", 4000L)
 ).assignAscendingTimestamps(_._2)
 stream1
 .join(stream2)
 .where(_._1) // 指定第一条流中元素的 key
 .equalTo(_._1) // 指定第二条流中元素的 key
 // 开窗口
 .window(TumblingEventTimeWindows.of(Time.seconds(5)))
 .apply(new JoinFunction[(String, Long), (String, Long), String] {
 // 处理来自两条流的相同 key 的事件
 override def join(first: (String, Long), second: (String, Long)) = {
 first + "=>" + second
 }
 }).print()
 env.execute()

间隔联结

一种基于KeyedStream的合流操作,就是针对一条流的每个数据,开辟出其时间戳前后的一段时间间隔,看这期间是否有来自另一条流的数据匹配。(就是加了个上届和下界,加了时间范围(间隔)的联结)
a.timestamp + lowerBound <= b.timestamp <= a.timestamp + upperBound
因为这种给流中数据划定范围的方式导致会有重复数据。intervalJoin上图所示,下面的流的每个数据都在主动范围联结上面的流,2这个数据有1,0,3这条数据有1,重复了1。

stream1
 .keyBy(_._1)
 .intervalJoin(stream2.keyBy(_._1))
 .between(Time.milliseconds(-2),Time.milliseconds(1))
 .process(new ProcessJoinFunction[(String, Long), (String, Long), String] {
 override def processElement(left: (String, Long), right: (String, Long), ctx: 
ProcessJoinFunction[(String, Long), (String, Long), String]#Context, out: 
Collector[String]) = {
//每当匹配一条数据就会调用processElement方法。
 out.collect(left + "," + right)
 }
 });
 ========================
  val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 // 订单事件流
 val orderStream: DataStream[(String, String, Long)] = env
 .fromElements(
 ("Mary", "order-1", 5000L),
 ("Alice", "order-2", 5000L),
 ("Bob", "order-3", 20000L),
 ("Alice", "order-4", 20000L),
 ("Cary", "order-5", 51000L)
 ).assignAscendingTimestamps(_._3)

 // 点击事件流
 val pvStream: DataStream[Event] = env
 .fromElements(
 Event("Bob", "./cart", 2000L),
 Event("Alice", "./prod?id=100", 3000L),
 Event("Alice", "./prod?id=200", 3500L),
 Event("Bob", "./prod?id=2", 2500L),
 Event("Alice", "./prod?id=300", 36000L),
 Event("Bob", "./home", 30000L),
 Event("Bob", "./prod?id=1", 23000L),
 Event("Bob", "./prod?id=3", 33000L)
 ).assignAscendingTimestamps(_.timestamp)
 // 两条流进行间隔联结,输出匹配结果
 orderStream
 .keyBy(_._1)
 .intervalJoin(pvStream.keyBy(_.user))
 // 指定间隔
 .between(Time.minutes(-5), Time.minutes(10))
 .process(
 new ProcessJoinFunction[(String, String, Long), Event, String] {
 override def processElement(left: (String, String, Long), right: Event, 
ctx: ProcessJoinFunction[(String, String, Long), Event, String]#Context, out: 
Collector[String]): Unit = {
 out.collect(left + "=>" + right)
 }
 })
 .print()
 env.execute()

状态编程

用来输出所有数据的结果。
无状态的算子,根据输入直接转换输出。map,filter,flatMap,不依赖其他数据
有状态的算子,需要一些其他数据(比如上一条数据的结果)来得到计算结果。窗口算子会保存已经到达的所有数据。
但是因为大数据场景,我们必须是在分布式框架下,在低延迟,高吞吐还要保证容错:(检查点checkpoint,保存点savepoint,状态一致性端到端精准一次),所以需要设置这些来管理状态,避免程序的正确性降低。
如果用不上状态也就不需要容错,因为来一条处理一条。

状态分为托管和原始两种:
托管就是flink统一管理,配置容错机制后,会自动持久化保存并在故障时自动回复。
原始状态是用字节保存,需要花费大量精力处理状态的管理和维护,不建议原始状态。直接flink的一整套容错机制就行。

  1. 值状态 列表状态 map状态
    状态的类型是string/int 还是List 或者Map。
namefunction
valueStatevalue获取 update更新
listStateget获取 update更新 add添加 addAll添加多个
MapStateget(key)获取 put(k,v)添加 putAll(Map(k,v))添加多个 remove(k)删除 contains(k)是否存在 entries获取所有 keys所有键 values所有值
reducingStateadd不是添加而是和旧状态归约,得到新状态
aggregatingState
RuntimeContextgetState获取valueState状态 getMapState获取Map状态 getListState获取List状态 getReducingState获取 getAggregatingState
ttlnewBuilder构造器方法 setUpdateType失效时间(OnCreateAndWrite只有创建和更改更新时效,OnReadAndWrite读写操作都会更新时效时间,就是一直活跃,寿命一直延长) setStateVisiblity状态可见性(NeverReturnExpired默认过期就不能读,ReturnExpireDefNotCleanUp过期但存在就能读) build生成StateTtlConfig
  1. 按键分区状态和算子状态
    作用范围不同,算子状态是当前的算子任务实例。状态对于同一个任务共享。 按键分区是key来维护和管理,只有keyBy后才可使用。
    按键分区使用很广泛。为什么sum/max/min/reduce只能keyBy才能使用,因为结果使用KeyedState来存储的,没有keyedState状态没法存前后结果。
    另外RichFunction来自定义KeyedState,只要提供了富函数接口算子,也可以使用keyedState。比如map,filter.或者实现CheckpointedFunction接口定义OperatorState。
    所以flink的所有算子都可以有状态。
    无论是 Keyed State 还是 Operator State,它们都是在本地实例上维护的,也就是说每个并
    行子任务维护着对应的状态,算子的子任务之间状态不共享。

  2. 托管状态和原始状态
    托管就是flink的容错机制,原始需要自定义,不建议使用

//valueState
val descriptor = new ValueStateDescriptor[Long](
"my state", // 状态名称
classOf[Long] // 状态类型
)
==================================================
//自定义RichFunction
class MyFlatMapFunction extends RichFlatMapFunction[Long, String] {
 // 声明状态,不用lazy懒加载的话会在生命周期开始就初始化状态变量。

lazy val state = getRuntimeContext.getState(new 
ValueStateDescriptor[Long]("my state", classOf[Long]))
 override defflatMap(input: Long, out: Collector[String] ): Unit = {
 // 访问状态
 var currentState = state.value()
 currentState += 1 // 状态数值加 1
 // 更新状态
 state.update(currentState)
 if (currentState >= 100) {
 out.collect("state: " + currentState)
 state.clear() // 清空状态
 }
 } }

========valueState=======
env
 .addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 .keyBy(_.user) // 按照用户分组
 .process(new PeriodicPvResult) // 自定义 KeyedProcessFunction 进行处理
 .print()
 env.execute()
 }
 // 注册定时器,周期性输出 pv
 class PeriodicPvResult extends KeyedProcessFunction[String, Event, String] {
 // 懒加载值状态变量,用来储存当前 pv 数据
 lazy val countState = getRuntimeContext.getState(
 new ValueStateDescriptor[Long]("count", classOf[Long])
 )
 // 懒加载状态变量,用来储存发送 pv 数据的定时器的时间戳
 lazy val timerTsState = getRuntimeContext.getState(
 new ValueStateDescriptor[Long]("timer-ts", classOf[Long])
 )
 override def processElement(value: Event, ctx: KeyedProcessFunction[String, 
Event, String]#Context, out: Collector[String]): Unit = {
 // 更新 count 值
 val count = countState.value()
 countState.update(count + 1)
 // 如果保存发送 pv 数据的定时器的时间戳的状态变量为 0L,则注册一个 10 秒后的定时器
 if (timerTsState.value() == 0L) {
 // 注册定时器
 ctx.timerService.registerEventTimeTimer(value.timestamp + 10 * 1000L)
 // 将定时器的时间戳保存在状态变量中
 timerTsState.update(value.timestamp + 10 * 1000L)
 }
 }
 override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, Event, 
String]#OnTimerContext, out: Collector[String]): Unit = {
 // 定时器触发,向下游输出当前统计结果
 out.collect("用户 " + ctx.getCurrentKey + " 的 pv 是:" + countState.value())
 // 清空保存定时器时间戳的状态变量,这样新数据到来时又可以注册定时器了
 timerTsState.clear()
 }
 } }
 ====ListState=======
  val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 val stream1 = env
 .fromElements(
 ("a", "stream-1", 1000L),
 ("b", "stream-1", 2000L)
 )
 .assignAscendingTimestamps(_._3)
 val stream2 = env
 .fromElements(
 ("a", "stream-2", 3000L),
 ("b", "stream-2", 4000L)
 )
 .assignAscendingTimestamps(_._3)
 stream1.keyBy(_._1)
 // 连接两条流
 .connect(stream2.keyBy(_._1))
 .process(new CoProcessFunction[(String, String, Long), (String, String, 
Long), String] {
 // 用来保存来自第一条流的事件的列表状态变量
 lazy val stream1ListState = getRuntimeContext.getListState(
 new ListStateDescriptor[(String, String, Long)]("stream1-list", 
classOf[(String, String, Long)])
 )
 // 用来保存来自第二条流的事件的列表状态变量
 lazy val stream2ListState = getRuntimeContext.getListState(
 new ListStateDescriptor[(String, String, Long)]("stream2-list", 
classOf[(String, String, Long)])
 )
 // 处理来自第一条流的事件
 override def processElement1(left: (String, String, Long), context: 
CoProcessFunction[(String, String, Long), (String, String, Long), String]#Context, 
collector: Collector[String]): Unit = {
 // 将事件添加到列表状态变量
 stream1ListState.add(left)
 // 导入隐式类型转换
 import scala.collection.JavaConversions._
 // 当前事件和第二条流的已经到达的事件做联结
 for (right <- stream2ListState.get) {
 collector.collect(left + " => " + right)
 }
 }
 // 处理来自第二条流的事件
 override def processElement2(right: (String, String, Long), context: 
CoProcessFunction[(String, String, Long), (String, String, Long), String]#Context, 
collector: Collector[String]): Unit = {
 // 将事件添加到列表状态变量中
 stream2ListState.add(right)
 import scala.collection.JavaConversions._
 // 当前事件和第一条流的已经到达的事件做联结
 for (left <- stream1ListState.get) {
 collector.collect(left + " => " + right)
 }
 }
 })
 .print()
 env.execute()
============MapState============
 env
 .addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 .keyBy(_.url)
 .process(new FakeWindowResult(10000L))
 .print()
 env.execute()
 }
 // 自定义 KeyedProcessFunction 实现滚动窗口功能
 class FakeWindowResult(windowSize: Long) extends KeyedProcessFunction[String, 
Event, String] {
 // 初始化一个 MapState 状态变量,key 为窗口的开始时间,value 为窗口对应的 pv 数据
 lazy val windowPvMapState = getRuntimeContext.getMapState(
 new MapStateDescriptor[Long, Long]("window-pv", classOf[Long], 
classOf[Long])
 )
 override def processElement(value: Event, ctx: KeyedProcessFunction[String, 
Event, String]#Context, out: Collector[String]): Unit = {
 // 根据事件的时间戳,计算当前事件所属的窗口开始和结束时间
 val windowStart = value.timestamp / windowSize * windowSize
 val windowEnd = windowStart + windowSize
 // 注册一个 windowEnd - 1ms 的定时器,用来触发窗口计算
 ctx.timerService.registerEventTimeTimer(windowEnd - 1)
 // 更新状态中的 pv 值
 if (windowPvMapState.contains(windowStart)) {
 val pv = windowPvMapState.get(windowStart)
 windowPvMapState.put(windowStart, pv + 1L)
 } else {
 // 如果 key 不存在,说明当前窗口的第一个事件到达
 windowPvMapState.put(windowStart, 1L)
 }
 }
 // 定时器触发,直接输出统计的 pv 结果
 override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, Event, 
String]#OnTimerContext, out: Collector[String]): Unit = {
 // 计算窗口的结束时间和开始时间
 val windowEnd = timestamp + 1L
 val windowStart = windowEnd - windowSize
 // 发送窗口计算的结果
 out.collect( "url: " + ctx.getCurrentKey()
 + " 访问量: " + windowPvMapState.get(windowStart)
 + " 窗口:" + new Timestamp(windowStart) + " ~ " + new Timestamp(windowEnd))
 // 模拟窗口的销毁,清除 map 中的 key
 windowPvMapState.remove(windowStart)
 }
 }
========aggregatingState=============
 env
 .addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 .keyBy(_.user)
 .flatMap(new AvgTsResult)
 .print()
 env.execute()
 }
 // 自定义 RichFlatMapFunction
 class AvgTsResult extends RichFlatMapFunction[Event, String]{
 // 定义聚合状态,用来计算平均时间戳,中间累加器保存一个(sum, count)二元组
 lazy val avgTsAggState = getRuntimeContext.getAggregatingState(
 new AggregatingStateDescriptor[Event, (Long, Long), Long](
 "avg-ts", // 状态变量的名字
 new AggregateFunction[Event, (Long, Long), Long] {
 override def add(value: Event, accumulator: (Long, Long)): (Long, Long) 
=
 (accumulator._1 + value.timestamp, accumulator._2 + 1)
 override def createAccumulator(): (Long, Long) = (0L, 0L)
 override def getResult(accumulator: (Long, Long)): Long = accumulator._1 
/ accumulator._2
 override def merge(a: (Long, Long), b: (Long, Long)): (Long, Long) = ???
 }, // 增量聚合函数的定义,定义了聚合的逻辑
 classOf[(Long, Long)] // 累加器的类型
 )
 )
 // 定义一个值状态,用来保存当前用户访问频次
 lazy val countState = getRuntimeContext.getState(
 new ValueStateDescriptor[Long]("count", classOf[Long])
 )
 override def flatMap(value: Event, out: Collector[String]): Unit = {
 // 更新 count 值
 val count = countState.value()
 countState.update(count + 1)
 // 增量聚合
 avgTsAggState.add(value)
 // 达到 5 次就输出结果,并清空状态
 if (count == 5){
 out.collect(value.user + " 平均时间戳: " + new 
Timestamp(avgTsAggState.get()))
 countState.clear()
 }
 }
  
=======TTL=======
val ttlConfig = StateTtlConfig
 .newBuilder(Time.seconds(10))
 
 .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
 .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
 .build()
val stateDescriptor = new ValueStateDescriptor[String](
 "my-state",
 classOf[String]
 )
 //开启TTL
stateDescriptor.enableTimeToLive(ttlConfig)

============checkpointedFunction============
val descriptor = new ListStateDescriptor[String](
 "buffer-elements",
 classOf[String]
 )
val checkpointedState = context.getOperatorStateStore().getListState(descriptor)
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 env.addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 .addSink(new BufferingSink(10))
 env.execute()
 }
 //实现 SinkFunction 和 CheckpointedFunction 这两个接口
 class BufferingSink(threshold: Int) extends SinkFunction[Event]
 with CheckpointedFunction {
 private var checkpointedState: ListState[Event] = _
 private val bufferedElements = ListBuffer[Event]()
 override def invoke(value: Event): Unit = super.invoke(value)
 // 每来一条数据调用一次 invode()方法
 override def invoke(value: Event, context: Context): Unit = {
 // 将数据先缓存起来
 bufferedElements += value
 // 当缓存中的数据量到达了阈值,执行 sink 逻辑
 if (bufferedElements.size == threshold) {
 for (element <- bufferedElements) {
 // 输出到外部系统,这里用控制台打印模拟
 println(element)
 }
 println("==========输出完毕=========")
 // 清空缓存
 bufferedElements.clear()
 }
 }
 // 对状态做快照
 override def snapshotState(context: FunctionSnapshotContext): Unit = {
 checkpointedState.clear() // 清空状态变量
 // 把当前局部变量中的所有元素写入到检查点中
 for (element <- bufferedElements) {
 // 将缓存中的数据写入状态变量
 checkpointedState.add(element)
 }
 }
 // 初始化算子状态变量
 override def initializeState(context: FunctionInitializationContext): Unit = 
{
 val descriptor = new ListStateDescriptor[Event](
 "buffered-elements",
 classOf[Event]
 )
 // 初始化状态变量
 checkpointedState = context.getOperatorStateStore.getListState(descriptor)
 // 如果是从故障中恢复,就将 ListState 中的所有元素添加到局部变量中
 if (context.isRestored) {
 import scala.collection.JavaConversions._
 for (element <- checkpointedState.get()) {
 bufferedElements += element
 }
 }
 }
 } }

算子状态也支持不同的结构类型,主要有三种:ListState、UnionListState 和 BroadcastState。
在 Flink 中,对状态进行持久化保存的快照机制叫作“检查点"(Checkpoint)。于是使用算子状态时,就需要对检查点的相关操作进行定义,实现一个 CheckpointedFunction 接口。
每次应用保存检查点做快照时,都会调用 snapshotState()方法,将状态进行外部持久化。而在算子任务进行初始化时,会调initializeState()方法。这又有两种情况:一种是整个应用第一次运行,这时状态会被初始化为一个默认值(default value);另一种是应用重启时,从检查点(checkpoint)或者保存点(savepoint)中读取之前状态的快照,并赋给本地状态。这里需要注意,CheckpointedFunction 接口中的两个方法,分别传入了一个上下文(context)作为参数。不同的是,snapshotState()方法拿到的是快照的上下文FunctionSnapshotContext,它可以提供检查点的相关信息,不过无法获取状态句柄;而 initializeState()方法拿到的是FunctionInitializationContext,这是函数类进行初始化时的上下文,是真正的 “运行时上下文”。FunctionInitializationContext 中提供了“算子状态存储”(OperatorStateStore)和“按键分区状态存储”(KeyedStateStore),在这两个存储对象中可以非常方便地获取当前任务实例中的Operator State 和 Keyed State。
当初始化好状态对象后,我们可以通过调用 isRestored()方法判断当前程序是从故障中恢复的还是第一次启动。在代码中初始化 BufferingSink 时,恢复出的 ListState 的所有元素会添加到一个局部变量 bufferedElements 中,以后进行检查点快照时就可以直接使用了。在调用snapshotState()时,直接清空 ListState,然后把当前局部变量中的所有元素写入到检查点中。

检查点:

val env = StreamExecutionEnvironment.getEnvironment
env.enableCheckpointing(1000)

除了检查点之外,Flink 还提供了“保存点”(savepoint)的功能。保存点在原理和形式上跟检查点完全一样,也是状态持久化保存的一个快照;区别在于,保存点是自定义的镜像保存,所以不会由 Flink 自动创建,而需要用户手动触发。这在有计划地停止、重启应用时非常有用

状态后端:

val env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new HashMapStateBackend())
env.setStateBackend(new EmbeddedRocksDBStateBackend())

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-statebackend-rocksdb_2.11</artifactId>
 <version>1.13.0</version>
</dependency>

容错机制

  1. 检查点
========启用检查点
val env=StreamExecutionEnvironment.getExecutionEnvironment
// 每隔 1 秒启动一次检查点保存
env.enableCheckpointing(1000)
========检查点存储
// 配置存储检查点到 JobManager 堆内存
env.getCheckpointConfig.setCheckpointStorage(new JobManagerCheckpointStorage)
// 配置存储检查点到文件系统
env.getCheckpointConfig.setCheckpointStorage(new 
FileSystemCheckpointStorage("hdfs://namenode:40010/flink/checkpoints"))

========检查点高级配置
val checkpointConfig = env.getCheckpointConfig
// 设置精确一次模式
//at-least-once至少一次   exactly-once精准一次,默认精准一次
//对于低延迟流处理程序,at-least-once够用且处理效率更高。
checkpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// 最小间隔时间 500 毫秒,上一个检查点完成之后,等多久保存下一个检查点。
//只要设了这个参数,setMaxConcurrentCheckpoints强制为1
checkpointConfig.setMinPauseBetweenCheckpoints(500)
// 超时时间 1 分钟,超过保存时间就丢掉。
checkpointConfig.setCheckpointTimeout(60000)
// 同时只能有一个检查点,设置并发检查点数量。
checkpointConfig.setMaxConcurrentCheckpoints(1)
// 开启检查点的外部持久化保存,作业取消后依然保留
checkpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)
//DELETE_ON_ CANCELLATION表示任务取消的时候自动删除外部检查点。如果任务失败退出,自动保留检查点
//DETAIN_ON_CANCELLATION:表示取消的时候也保留外部检查点

// 启用不对齐的检查点保存方式,要求检查点模式必须为exctly-once,并且并发数为1,可以大大减少检查点保存时间
checkpointConfig.enableUnalignedCheckpoints
// 设置检查点存储,可以直接传入一个 String,指定文件系统的路径
checkpointConfig.setCheckpointStorage("hdfs://my/checkpoint/dir")
  1. 保存点
    检查点是有flink自动管理,定期创建,发生故障自动读取进行回复。
    保存点手动触发保存操作。更加灵活,手动备份。
    使用uid(ID)保存,没有指定ID的话flink会自动设置,但重启可能会因为ID不同无法兼容,所以强烈建议为每个算子手动指定ID
val stream = env
 .addSource(new StatefulSource)
 .uid("source-id")
 .map(new StatefulMapper)
 .uid("mapper-id")
 .print()
  1. 使用保存点
    bin/flink savepoint :jobId [:targetDirectory]为运行的作业创建一个保存点镜像
    state.savepoints.dir: hdfs:///flink/savepoints设置flink-conf.yaml的保存点默认路径
    env.setDefaultSavepointDir("hdfs:///flink/savepoints")单独的作业,在程序中设置路径
    bin/flink stop --savepointPath [:targetDIrectory] :jobId停掉一个作业直接创建保存点
    bin/flink run -s :savepointPath [:runArgs]从保存点重启应用

状态一致性

一致性检查点是flink容错机制的核心。
事务实现一致性,表示结果正确性
分布式系统强调的一致性是相同数据的副本应该总是一致的,保证计算结果正确+不漏掉任何一个数据+不会重复处理同一个数据。
流式数据正常来讲肯定是正确的,但是发生故障且需要恢复状态进行回滚的时候就有可能出错。
通过检查点的保存状态回复后结果的正确来讨论状态的一致性。

namefun
最多一次at-most-once故障直接重启 ,直接丢掉
至少一次at-least-once不能保证重复处理问题,适合去重那种
精准一次exactly-once正好一次,数据不丢,跟没故障一样。

我觉得是通过检查点保证至少一次(数据不丢),然后检查点数据判断达到正好一次。而这个判断就需要从输入和输出进行判断有没有处理过,重放的数据引起的状态改变有没有包含在里面。
输入端保证就用故障恢复,就是需要数据源有重放数据的能力,比如kafka的偏移量保存为状态。保证数据至少一次
输出端需要sink端有幂等写入(重复次数不影响结果)或事务写入(保证数据不重复。

事务写入:可以根据检查点来提交和回滚,当sink遇到阻碍就开始保存状态并开启事务,保存点保存完毕,再提交事务,故障的话会回退到上一个检查点,事务也被回滚撤销)
事务写入包括预写和两阶段提交
预写WAL:结果数据作为日志保存-检查点保存时结果也持久化–检查点完成后数据一次写入外部系统并将–提交操作写入日志。 可以看出性能有问题,但适合单机数据库。而且当已经写入了外部系统但在最终保存确认的时候出故障会导致flink重复写入。
两阶段提交2PC:先准备然后再提交。当数据到来或收到检查点的分界线,sink会开启事务,然后接收数据,因为事务开启所以现在是预提交—sink收到jm发送的检查点完成的通知时就提交事务,才是真正提交。而如果中间有任何一台发生故障都可以事务回滚。充分利用检查点机制,没有写日志的批处理的性能问题,只需要一个确认信息。但是缺点就是要求比预写高。

面试题:二阶段提交的外部系统要求

两阶段提交虽然精巧,却对外部系统有很高的要求。这里将 2PC 对外部系统的要求
列举如下:
⚫ 外部系统必须提供事务支持,或者 Sink 任务必须能够模拟外部系统上的事务。
⚫ 在检查点的间隔期间里,必须能够开启一个事务并接受数据写入。
⚫ 在收到检查点完成的通知之前,事务必须是“等待提交”的状态。在故障恢复的情况
下,这可能需要一些时间。如果这个时候外部系统关闭事务(例如超时了),那么未提交的数据就会丢失。
⚫ Sink 任务必须能够在进程失败后恢复事务。
⚫ 提交事务必须是幂等操作。也就是说,事务的重复提交应该是无效的。
可见,2PC 在实际应用同样会受到比较大的限制。具体在项目中的选型,最终还应该是一致性级别和处理性能的权衡考量。

flink和kafka的精准一次

kafka的可重置偏移量是很优秀的保证精准一次的输入源,最后只要保证输出端的精准一次,分布式当然用2PC了,当然也可以用和flink天生一对kafka当输出端
exact-once配置:

  1. 启动检查点
  2. kafka的producer设为精准一次
  3. 设置消费者隔离级别,默认是read_uncommited可以读取未提交的数据,应该设为read_commited遇到未提交数据会停止从分区消费数据
  4. 事务超时配置:Flink 的 Kafka连接器中配置的事务超时时间 transaction.timeout.ms 默认是 1小时,而Kafka
    集群配置的事务最大超时时间 transaction.max.timeout.ms 默认是 15 分钟。这两个超时时间,前者应该小于等于后者。

tableApi

val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 // 读取数据源
 val eventStream = env
 .fromElements(
 Event("Alice", "./home", 1000L),
 Event("Bob", "./cart", 1000L),
 Event("Alice", "./prod?id=1", 5 * 1000L),
 Event("Cary", "./home", 60 * 1000L),
 Event("Bob", "./prod?id=3", 90 * 1000L),
 Event("Alice", "./prod?id=7", 105 * 1000L)
 )
 // 获取表环境
 val tableEnv = StreamTableEnvironment.create(env)
 // 将数据流转换成表
 val eventTable = tableEnv.fromDataStream(eventStream)
 // 用执行 SQL 的方式提取数据
 val visitTable = tableEnv.sqlQuery("select url, user from " + eventTable)
 // 将表转换成数据流,打印输出
 tableEnv.toDataStream(visitTable).print()
 // 执行程序
 import org.apache.flink.table.api.Expressions.$
 val clickTable2 = eventTable.select($("url"), $("user"))
 env.execute()

// 创建输入表,连接外部系统读取数据
tableEnv.executeSql("CREATE TEMPORARY TABLE inputTable ... WITH ( 'connector' 
= ... )")
// 注册一个表,连接到外部系统,用于输出
tableEnv.executeSql("CREATE TEMPORARY TABLE outputTable ... WITH ( 'connector' 
= ... )")
// 执行 SQL 对表进行查询转换,得到一个新的表
val table1 = tableEnv.sqlQuery("SELECT ... FROM inputTable... ")

// 使用 Table API 对表进行查询转换,得到一个新的表
val table2 = tableEnv.from("inputTable").select(...)
// 将得到的结果写入输出表
val tableResult = table1.executeInsert("outputTable")

val settings = EnvironmentSettings
 .newInstance()
 .inStreamingMode() // 使用流处理模式
 .build()
val tableEnv = TableEnvironment.create(settings)

tEnv.useCatalog("custom_catalog")
tEnv.useDatabase("custom_database")

tableAPI创建环境

//1.
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 val tableEnv = StreamTableEnvironment.create(env)

//2,
val settings = EnvironmentSettings
 .newInstance()
 .inStreamingMode() // 使用流处理模式
 .build()
val tableEnv = TableEnvironment.create(settings)

//设置库
tEnv.useCatalog("custom_catalog")
tEnv.useDatabase("custom_database")

tableAPI建表

//从本地数据 case class
val eventStream = env
 .fromElements(
 Event("Alice", "./home", 1000L),
 Event("Bob", "./cart", 1000L),
 Event("Alice", "./prod?id=1", 5 * 1000L),
 Event("Cary", "./home", 60 * 1000L),
 Event("Bob", "./prod?id=3", 90 * 1000L),
 Event("Alice", "./prod?id=7", 105 * 1000L)
 )
 val eventTable = tableEnv.fromDataStream(eventStream)
 val visitTable = tableEnv.sqlQuery("select url, user from " + eventTable)
 tableEnv.toDataStream(visitTable).print()
// 创建输入表,连接外部系统读取数据
tableEnv.executeSql("CREATE TEMPORARY TABLE inputTable ... WITH ( 'connector' 
= ... )")
tableEnv.createTemporaryView("NewTable", newTable)
val eventTable = tableEnv.from("EventTable")
// 注册一个表,连接到外部系统,用于输出
tableEnv.executeSql("CREATE TEMPORARY TABLE outputTable ... WITH ( 'connector' 
= ... )")

//从addSource
val eventStream = env.addSource(...)
tableEnv.fromDataStream(eventStream,[$("").as(""),$("")])
tableEnv.createTemporaryView("EventTable", eventStream, 
$("timestamp").as("ts"),$("url"));

//tuple
val table = tableEnv.fromDataStream(stream, $("_2").as("myInt"), 
$("_1").as("myLong"))

//case class
val table = tableEnv.fromDataStream(stream, $("user").as("myUser"), 
$("url").as("myUrl"))

// row
env.fromElements(
 Row.ofKind(RowKind.INSERT, "Alice", 12),
 Row.ofKind(RowKind.INSERT, "Bob", 5),
 Row.ofKind(RowKind.UPDATE_BEFORE, "Alice", 12),
 Row.ofKind(RowKind.UPDATE_AFTER, "Alice", 100))
// 将更新日志流转换为表
val table = tableEnv.fromChangelogStream(dataStream)

tableAPI处理数据

// 执行 SQL 对表进行查询转换,得到一个新的表
//使用sql
tableEnv.createTemporaryView("NewTable", newTable)
val table1 = tableEnv.sqlQuery("SELECT ... FROM inputTable... ")
201


// 使用 Table API 对表进行查询转换,得到一个新的表
//不用sql
 import org.apache.flink.table.api.Expressions.$
val table2 = tableEnv.from("inputTable").select(...)
val eventTable = tableEnv.from("EventTable")
val maryClickTable = eventTable
 .where($("user").isEqual("Alice"))
 .select($("url"), $("user"))

val clickTable2 = eventTable.select($("url"), $("user"))

tableAPI保存数据

//打印出来
//只有简单变换,没有统计
tableEnv.toDataStream(aliceVisitTable).print()
//有统计,count,sum,gourpby
//输出十更新日志
tableEnv.toChangelogStream(tableEnv.sqlQuery()).print()

// 将得到的结果写入输出表
val tableResult = table1.executeInsert("outputTable")


例子

val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 // 读取数据源,并分配时间戳、生成水位线
 val eventStream = env
 .fromElements(
 Event("Alice", "./home", 1000L),
 Event("Bob", "./cart", 1000L),
 Event("Alice", "./prod?id=1", 25 * 60 * 1000L),
 Event("Alice", "./prod?id=4", 55 * 60 * 1000L),
 Event("Bob", "./prod?id=5", 3600 * 1000L + 60 * 1000L),
 Event("Cary", "./home", 3600 * 1000L + 30 * 60 * 1000L),
 Event("Cary", "./prod?id=7", 3600 * 1000L + 59 * 60 * 1000L)
 )
 .assignAscendingTimestamps(_.timestamp)
 // 创建表环境
 val tableEnv = StreamTableEnvironment.create(env)
 // 将数据流转换成表,并指定时间属性
 val eventTable = tableEnv.fromDataStream(
 eventStream,
 $("user"),
 $("url"),
 $("timestamp").rowtime().as("ts")
 // 将 timestamp 指定为事件时间,并命名为 ts
 )
 // 为方便在 SQL 中引用,在环境中注册表 EventTable
 tableEnv.createTemporaryView("EventTable", eventTable);
 // 设置 1 小时滚动窗口,执行 SQL 统计查询
 val result = tableEnv
 .sqlQuery(
 "SELECT " +
 "user, " +
 "window_end AS endT, " + // 窗口结束时间
 "COUNT(url) AS cnt " + // 统计 url 访问次数
 "FROM TABLE( " +
 "TUMBLE( TABLE EventTable, " + // 1 小时滚动窗口
 "DESCRIPTOR(ts), " +
 "INTERVAL '1' HOUR)) " +
 "GROUP BY user, window_start, window_end "
 )
 tableEnv.toDataStream(result).print()
 env.execute()
 }

tableAPI水位线

Flink 中支持的事件时间属性数据类型必须为 TIMESTAMP 或者 TIMESTAMP_LTZ。这里TIMESTAMP_LTZ 是指带有本地时区信息的时间戳(TIMESTAMP WITH LOCAL TIME ZONE);一般情况下如果数据中的时间戳是“年-月-日-时-分-秒”的形式,那就是不带时区信息的,可以将事件时间属性定义为 TIMESTAMP 类型。

//创建表的时候就定义水位线

CREATE TABLE events (
 user STRING,
 url STRING,
 ts BIGINT,
 ts_ltz AS TO_TIMESTAMP_LTZ(ts, 3),
 WATERMARK FOR ts_ltz AS time_ltz - INTERVAL '5' SECOND
) WITH (
 ...
);

// 方法一:
// 流中数据类型为二元组 Tuple2,包含两个字段;需要自定义提取时间戳并生成水位线
val stream = inputStream.assignTimestampsAndWatermarks(...)
// 声明一个额外的逻辑字段作为事件时间属性
val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").rowtime())
// 方法二:
// 流中数据类型为三元组 Tuple3,最后一个字段就是事件时间戳
val stream = inputStream.assignTimestampsAndWatermarks(...)
// 不再声明额外字段,直接用最后一个字段作为事件时间属性
val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").rowtime())

处理时间就比较简单,直接就是系统时间,不需要提取时间戳和生成水位线,所以必须额外声明一个字段,专门保存当前的处理时间。

CREATE TABLE EventTable(
 user STRING,
 url STRING,
 ts AS PROCTIME()
) WITH (
 ...
);

val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").proctime())

tableAPI窗口

val result = tableEnv.sqlQuery(
 "SELECT " +
 "user, " +
"TUMBLE_END(ts, INTERVAL '1' HOUR) as endT, " +
 "COUNT(url) AS cnt " +
 "FROM EventTable " +
 "GROUP BY " + // 使用窗口和用户名进行分组
 "user, " +
 "TUMBLE(ts, INTERVAL '1' HOUR)" // 定义 1 小时滚动窗口
 )
namedun
滚动tumbleTUMBLE(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOUR)
滑动hopHOP(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '5' MINUTES, INTERVAL '1' HOURS));
累积cumulateCUMULATE(TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOURS, INTERVAL '1' DAYS))
会话暂不支持

tableAPI分组聚合

sum/max/min/avg/count


//根据时间的持续增长,导致key不断增加,需要设置ttl防止耗尽资源

//结果准确性会低,但为了资源消耗,这是值得的

//1.
val tableConfig = tableEnv.getConfig();
// 配置状态保持时间
tableConfig.setIdleStateRetention(Duration.ofMinutes(60))
//2.
val tableEnv = ...
val configuration = tableEnv.getConfig().getConfiguration()
configuration.setString("table.exec.state.ttl", "60 min")


//分组聚合
val result = tableEnv.sqlQuery(
 "SELECT " +
 "user, " +
 "window_end AS endT, " +
 "COUNT(url) AS cnt " +
 "FROM TABLE( " +
 "TUMBLE( TABLE EventTable, " +
 "DESCRIPTOR(ts), " +
 "INTERVAL '1' HOUR)) " +
 "GROUP BY user, window_start, window_end "
 )


//例子
 .fromElements(
 Event("Alice", "./home", 1000L),
 Event("Bob", "./cart", 1000L),
 Event("Alice", "./prod?id=1", 25 * 60 * 1000L),
 Event("Alice", "./prod?id=4", 55 * 60 * 1000L),
 Event("Bob", "./prod?id=5", 3600 * 1000L + 60 * 1000L),
 Event("Cary", "./home", 3600 * 1000L + 30 * 60 * 1000L),
 Event("Cary", "./prod?id=7", 3600 * 1000L + 59 * 60 * 1000L)
 )
 .assignAscendingTimestamps(_.timestamp)
 // 创建表环境
 val tableEnv = StreamTableEnvironment.create(env)
 // 将数据流转换成表,并指定时间属性
 val eventTable = tableEnv.fromDataStream(
 eventStream,
 $("user"),
 $("url"),
 $("timestamp").rowtime().as("ts")
 )
 // 为方便在 SQL 中引用,在环境中注册表 EventTable
 tableEnv.createTemporaryView("EventTable", eventTable);
 // 设置累积窗口,执行 SQL 统计查询
 val result = tableEnv
 .sqlQuery(
 "SELECT " +
 "user, " +
 "window_end AS endT, " +
 "COUNT(url) AS cnt " +
 "FROM TABLE( " +
 "CUMULATE( TABLE EventTable, " + // 定义累积窗口
 "DESCRIPTOR(ts), " +
 "INTERVAL '30' MINUTE, " +
 "INTERVAL '1' HOUR)) " +
 "GROUP BY user, window_start, window_end "
 )
 tableEnv.toDataStream(result).print()
 env.execute()

tableAPI开窗函数

RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW时间开窗范围
ROWS BETWEEN 5 PRECEDING AND CURRENT ROW行开窗

SELECT user, ts,
 COUNT(url) OVER (
 PARTITION BY user
 ORDER BY ts
 RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
 ) AS cnt
FROM EventTable


========
SELECT user, ts,
 COUNT(url) OVER w AS cnt,
 MAX(CHAR_LENGTH(url)) OVER w AS max_url
FROM EventTable
WINDOW w AS (
 PARTITION BY user
 ORDER BY ts
 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)


//例子
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 // 读取数据源,并分配时间戳、生成水位线
 val eventStream = env
 .fromElements(
 Event("Alice", "./home", 1000L),
 Event("Bob", "./cart", 1000L),
 Event("Alice", "./prod?id=1", 25 * 60 * 1000L),
 Event("Alice", "./prod?id=4", 55 * 60 * 1000L),
 Event("Bob", "./prod?id=5", 3600 * 1000L + 60 * 1000L),
 Event("Cary", "./home", 3600 * 1000L + 30 * 60 * 1000L),
 Event("Cary", "./prod?id=7", 3600 * 1000L + 59 * 60 * 1000L)
 )
 .assignAscendingTimestamps(_.timestamp)
 // 创建表环境
 val tableEnv = StreamTableEnvironment.create(env)
 // 将数据流转换成表,并指定时间属性
 val eventTable = tableEnv.fromDataStream(
 eventStream,
 $("user"),
 $("url"),
 $("timestamp").rowtime().as("ts")
 // 将 timestamp 指定为事件时间,并命名为 ts
 )
 // 为方便在 SQL 中引用,在环境中注册表 EventTable
 tableEnv.createTemporaryView("EventTable", eventTable)
 // 定义子查询,进行窗口聚合,得到包含窗口信息、用户以及访问次数的结果表
 val subQuery =
 "SELECT window_start, window_end, user, COUNT(url) as cnt " +
 "FROM TABLE ( " +
 "TUMBLE( TABLE EventTable, DESCRIPTOR(ts), INTERVAL '1' HOUR )) " +
 "GROUP BY window_start, window_end, user "
 // 定义 Top N 的外层查询
 val topNQuery =
 "SELECT * " +
 "FROM (" +
 "SELECT *, " +
 "ROW_NUMBER() OVER ( " +
 "PARTITION BY window_start, window_end " +
 "ORDER BY cnt desc " +
 ") AS row_num " +
 "FROM (" + subQuery + ")) " +
 "WHERE row_num <= 2"
 // 执行 SQL 得到结果表
 val result = tableEnv.sqlQuery(topNQuery)
 tableEnv.toDataStream(result).print()
 env.execute()
 }

tableAPI联合

两表连接笛卡尔积直接from a,b就行
内连/左连/右连/全连接和sql一样inner join ,left join,right join,full outer join =on
时间间隔限制,可以用between and也可以>=这样的

  1. ltime=rtime
  2. ltime>=rtime and ltime<rtime + INTERVAL ‘10’ MINUTE
  3. ltime BETWEEN rtime -INTERVAL ‘10’ SECOND AND rtime + INTERVAL ‘5’ SECOND
SELECT *
FROM Order o, Shipment s
WHERE o.id = s.order_id
AND o.order_time BETWEEN s.ship_time - INTERVAL '4' HOUR AND s.ship_time

时间了解是一种更新版本的方式,涉及版本表的定义。感兴趣的可以去官网资料。

tableAPI函数

namefun
比较函数= <> is not null
逻辑函数and or not is false
算数函数+ power rand()
字符串函数`string1
时间函数date timestamp current_time interval string range mibute year to month interval '2-10' year to month
聚合函数count sum rank row_number

自定义函数:

// 注册函数
tableEnv.createTemporarySystemFunction("MyFunction", classOf[MyFunction])
//使用
tableEnv.from("MyTable").select(call("MyFunction", $("myField")))
tableEnv.from("MyTable").select(call(classOf[SubstringFunction],$("myField"))
tableEnv.sqlQuery("SELECT MyFunction(myField) FROM MyTable")



=============标量函数==========
class HashFunction extends ScalarFunction {
 // 接受任意类型输入,返回 INT 型输出
 def eval(@DataTypeHint(inputGroup = InputGroup.ANY) o: AnyRef) : Int ={
 o.hashCode()
 } }
// 注册函数
tableEnv.createTemporarySystemFunction("HashFunction",classOf[HashFunction])
// 在 SQL 里调用注册好的函数
tableEnv.sqlQuery("SELECT HashFunction(myField) FROM MyTable")

==============表函数===============
@FunctionHint(output = new DataTypeHint("ROW<word STRING, length INT>"))
class SplitFunction extends TableFunction[Row] {
 def eval(str: String) {
 str.split(" ").foreach(s => collect(Row.of(s, Int.box(s.length))))
 } }
// 注册函数
tableEnv.createTemporarySystemFunction("SplitFunction",classOf[SplitFunction]
)
// 在 SQL 里调用注册好的函数
// 1. 交叉联结
tableEnv.sqlQuery(
 "SELECT myField, word, length " +
 "FROM MyTable, LATERAL TABLE(SplitFunction(myField))")
// 2. 带 ON TRUE 条件的左联结
tableEnv.sqlQuery(
 "SELECT myField, word, length " +
 "FROM MyTable " +
 "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE")
// 重命名侧向表中的字段
tableEnv.sqlQuery(
 "SELECT myField, newWord, newLength " +
 "FROM MyTable " +
 "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON 
TRUE")

==================聚合函数=======================================
// 累加器类型定义
case class WeightedAvgAccumulator(var sum: Long = 0L, var count: Int = 0)
// 自定义聚合函数,输出为长整型的平均值,累加器类型为WeightedAvgAccumulator
class WeightedAvg extends AggregateFunction[java.lang.Long, 
WeightedAvgAccumulator> {
 
 override def createAccumulator(): WeightedAvgAccumulator = {
 WeightedAvgAccumulator() // 创建累加器
 }
 
 override def getValue(acc: WeightedAvgAccumulator): java.lang.Long = {
 if (acc.count == 0) {
 null // 防止除数为 0
 } else {
 acc.sum / acc.count // 计算平均值并返回
 }
 }
 // 累加计算方法,每来一行数据都会调用
 def accumulate(acc: WeightedAvgAccumulator, iValue: java.lang.Long, iWeight: 
Int) {
 acc.sum += iValue * iWeight
 acc.count += iWeight
 } }
// 注册自定义聚合函数
tableEnv.createTemporarySystemFunction("WeightedAvg", classOf[WeightedAvg])
// 调用函数计算加权平均值
val result = tableEnv.sqlQuery(
 "SELECT student, WeightedAvg(score, weight) FROM ScoreTable GROUP BY 
student"
)

==============表聚合函数=====
// 聚合累加器的类型定义,包含最大的第一和第二两个数据
case class Top2Accumulator(
 var first: Integer,
 var second: Integer
)
// 自定义表聚合函数,查询一组数中最大的两个,返回值为(数值,排名)的二元组
class Top2 extends TableAggregateFunction[Tuple2[Integer, Integer], 
Top2Accumulator] {
 
 override def createAccumulator(): Top2Accumulator = {
Top2Accumulator(
 Integer.MIN_VALUE,
 Integer.MIN_VALUE
)
 }
 // 每来一个数据调用一次,判断是否更新累加器
 def accumulate(acc: Top2Accumulator, value: Integer): Unit = {
 if (value > acc.first) {
 acc.second = acc.first
 acc.first = value
 } else if (value > acc.second) {
 acc.second = value
 }
 }
 // 输出(数值,排名)的二元组,输出两行数据
 def emitValue(acc: Top2Accumulator, out: Collector[Tuple2[Integer, Integer]]): 
Unit = {
 if (acc.first != Integer.MIN_VALUE) {
 out.collect(Tuple2.of(acc.first, 1))
 }
 if (acc.second != Integer.MIN_VALUE) {
 out.collect(Tuple2.of(acc.second, 2))
 }
 } }

// 注册表聚合函数函数
tableEnv.createTemporarySystemFunction("Top2", classOf[Top2])
// 在 Table API 中调用函数
tableEnv.from("MyTable")
 .groupBy($("myField"))
 .flatAggregate(call("Top2", $("value")).as("value", "rank"))
 .select($("myField"), $("value"), $("rank"))

sql客户端

start-cluster.sh sql-client.sh启动集群然后启动客户端
set 'execution.runtime-mode'='streaming'设置流处理/batch批
set 'sql-client.execution.result-mode'='table'设置执行结果为table、changelog、tableau
set 'table.exec.stat.ttl'='1000'设置ttl
执行sql查询:

建表:

CREATE TABLE EventTable(
> user STRING,
> url STRING,
> `timestamp` BIGINT
> ) WITH (
> 'connector' = 'filesystem',
> 'path' = 'events.csv',
> 'format' = 'csv'
> );
Flink SQL> CREATE TABLE ResultTable (
> user STRING,
> cnt BIGINT
> ) WITH (
> 'connector' = 'print'
> )

插入:

 INSERT INTO ResultTable SELECT user, COUNT(url) as cnt FROM EventTable
GROUP BY user;

tableAPI联结外部系统

print
create table printtable(user string,cnt bigint) with( 'connector'='print')
kafka

不加上user的话会造成名字和系统冲突导致建表失败

CREATE TABLE KafkaTable (
 `user` STRING,
 `url` STRING,
 `ts` TIMESTAMP(3) METADATA FROM 'timestamp'
) WITH (
 'connector' = 'kafka',
 'topic' = 'events',
 'properties.bootstrap.servers' = 'localhost:9092',
 'properties.group.id' = 'testGroup',
 'scan.startup.mode' = 'earliest-offset',
 'format' = 'csv'
)
upsert-kafka
CREATE TABLE pageviews_per_region (
 user_region STRING,
 pv BIGINT,
 uv BIGINT,
 PRIMARY KEY (user_region) NOT ENFORCED
) WITH (
 'connector' = 'upsert-kafka',
 'topic' = 'pageviews_per_region',
 'properties.bootstrap.servers' = '...',
 'key.format' = 'avro',
 'value.format' = 'avro'
);

kafka+watermark
CREATE TABLE pageviews (
 user_id BIGINT,
 page_id BIGINT,
 viewtime TIMESTAMP,
 user_region STRING,
 WATERMARK FOR viewtime AS viewtime - INTERVAL '2' SECOND
) WITH (
 'connector' = 'kafka',
 'topic' = 'pageviews',
 'properties.bootstrap.servers' = '...',
 'format' = 'json'
);
kafka插入upsertkafka
INSERT INTO pageviews_per_region
SELECT
 user_region,
 COUNT(*),
 COUNT(DISTINCT user_id)
FROM pageviews
GROUP BY user_region;
filesystem
CREATE TABLE MyTable (
 column_name1 INT,
 column_name2 STRING,
 ...
 part_name1 INT,
 part_name2 STRING
) PARTITIONED BY (part_name1, part_name2) WITH (
 'connector' = 'filesystem', -- 连接器类型
 'path' = '...', -- 文件路径
 'format' = '...' -- 文件格式
)
jdbc
-- 创建一张连接到 MySQL 的 表
CREATE TABLE MyTable (
 id BIGINT,
 name STRING,
 age INT,
 status BOOLEAN,
 PRIMARY KEY (id) NOT ENFORCED
) WITH (
 'connector' = 'jdbc',
 'url' = 'jdbc:mysql://localhost:3306/mydatabase',
 'table-name' = 'users'
);
-- 将另一张表 T 的数据写入到 MyTable 表中
INSERT INTO MyTable
SELECT id, name, age, status FROM T;
elasticsearch
-- 创建一张连接到 Elasticsearch 的 表
CREATE TABLE MyTable (
 user_id STRING,
 user_name STRING
 uv BIGINT,
 pv BIGINT,
 PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
 'connector' = 'elasticsearch-7',
 'hosts' = 'http://localhost:9200',
 'index' = 'users'
);
hive
val env=StreamExecutionEnvironment.getExecutionEnvironment
val conf=new Configuration()
conf.setString("rest.bind-port","8081-8089")
env.enableCheckpointing(5000,CheckpointMode.EXACTLY_ONCE)
env.getCheckpointConfig.setCheckpointStorage("hdfs://master:9000/tmp/checkpoint")
env.getCheckpointConfig.setMaxConcurrentCheckpoints(2)
if((arg.length>0 && args(0).equals("local")) || args.length==0){
	tableEnv=StreamTableEnvironment.create(env)
}
val myhive=new HiveCatalog("myhive","flinkt","/opt/hive-conf.xml")
tableEnv.registerCatalog("myhive",myhive)
tableEnv.useCatalog("myhive")
tableEnv.getConfig().setConfiguration.setString("table.exec.source.idle-timeout","20minutes")

tableEnv.executeSql(s"""drop table if exists kafka_order_master""")
tableEnv.executeSql(s"""create table kakfa_order_master(
	|字段列表
	|)
	|with(
	|'connector'='kafka',
	|'topic'='order_master',
	|'properties.bootstrap.servers'='master:9092,slave1:9092,slave2:9092',
	|'format'='json',
	|'scan.startup.mode'='latest-offset'
	|)
	|""".stripMargin)
	
tableEnv.executeSql(s"""CREATE TABLE hive_sink (
  |user_id STRING,
  |item_id STRING,
  |behavior STRING,
  |ts TIMESTAMP(3)
  |) WITH (
  |'connector' = 'hive',
  |'database' = 'default',
  |'table' = 'behavior'
  |);""".stripMargin)
tableEnv.executeSql(s"""
|INSERT INTO hive_sink
|SELECT
| user_id,
| item_id,
| behavior,
| ts
|FROM kafka_source;
	|)
	|""".stripMargin)
hbase
-- 创建一张连接到 HBase 的 表
CREATE TABLE MyTable (
rowkey INT,
family1 ROW<q1 INT>,
family2 ROW<q2 STRING, q3 BIGINT>,
family3 ROW<q4 DOUBLE, q5 BOOLEAN, q6 STRING>,
PRIMARY KEY (rowkey) NOT ENFORCED
) WITH (
'connector' = 'hbase-2.2',
'table-name' = 'mytable',
'zookeeper.quorum' = 'master:2181,slave1:2181,slave2:2181'
);
-- 假设表 T 的字段结构是 [rowkey, f1q1, f2q2, f2q3, f3q4, f3q5, f3q6]
INSERT INTO MyTable
SELECT rowkey, ROW(f1q1), ROW(f2q2, f2q3), ROW(f3q4, f3q5, f3q6) FROM T;
hive
val settings = EnvironmentSettings.newInstance.useBlinkPlanner.build()
val tableEnv = TableEnvironment.create(settings)
val name = "myhive"
val defaultDatabase = "mydatabase"
val hiveConfDir = "/opt/hive-conf"
// 创建一个 HiveCatalog,并在表环境中注册
val hive = new HiveCatalog(name, defaultDatabase, hiveConfDir)
tableEnv.registerCatalog("myhive", hive)
// 使用 HiveCatalog 作为当前会话的 catalog
tableEnv.useCatalog("myhive")

 create catalog myhive with ('type' = 'hive', 'hive-conf-dir' = 
'/opt/hive-conf');
 use catalog myhive;
 
// 配置 hive 方言
tableEnv.getConfig().setSqlDialect(SqlDialect.HIVE)
// 配置 default 方言
tableEnv.getConfig().setSqlDialect(SqlDialect.DEFAULT)


-- 设置 SQL 方言为 hive,创建 Hive 表
SET table.sql-dialect=hive;
CREATE TABLE hive_table (
 user_id STRING,
 order_amount DOUBLE
) PARTITIONED BY (dt STRING, hr STRING) STORED AS parquet TBLPROPERTIES (
 'partition.time-extractor.timestamp-pattern'='$dt $hr:00:00',
 'sink.partition-commit.trigger'='partition-time',
 'sink.partition-commit.delay'='1 h',
 'sink.partition-commit.policy.kind'='metastore,success-file'
);
-- 设置 SQL 方言为 default,创建 Kafka 表
SET table.sql-dialect=default;
CREATE TABLE kafka_table (
 user_id STRING,
 order_amount DOUBLE,
 log_ts TIMESTAMP(3),
 WATERMARK FOR log_ts AS log_ts - INTERVAL '5' SECOND – 定义水位线
) WITH (...);
-- 将 Kafka 中读取的数据经转换后写入 Hive 
INSERT INTO TABLE hive_table 
SELECT user_id, order_amount, DATE_FORMAT(log_ts, 'yyyy-MM-dd'),
DATE_FORMAT(log_ts, 'HH')
FROM kafka_table;

CEP

val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 // 获取登录事件流,并提取时间戳、生成水位线
 val stream = env
 .fromElements(
 LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
 LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
 LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
 LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
 LoginEvent("user_2", "192.168.1.29", "success", 6000L),
 LoginEvent("user_2", "192.168.1.29", "fail", 7000L),
 LoginEvent("user_2", "192.168.1.29", "fail", 8000L)
 )
 .assignAscendingTimestamps(_.timestamp)
 .keyBy(_.userId)
 // 1. 定义 Pattern,连续的三个登录失败事件
 val pattern = Pattern
 .begin[LoginEvent]("first") // 以第一个登录失败事件开始
 .where(_.eventType.equals("fail"))
 .next("second") // 接着是第二个登录失败事件
 .where(_.eventType.equals("fail"))
 .next("third") // 接着是第三个登录失败事件
 .where(_.eventType.equals("fail"))
 // 2. 将 Pattern 应用到流上,检测匹配的复杂事件,得到一个 PatternStream
 val patternStream = CEP.pattern(stream, pattern)
 // 3. 将匹配到的复杂事件选择出来,然后包装成字符串报警信息输出
 patternStream
 .select(new PatternSelectFunction[LoginEvent, String] {
 override def select(map: util.Map[String, util.List[LoginEvent]]): String 
= {
 val first = map.get("first").get(0)
 val second = map.get("second").get(0)
 val third = map.get("third").get(0)
 first.userId + " 连续三次登录失败!登录时间:" + first.timestamp + ", " + 
second.timestamp + ", " + third.timestamp
 }
 })
 .print("warning")
 env.execute()
 }
Pattern模式example解释
begin 以…开始beginType开始标志,名字随意
next接着【近邻】next(“second”)第二次标志,名字随意
followedBy再下一个【宽邻居】.followedBy(“follow”).where和begin next一样为了定义事件名
followedByAny()【非确定宽松】start.followedByAny(“middle”).where(…)
allowCombinations【非确定宽松】重复使用已经匹配的事件和folledByAny相同
ontNext不能紧跟start.notNext(“not”).where(…)
notFollowedByAny()start.notFollowedBy(“not”).where(…)
within时间限制条件middle.within(Time.seconds)
where当…时where(_.eventType.equals(“fail”))判断,和filter一样
oneOrMore一次或更多.oneOrMore匹配时间出现1或多次
times匹配指定次数.times(2,4)比如aa/aaa/aaaa
consecutive循环检测[近邻].where().times(n).consecutive()循环检测3次才可
greedy贪心.times(3,4).greedy匹配最小3次最多4次,比如aaaa,尽可能多
optional可选.times(4).optional匹配结果可以不符合出现4次

面试题:allowCombinations和followedByAny区别:

allowCombinations是模式中的事件任意排序,顺序可以变,都发生了就行
followedByAny是只要是连续的,顺序不变就行


正是因为个体模式可以通过量词定义为循环模式,一个模式能够匹配到多个事件,所以之前代码中事件的检测接收才会用 Map 中的一个列表(List)来保存。而之前代码中没有定义量词,都是单例模式,所以只会匹配一个事件,每个 List 中也只有一个元素:
val first = map.get("first").get(0)

Pattern模式example解释
subtype子类型subtupe(classOf[type])这个类型才可
where.where(_.user.startsWith(“A”))A开头才可
组合多个where就是组合条件
oror和where一样满足一个就行
终止条件untiloneOrMore或oneOrMore.optional 结合until使用

where可以传函数:
迭代条件
只有当所有事件的amount之和小于100且以A开头才可以

middle.oneOrMore
 .where((value, ctx) => {
 lazy val sum = ctx.getEventsForPattern("middle").map(_.amount).sum
 value.user.startsWith("A") && sum + value.amount < 100
})

chatGPT写的代码:
例子中,我们定义了一个名为 pattern 的模式,该模式指定了要匹配的事件序列,即:先出现一个名称为 “foo” 的事件(被标记为 “start”),然后是另一个名称为 “foo” 的事件(被标记为 “middle”),最后又出现一个名称为 “foo” 的事件(被标记为 “end”)。这些事件出现的时间间隔不能超过 10 秒。我们通过 CEP.pattern 方法将该模式应用于输入流 input,并最终通过 result.select 方法获取匹配结果。

val pattern = Pattern.begin[Event]("start")
  .where(_.name == "foo")
  .followedBy("middle")
  .where(_.name == "foo")
  .followedBy("end")
  .where(_.name == "foo")
  .within(Time.seconds(10))

val result = CEP.pattern(input, pattern)

result.select(pattern => {
  val start = pattern("start").head
  val end = pattern("end").head
  (start.id, end.id, end.timestamp - start.timestamp)
})

第二种:
使用 consecutive() 操作符匹配了连续出现的类型为 EventType.TYPE_B 的事件。
Pattern.<Event>begin("start")
  .subtype(Event.class).where(e -> e.getType() == EventType.TYPE_A)
  .followedBy("middle")
  .subtype(Event.class).where(e -> e.getType() == EventType.TYPE_B)
  .consecutive()  // 匹配连续出现的 B 事件
  .followedBy("end")
  .subtype(Event.class).where(e -> e.getType() == EventType.TYPE_C);

模式组:

Pattern套Pattern

// 以模式序列作为初始模式
val start = Pattern.begin(
Pattern.begin[Event]("start_start").where(...)
.followedBy("start_middle").where(...)
)
// 在 start 后定义严格近邻的模式序列,并重复匹配两次
val strict = start.next(
Pattern.begin[Event]("next_start").where(...)
.followedBy("next_middle").where(...)
).times(2)
// 在 start 后定义宽松近邻的模式序列,并重复匹配一次或多次
val relaxed = start.followedBy(
Pattern.begin("followedby_start")[Event].where(...)
.followedBy("followedby_middle").where(...)
).oneOrMore
//在 start 后定义非确定性宽松近邻的模式序列,可以匹配一次,也可以不匹配
val nonDeterminRelaxed = start.followedByAny(
Pattern.begin[Event]("followedbyany_start").where(...)
.followedBy("followedbyany_middle").where(...)
).optional
跳跃策略
namefun
NO_SKIP不跳过默认,所有匹配都输出
SKIP_TO_NEXT跳至下一个策略和greedy效果相同(a1 a2 a3 b),(a2 a3 b),(a3 b)。
SKIP_PAST_LAST_EVENT跳过所有子匹配AfterMatchSkipStrategy.skipPastLastEvent()跳过所有子匹配
SKIP_TO_FIRST[a]跳至第一个通过代码调用 AfterMatchSkipStrategy.skipToFirst(“a”)选择跳至第一个策略,这里传入一个参数,指明跳至哪个模式的第一个匹配事件。找到 a1 开始的匹配(a1 a2 a3 b)后,跳到以最开始一个 a(也就是 a1)为开始的匹配,相当于只留下 a1 开始的匹配。最终得到(a1 a2 a3 b)
SKIP_TO_LAST[a]跳至最后一个通过代码调用 AfterMatchSkipStrategy.skipToLast(“a”)跳至最后一个策略,同样传入一个参数,指明跳至哪个模式的最后一个匹配事件。找到 a1 开始的匹配(a1 a2 a3 b)后,跳过所有a1、a2 开始的匹配,跳到以最后一个 a(也就是 a3)为开始的匹配。最终得到(a1 a2 a3 b),(a3 b)。
匹配时间的选择提取
val patternStream=CEP.parttern(dataStream,pattern)
val result=patternStream.select(new MyPatternSelectFunction)
class MyPatternSelectFunction extends PatternSelectFunction[Event, String]{
 override def select(pattern: Map[String, List[Event]] ): String = {
 val startEvent = pattern.get("start").get(0)
 val middleEvent = pattern.get("middle").get(0)
 startEvent.toString + " " + middleEvent.toString
 } }

例子
val pattern = Pattern
 .begin[LoginEvent]("fails")
 .where(_.eventType.equals("fail")).times(3).consecutive()
// 2. 将 Pattern 应用到流上,检测匹配的复杂事件,得到一个 PatternStream
val patternStream = CEP.pattern(stream, pattern)
// 3. 将匹配到的复杂事件选择出来,然后包装成报警信息输出
patternStream
 .select(new PatternSelectFunction[LoginEvent, String] {
 
 override def select(map:util.Map[String, util.List[LoginEvent]]): 
String = {
// 只有一个模式,匹配到了 3 个事件,放在 List 中
 val first = map.get("fails").get(0)
 val second = map.get("fails").get(1)
 val third = map.get("fails").get(2);
 first.userId + " 连续三次登录失败!登录时间:" + first.timestamp + ", " 
+ second.timestamp + ", " + third.timestamp
 }
 })
 .print("warning")

PatternFlatSelectFunction
patternStream.flatSelect(new PatternFlatSelectFunction[LoginEvent, String] {
override def flatSelect(map: util.Map[String, util.List[LoginEvent]],
out: Collector[String]):Unit = {
val first = map.get("fails").get(0)
val second = map.get("fails").get(1)
val third = map.get("fails").get(2)
out.collect(first.userId + " 连续三次登录失败!登录时间:" + first.timestamp + 
", " + second.timestamp + ", " + third.timestamp)
}
}).print("warning")
匹配时间的通用检测方法
patternStream.process(new PatternProcessFunction[LoginEvent, String] {
override def processMatch(map: util.Map[String, util.List[LoginEvent]] , ctx: 
Context, 
out: Collector[String]): Unit ={
val first = map.get("fails").get(0)
val second = map.get("fails").get(1)
val third = map.get("fails").get(2)
out.collect(first.userId + " 连续三次登录失败!登录时间:" + first.timestamp + 
", " + second.timestamp + ", " + third.timestamp)
}
}).print("warning")
patternProcessFunction的侧输出流
class MyPatternProcessFunction extends PatternProcessFunction[Event, String] 
with TimedOutPartialMatchHandler[Event] {
 // 正常匹配事件的处理
 override def processMatch(match: Map[String, List<Event]] , ctx: Context, out: 
Collector[String]) throws Exception{
 ...
 }
 // 超时部分匹配事件的处理
 Override def processTimedOutMatch(match: Map[String, List[Event]], ctx: 
Context): Unit {
 val startEvent = match.get("start").get(0)
 val outputTag = new OutputTag[Event]("time-out")
 ctx.output(outputTag, startEvent)
 } }
PatternTimeoutFunction
val timeoutTag = new OutputTag[String]("timeout");
// 将匹配到的,和超时部分匹配的复杂事件提取出来,然后包装成提示信息输出
val resultStream = patternStream
.select(timeoutTag,
// 超时部分匹配事件的处理
 new PatternTimeoutFunction[Event, String] {
 override def timeout(pattern: Map[String, List[Event]], 
timeoutTimestamp: Long): String {
 val event = pattern.get("start").get(0)
 "超时:" + event.toString()
 }
 },
// 正常匹配事件的处理
 new PatternSelectFunction[Event, String] {
 override def select(pattern: Map[String, List[Event]] ): String{
...
 }
 } )
// 将正常匹配和超时部分匹配的处理结果流打印输出
resultStream.print("matched")
resultStream.getSideOutput(timeoutTag).print("timeout")
CEP应用
val pattern = Pattern
 .begin[OrderEvent]("create")
 .where(_.eventType.equals("create"))
 .followedBy("pay")
 .where(_.eventType.equals("pay"))
 .within(Time.minutes(15)) // 限制在 15 分钟之内
=======
val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 // 获取订单事件流,并提取时间戳、生成水位线
 val stream = env
 .fromElements(
 OrderEvent("user_1", "order_1", "create", 1000L),
 OrderEvent("user_2", "order_2", "create", 2000L),
 OrderEvent("user_1", "order_1", "modify", 10 * 1000L),
 OrderEvent("user_1", "order_1", "pay", 60 * 1000L),
 OrderEvent("user_2", "order_3", "create", 10 * 60 * 1000L),
 OrderEvent("user_2", "order_3", "pay", 20 * 60 * 1000L)
 )
 .assignAscendingTimestamps(_.timestamp)
 .keyBy(_.orderId) // 按照订单 ID 分组
 val pattern = Pattern
 .begin[OrderEvent]("create")
 .where(_.eventType.equals("create"))
 .followedBy("pay")
 .where(_.eventType.equals("pay"))
 .within(Time.minutes(15))
 val patternStream = CEP.pattern(stream, pattern)
 val payedOrderStream = patternStream.process(new 
OrderPayPatternProcessFunction)
 payedOrderStream.print("payed")
 payedOrderStream.getSideOutput(new 
OutputTag[String]("timeout")).print("timeout")
 env.execute()
 }
 class OrderPayPatternProcessFunction extends PatternProcessFunction[OrderEvent, 
String] with TimedOutPartialMatchHandler[OrderEvent] {
 override def processMatch(map: util.Map[String, util.List[OrderEvent]], 
context: PatternProcessFunction.Context, collector: Collector[String]): Unit = 
{
 val payEvent = map.get("pay").get(0)
 collector.collect("订单 " + payEvent.orderId + " 已支付!")
 }
 override def processTimedOutMatch(map: util.Map[String, 
util.List[OrderEvent]], context: PatternProcessFunction.Context): Unit = {
 val createEvent = map.get("create").get(0)
 context.output(new OutputTag[String]("timeout"), "订单 " + 
createEvent.orderId + " 超时未支付!用户为:" + createEvent.userId)
 }
 }
处理迟到数据CEP版
val patternStream = CEP.pattern(input, pattern)
// 定义一个侧输出流的标签
val lateDataOutputTag = new OutputTag[String]("late-data")
val result = patternStream
 .sideOutputLateData(lateDataOutputTag) // 将迟到数据输出到侧输出流
 .select( 
// 处理正常匹配数据
 new PatternSelectFunction[Event, ComplexEvent] {...}
 )
// 从结果中提取侧输出流
val lateData = result.getSideOutput(lateDataOutputTag)
处理迟到数据原始版
val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 val stream = env.fromElements(
 LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
 LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
 LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
 LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
 LoginEvent("user_2", "192.168.1.29", "success", 6000L),
 LoginEvent("user_2", "192.168.1.29", "fail", 7000L),
 LoginEvent("user_2", "192.168.1.29", "fail", 8000L)
 )
 .keyBy(_.userId)
 val alertStream = stream.flatMap(new StateMachineMappter)
 alertStream.print("warning")
 env.execute()
 }
 class StateMachineMappter extends RichFlatMapFunction[LoginEvent, String] {
 lazy val currentState = getRuntimeContext.getState(
 new ValueStateDescriptor[State]("state", classOf[State])
 )
 override def flatMap(value: LoginEvent, out: Collector[String]): Unit = {
 if (currentState.value() == null) {
 currentState.update(Initial)
 }
 val nextState = transition(currentState.value(), value.eventType)
 nextState match {
 case Matched => out.collect(value.userId + " 连续三次登录失败")
 case Terminal => currentState.update(Initial)
 case _ => currentState.update(nextState)
 }
 }
 }
 case class LoginEvent(userId: String, ipAddress: String, eventType: String, 
timestamp: Long)
 sealed trait State
 case object Initial extends State
 case object Terminal extends State
 case object Matched extends State
 case object S1 extends State
 case object S2 extends State
 def transition(state: State, event: String): State = {
 (state, event) match {
 case (Initial, "success") => Terminal
 case (Initial, "fail") => S1
 case (S1, "fail") => S2
 case (S2, "fail") => Matched
 case (Terminal, "fail") => S2
 case (S1, "success") => Terminal
 case (S2, "success") => Terminal
 }
 }

运行代码,可以看到输出与之前 CEP 的实现是完全一样的。显然,如果所有的复杂事件处理都自己设计状态机来实现是非常繁琐的,而且中间逻辑非常容易出错;所以 Flink CEP 将底层 NFA 全部实现好并封装起来,这样我们处理复杂事件时只要调上层的 Pattern API 就可以,
无疑大大降低了代码的复杂度,提高了编程的效率

运行

```flink run --class 主类 jar包`

  1. flinkCEP
  2. flink 自定义函数
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

厨 神

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值