Flink DataStream 流处理 APi

Flink DataStream 流处理 APi

一个 Flink 程序,其实就是对 DataStream 的各种转换。具体来说,代码基本上都由以下几部分构成:

  • 获取执行环境(Execution Environment)
  • 读取数据源(Source)
  • 定义基于数据的转换操作(Transformations)
  • 定义计算结果的输出位置(Sink)
  • 触发程序执行(Execute)
    其中,获取环境和触发执行,都可以认为是针对执行环境的操作。
    在这里插入图片描述

一、创建执行环境

编 写 Flink 程 序 的 第 一 步 , 就 是 创 建 执 行 环 境 。 我 们 要 获 取 的 执 行 环 境 , 是 StreamExecutionEnvironment 类的对象,这是所有 Flink 程序的基础。在代码中创建执行环境的方式,就是调用这个类的静态方法,具体有以下三种。

1、getExecutionEnvironment

最简单的方式,就是直接调用 getExecutionEnvironment 方法。它会根据当前运行的上下文直接得到正确的结果;也就是说,这个方法会根据当前运行的方式,自行决定该返回什么样的运行环境。
val env = StreamExecutionEnvironment.getExecutionEnvironment
这种“智能”的方式不需要我们额外做判断,用起来简单高效,是最常用的一种创建执行环境的方式。

2、 createLocalEnvironment

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

3、createRemoteEnvironment

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

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

在获取到程序执行环境后,我们还可以对执行环境进行灵活的设置。

二、执行模式(Execution Mode)

在之前的 Flink 版本中,批处理的执行环境与流处理类似,是调用类 ExecutionEnvironment的静态方法,并返回它的对象

// 批处理环境
val env = ExecutionEnvironment.getExecutionEnvironment
// 流处理环境
val env = StreamExecutionEnvironment.getExecutionEnvironment

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

  • 流执行模式(STREAMING)
    这是 DataStream API 最经典的模式,一般用于需要持续实时处理的无界数据流。默认情况下,程序使用的就是 streaming 执行模式。
  • 批执行模式(BATCH)
    专门用于批处理的执行模式, 这种模式下,Flink 处理作业的方式类似于 MapReduce 框架。对于不会持续计算的有界数据,我们用这种模式处理会更方便。
  • 自动模式(AUTOMATIC)
    在这种模式下,将由程序根据输入数据源是否有界,来自动选择执行模式。
    由于 Flink 程序默认是 streaming 模式,我们这里重点介绍一下 BATCH 模式的配置。
    主要有两种方式:
    1、通过命令行配置
bin/flink run -Dexecution.runtime-mode=BATCH

在提交作业时,增加 execution.runtime-mode 参数,指定值为 BATCH。
2、通过代码配置

val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setRuntimeMode(RuntimeExecutionMode.BATCH)

在代码中,直接基于执行环境调用 setRuntimeMode 方法,传入 BATCH 模式。

三、触发程序执行

我们需要显式地调用执行环境的 execute()方法,来触发程序执行。execute()方法将一直等待作业完成,然后返回一个执行结果(JobExecutionResult)。

env.execute()

四、源算子(Source)

在这里插入图片描述
Flink 可以从各种来源获取数据,然后构建 DataStream 进行转换处理。一般将数据的输入来源称为数据源,而读取数据的算子就是源算子(Source)。所以,Source 就是我们整个处理程序的输入端。
Flink 代码中通用的添加 Source 的方式,是调用执行环境的 addSource()方法:

//通过调用 addSource()方法可以获取 DataStream 对象
val stream = env.addSource(...)

方法传入一个对象参数,需要实现 SourceFunction 接口,返回一个 DataStream。

1、读取数据

可以分为从集合中读取数据和从文件中读取数据
从集合中中读取数据:env.fromCollection 方法,里面的参数是集合
从文本中读取数据:env.readTextFile() 方法,里面的参数是文件的路径

package API_Test_Source

import org.apache.flink.streaming.api.scala._

class flink01_TestSource {

}
//定义样例类,温度传感器
case class chuangan(id:String,timestamp:Long,wendu:Double)  //这里面三个属性,分别是id,时间戳,还有温度值

//这是测试从集合中获取文件的程序
object flink01_TestSource{
  def main(args: Array[String]): Unit = {

    //创建执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    //1、从集合中读取数据
    //fromCollection 方法从集合中读取数据
    val dataList = List(
      chuangan("sensor_1", 1547718199, 35.7),
      chuangan("sensor_2", 1547718201, 15.4),
      chuangan("sensor_3", 1547718202, 6.7),
      chuangan("sensor_4", 1547718205, 38.1)
    )

    val data1 = env.fromCollection(dataList) //romCollection() 方法,从集合中读取数据
    data1.print() //打印输出  因为,没设并行度,根据系统默认的cpu核来决定,所以输出可能是乱序的现象

    print("=======================================")
    //2、从文件中读取数据
    //readTextFile 方法,从文件中读取数据
    //val inputPath = "E:\\study\\BigDatas\\Flinks\\flink\\src\\main\\resources\\sensor.txt"
      //每一步的操作都设置为并行度为1的话。那么输出出来的数据就会是按照顺序的
    val data2 = env.readTextFile("E:\\study\\BigDatas\\Flinks\\flink\\src\\main\\resources\\sensor.txt").setParallelism(1)
    data2.print().setParallelism(1)

    //执行任务
    env.execute()

  }
}

在这里插入图片描述

2、从 kafka 读取数据

因为flink和kafka是两个完全不同的组件,所以想要从kafka读取数据我们需要,导入kafka的依赖
在这里插入图片描述

<?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>flink</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <flink.version>1.13.0</flink.version>
        <target.java.version>1.8</target.java.version>
        <scala.binary.version>2.12</scala.binary.version>
    </properties>

    <dependencies>
        <!-- 引入 Flink 相关依赖-->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-scala_2.12</artifactId>
            <version>1.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-scala_2.12</artifactId>
            <version>1.13.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-clients -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.12</artifactId>
            <version>1.13.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-connector-kafka -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka_2.12</artifactId>
            <version>1.13.0</version>
        </dependency>
    </dependencies>

</project>

因为kafka是基于zookeeper,我们首先要在虚拟机把zookeeper 启动起来,三台机器都要启
输入命令: ./bin/zkServer.sh start
在这里插入图片描述
然后启动kafka
输入命令:./bin/kafka-server-start.sh -daemon ./config/server.properties
在这里插入图片描述
然后再linux里面创建一个生产者 ,注意这里的主题和IDEA里面的主题要是一样的
输入命令:./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic sensor
在这里插入图片描述
然后IDEA这边运行程序,就可以接收到数据

package API_Test_Source.flink02_TestSource

import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer

import java.util.Properties

class flink02_testSource2 {

}
//从kafka 读取数据
object flink02_testSource2{
  def main(args: Array[String]): Unit = {
    //创建环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    //从kafka 读取数据
    //其实可以设置好多,但是这里设置这两个就可以了
    val properties = new Properties()
    properties.setProperty("bootstrap.servers","192.168.10.102:9092") //定义kafka 要连接的主机和端口号
    //properties.setProperty("group.id","consumer-group") //这个是分组ID

    val data = env.addSource(new FlinkKafkaConsumer[String]("sensor",new SimpleStringSchema(),properties)) //添加一个源

    //执行任务
    data.print()
    env.execute()

  }
}

这边发数据
在这里插入图片描述
IDEA 可以进行接收
在这里插入图片描述

3、自定义 Source

接下来我们创建一个自定义的数据源,实现 SourceFunction 接口。主要重写两个关键方法:
run()和 cancel()。

  • run()方法:使用运行时上下文对象(SourceContext)向下游发送数据;
  • cancel()方法:通过标识位控制退出循环,来达到中断数据源的效果。
 package API_Test_Source.flink02_TestSource

import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext
import org.apache.flink.streaming.api.scala.{StreamExecutionEnvironment, createTypeInformation}

import java.util.Calendar
import scala.util.Random
class test{

}
object test{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val data = env.addSource(new ClickSource)  //使用 addSource 方法
      .setParallelism(1)

    data.print()

    env.execute()
  }
}
class ClickSource extends SourceFunction[Events]{
  var running = true
  override def run(sourceContext: SourceFunction.SourceContext[Events]): Unit = {
    // 实例化一个随机数发生器
    val random = new Random()
    // 供随机选择的用户名的数组
    val users = Array("Mary", "Bob", "Alice", "Cary")
    // 供随机选择的 城市 的数组
    val urls = Array("四川", "上海", "北京", "山西", "苏杭")
    //通过 while 循环发送数据,running 默认为 true,所以会一直发送数据
    while (running) {
      // 调用 collect 方法向下游发送数据
      // //ctx是SourceFunction.SourceContext[Events]类型的参数,它是一个上下文对象,用于向下游发送数据。
      // 在run方法中,通过调用ctx.collect方法向下游发送数据。
      sourceContext.collect(  // 使用上下文对象 .collent 方法来采集数据
        Events(
          users(random.nextInt(users.length)), // 随机选择一个用户名
          urls(random.nextInt(urls.length)), // 随机选择一个 url
          Calendar.getInstance.getTimeInMillis // 当前时间戳
        )
      )
      54
      // 隔 1 秒生成一个点击事件,方便观测
      Thread.sleep(1000)
    }
  }

  override def cancel(): Unit = running = false
}

五、Transform (转换算子)

1、简单转换算子

这三个算子比较简单,就写到一起了

(1) map

概念:
map()是大家非常熟悉的大数据操作算子,主要用于将数据流中的数据进行转换,形成新
的数据流。简单来说,就是一个“一一映射”,消费一个元素就产出一个元素。
实现:
大概两种方式,第一种就是正常的使用map()方法,我们这里先是自己定义了一个样例类,然后我们把数据进行包装起来,比如这里三个字段,user,dizhi,shijian,我们这里只要user字段,我们就可以 map(_.user) 得到的就只有user字段的数据,自定义的话,需要继承 MapFunction[Events,String] 类,这里面有两个泛型,第一个泛型是当前输入的类型,也就是我们自己定义的样例类Evnts,然后第二个的话就是输出的类型,定义为String

package chat01

import org.apache.flink.api.common.functions.MapFunction
import org.apache.flink.streaming.api.scala._
case class Events(user:String,dizhi:String,shijian:Long)
//测试map算子
class flink01_map {

}
object flink01_map{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    val data = env.fromElements(
      Events("Mary","./home",1000L),
      Events("Bob","./cart",2000L)
    )

    //提取每次点击事件的用户名
    //1、使用匿名函数
    data.map(_.user).print()  //

    //2、实现mapFuncation接口
    data.map( new UserExtractor)

    env.execute()

  }
  class UserExtractor extends MapFunction[Events,String]{  //两个泛型,输入的数据类型和输出的数据类型
    override def map(t: Events): String = t.user  //直接返回user
  }

}
(2) flatMap

概念:
flatMap()操作又称为扁平映射,主要是将数据流中的整体(一般是集合类型)拆分成一个
一个的个体使用。消费一个元素,可以产生 0 到多个元素。flatMap()可以认为是“扁平化”
(flatten)和“映射”(map)两步操作的结合,也就是先按照某种规则对数据进行打散拆分,
再对拆分后的元素做转换处理,如图 5-7 所示。我们此前 WordCount 程序的第一步分词操作,
就用到了 flatMap()。

package chat01

import org.apache.flink.api.common.functions.FlatMapFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

class flink03_flatMap {

}
object flink03_flatMap{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val data: DataStream[Events] = env.fromElements(
      Events("Mary", "./home", 1000L),
      Events("Bob", "./cart", 2000L),
      Events("Alice", "./cart", 3000L)
    )

    // 直接labam 表达式调用
    //测试灵活的输出模式
    data.flatMap( new MyFlatMap).print()
    env.execute()
  }
  //自定义实现 flatMap 方法
  class MyFlatMap extends FlatMapFunction[Events,String]{  //两个泛型,一个是输入的泛型,一个是输出的泛型
    override def flatMap(t: Events, collector: Collector[String]): Unit = {
        //如果当前数据的user字段是Mary,那么就直接输出user
      if(t.user == "Mary"){
        collector.collect(t.user)  // 直接用 collector调用 collect 方法,采集数据打印
      } else if (t.user == "Bob"){ //如果当前数据是Bob 的点击事件,那么就输出 user和dizhi
        collector.collect(t.user)
        collector.collect(t.dizhi)
        collector.collect(t.shijian.toString)
      }
    }
  }
}
(3) filter

概念:
filter()转换操作,顾名思义是对数据流执行一个过滤,通过一个布尔条件表达式设置过滤
条件,对于每一个流内元素进行判断,若为 true 则元素正常输出,若为 false 则元素被过滤掉。
实现:
和map也是一样的,也是匿名函数 filter 方法直接调用,或者是自定义一个都是一样的。这个不涉及到转换,所以要是自定义的话,继承 FilterFunction[Events] 类,这个就一个泛型,就是当前数据的类型

package chat01

import org.apache.flink.api.common.functions.FilterFunction
import org.apache.flink.streaming.api.scala._

class flink02_filter {

}
object flink02_filter{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val data = env.fromElements(
      Events("Mary", "./home", 1000L),
      Events("Bob", "./cart", 2000L)
    )

    //过滤出用户名为Mary的所有数据  要是是Mary 的话那么就保留,如果不是Mary那么就会被过滤掉
    //1、使用匿名函数
    data.filter(_.user == "Mary").print()   //还是直接匿名函数进行调用就可以了


    //2、实现filterFunction 对象
    data.filter(new UserFilter).print()

    env.execute()
  }

  class UserFilter extends FilterFunction[Events]{  // FilterFunction 只有一个泛型,因为不涉及到输入和输出的转换
    override def filter(t: Events): Boolean = t.user == "Bob" //这里的调用也是和上面是一样的
  }
}

2、基本转换算子

1)、按键分区(keyBy)

概念:
keyBy() 必须先keyBy() 进行分组之后才能进行聚合,不然连那些方法都没得
对于 Flink 而言,DataStream 是没有直接进行聚合的 API 的。因为我们对海量数据做聚合
肯定要进行分区并行处理,这样才能提高效率。所以在 Flink 中,要做聚合,需要先进行分区;
这个操作就是通过 keyBy()来完成的。
keyBy()是聚合前必须要用到的一个算子。keyBy()通过指定键(key),可以将一条流从逻
辑上划分成不同的分区(partitions)。这里所说的分区,其实就是并行处理的子任务,也就对
应着任务槽(task slots)。
基于不同的 key,流中的数据将被分配到不同的分区中去,如图 5-8 所示;这样一来,所
有具有相同的 key 的数据,都将被发往同一个分区,那么下一步算子操作就将会在同一个 slot
中进行处理了
思路:
直接按照 keyBy(_.字段) 进行分组就可以了

package chat01

import org.apache.flink.api.java.functions.KeySelector
import org.apache.flink.streaming.api.scala._

class flink04_keyBy {

}
//这个是聚合算子 keyBy
object flink04_keyBy{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val data: DataStream[Events] = env.fromElements(
      Events("Mary", "./home", 1000L),
      Events("Bob", "./cart", 2000L),
      Events("Alice", "./cart", 3000L)
    )

    data.keyBy(_.user).min(2).print()  //根据索引号来决定根据谁进行分组,也可以直接写入根据哪个字段拍也可以,推荐这个
    data.keyBy( new MyKeySelector).print()  //现在推荐这样写


    env.execute()
  }
  class MyKeySelector() extends KeySelector[Events,String]{  //两个泛型,第一个是输入的类型,第二个是键的类型
    override def getKey(in: Events): String = in.user  //直接这样返回这个字段就可以了,意思是根据这个字段进行排序
  }
}
2) 简单聚合

概念:
有了按键分区的数据流 KeyedStream,我们就可以基于它进行聚合操作了。Flink 为我们
内置实现了一些最基本、最简单的聚合 API,主要有以下几种:

  • sum():在输入流上,对指定的字段做叠加求和的操作。
  • min():在输入流上,对指定的字段求最小值。
  • max():在输入流上,对指定的字段求最大值。
  • minBy():与 min()类似,在输入流上针对指定字段求最小值。不同的是,min()只计
    算指定字段的最小值,其他字段会保留最初第一个数据的值;而 minBy()则会返回包 含字段最小值的整条数据。
  • maxBy():与 max()类似,在输入流上针对指定字段求最大值。两者区别与 min()/minBy()完全一致
    思路:
    max,maxBy 的区别在于,比如max来说,我们要先进行分组,有三个字段,user,dizhi,shijian,我们得指定根据谁来进行进行分组,然后相同的key的都分到一个组,然后我们max(“shijian”),进行指定以哪个字段指定求最大值,我们这里的是以shijian,然后我们的key是有多个数据的,然后会根据时间来排,前面的是没有影响的,然后排到后面遇到了那个最大值,然后所有的shijian的字段的都会按照最大的值来取,maxBy 的话,所以的数据都会按照 shijian 字段最大的那个来取。
package chat01
import org.apache.flink.streaming.api.scala._
//测试简单聚合算子
class flink05_min {

}
object flink05_min{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(4)

    val data: DataStream[Events] = env.fromElements(
      Events("Mary", "./home", 1000L),
      Events("Bob", "./cart", 2000L),
      Events("Alice", "./cart", 3000L),
      Events("Mary", "./prod?id=1", 4000L),
      Events("Mary", "./prod?id=2", 6000L),
      Events("Mary", "./prod?id=3", 5000L)
    )
    val group = data.keyBy(_.user)
    group.maxBy("shijian")   //直接输入字段的名称,+
      .print()


    env.execute()
  }
}

请添加图片描述
并不像基本转换算子那样需要实现自定义函数,只要说明聚合指定的字段就可以了。指定字
段的方式有两种:指定位置,和指定名称。对于元组类型的数据,同样也可以使用这两种方式来指定字段。需要注意的是,元组中字段的名称,是以_1、_2、_3、…来命名的。

3) 规约聚合

与简单聚合类似,reduce()操作也会将 KeyedStream 转换为 DataStream。它不会改变流的
元素数据类型,所以输出类型和输入类型是一样的。
调用 KeyedStream 的 reduce()方法时,需要传入一个参数,实现 ReduceFunction 接口。

口在源码中的定义如下:

public interface ReduceFunction<T> extends Function, Serializable {
T reduce(T value1, T value2) throws Exception;
}
package chat01
import org.apache.flink.api.common.functions.ReduceFunction
import org.apache.flink.streaming.api.scala._

import scala.sys.env
class flink06_reduce {

}
object flink06_reduce{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(4)

    val data: DataStream[Events] = env.fromElements(
      Events("Mary", "./home", 1000L),
      Events("Bob", "./cart", 2000L),
      Events("Alice", "./cart", 3000L),
      Events("Mary", "./prod?id=1", 4000L),
      Events("Mary", "./prod?id=2", 6000L),
      Events("Mary", "./prod?id=3", 5000L)
    )

    //reducu 规约聚合,提取当前最活跃用户 //想要聚合还是得先 keyBy
   data.map(data => (data.user,1L))
     .keyBy(_._1)
     .reduce(new MySum) //统计每个用户的活跃度
     //.reduce((data1,data2) => (data1._1,data1._2+data2._2))
     .keyBy(data => true) //将所有数据按照同样的key分到一个组中
     .reduce((data1,data2) => if (data2._2 > data1._2) data2 else data1) //这个是求最活跃的哪个用户,要是第二个用户的点击次数大于第一个,那么就输出第二个用户
     .print()


    env.execute()
  }
  class MySum() extends ReduceFunction[(String,Long)]{  //两个泛型,第一个是键的所以是String,第二个是我们要聚合的点击次数
    override def reduce(t1: (String, Long), t2: (String, Long)): (String, Long) = { //第一个是聚合好的,然后和第二个聚合
      (t1._1,t1._2 + t2._2)  //是这个意思,第一个参数是t1的key,第二个参数是第一个的值加上第二个的值,然后就等于现在这个key的值
    }
  }
}

在这里插入图片描述

六、Flink 支持的数据类型

1、flink 的类型系统

为了方便地处理数据,Flink 有自己一整套类型系统。Flink 使用“类型信息”
(TypeInformation)来统一表示数据类型。TypeInformation 类是 Flink 中所有类型描述符的基类。
它涵盖了类型的一些基本属性,并为每个数据类型生成特定的序列化器、反序列化器和比较器。

2、flink 支持的数据类型

简单来说,对于常见的 Java 和 Scala 数据类型,Flink 都是支持的。Flink 在内部,Flink
对支持不同的类型进行了划分,这些类型可以在 Types 工具类中找到:
(1)基本类型
所有 Java 基本类型及其包装类,再加上 Void、String、Date、BigDecimal 和 BigInteger。
(2)数组类型
包括基本类型数组(PRIMITIVE_ARRAY)和对象数组(OBJECT_ARRAY)
(3)复合数据类型

  • Java 元组类型(TUPLE):这是 Flink 内置的元组类型,是 Java API 的一部分。最多 25 个字段,也就是从 Tuple0~Tuple25,不支持空字段
  • Scala 样例类及 Scala 元组:不支持空字段
  • 行类型(ROW):可以认为是具有任意个字段的元组,并支持空字段
  • POJO:Flink 自定义的类似于 Java bean 模式的类

(4)辅助类型
Option、Either、List、Map 等
(5)泛型类型(GENERIC)
Flink 支持所有的 Java 类和 Scala 类。不过如果没有按照上面 POJO 类型的要求来定义,
就会被 Flink 当作泛型类来处理。Flink 会把泛型类型当作黑盒,无法获取它们内部的属性;它
们也不是由 Flink 本身序列化的,而是由 Kryo 序列化的。
在这些类型中,元组类型和 POJO 类型最为灵活,因为它们支持创建复杂类型。而相比之
下,POJO 还支持在键(key)的定义中直接使用字段名,这会让我们的代码可读性大大增加。
所以,在项目实践中,往往会将流处理程序中的元素类型定为 Flink 的 POJO 类型。
Flink 对 POJO 类型的要求如下:

  • 类是公共的(public)和独立的(standalone,也就是说没有非静态的内部类);
  • 类有一个公共的无参构造方法;
  • 类中的所有字段是 public 且非 final 修饰的;或者有一个公共的 getter 和 setter 方法,

这些方法需要符合 Java bean 的命名规范。
Scala 的样例类就类似于 Java 中的 POJO 类,所以我们看到,之前的 Event,就是我们创
建的一个 Scala 样例类,使用起来非常方便。

七、flink UDF 实现自定义函数

对于大部分操作而言,都需要传入一个用户自定义函数(UDF),实现相关操作的接口,
来完成处理逻辑的定义。Flink 暴露了所有 UDF 函数的接口,具体实现方式为接口或者抽象类,
例如 MapFunction、FilterFunction、ReduceFunction 等。
所以最简单直接的方式,就是自定义一个函数类,实现对应的接口。
思路;
我们这里自己定义了一个 自定义函数 的filter,想要自定义首先要继承一个类,FilterFunction[Events] 注意他有一个泛型,他这里的泛型我们可以把样例类放进去,然后重写里面的filter方法, 用方法里面的那个参数,比如我们这里的参数是 value 用 value.字段.startWith("sensor_1") 方法 比如我们这里是 以user字段的sensor_1作为基准,筛选只有包含 用户名 sensor_1 的数据。
调用也非常的简单,把数据给读取进来,然后我们要用map方法,先对数据进行分割,这里是用逗号分割,具体的看文件的格式,然后用我们的样例类给包装起来,然后最后调用 filter(new MyFilter) 直接在里面 new 我们刚刚自己写的哪个类就可以了。
就把用户名为 sensor_1 的数据给筛选出来了
在这里插入图片描述

package apitest

import org.apache.flink.api.common.functions.FilterFunction
import org.apache.flink.streaming.api.scala._
case class Events(user:String,url:String,timestamp: Long)
// 自定义函数测试
class flink01_funcation {

}
object flink01_funcation{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val dataInpath = "E:\\study\\BigDatas\\Flinks\\flink\\src\\main\\resources\\sensor.txt"
    val data = env.readTextFile(dataInpath)

    val dataStream: DataStream[Events] = data.map(  //必须要先用map 进行转换之后,然后包装为 DataStream[Events] 里面的泛型才能是样例类
      data => {
        var arr = data.split(",")
        Events(arr(0), arr(1), arr(2).toLong)
      }
    ).filter(new MyFilter) //调用方式 filter()  里面直接new我们刚刚刚刚自己写的哪个filter类就可以了

    val value: DataStream[String] = data.flatMap(
      data => data.split(",")
    )

    
    dataStream.print()
    env.execute()


  }
}

//自定义一个函数类  继承  FilterFunction 类
class MyFilter extends FilterFunction[Events]{  //这里的泛型还是用自己写的样例类
  override def filter(value: Events): Boolean = {  //重写方法
    value.user.startsWith("sensor_1")  //以这个user字段作为 filter的标准

  }
}

还有个富函数,就是继承重写的函数前面加上一个 Rich ,富有就富有在,有生命周期方法,有上下文你的环境对象,然后连接数据库等等。

//富函数:可以获取到运行时上下文,还有一些生命周期

class MyMap extends RichMapFunction[Events,String]{
  override def open(parameters: Configuration): Unit = {
    getRuntimeContext //获取上下文的环境配置,就可以连接数据库操作等等
  }


  override def map(in: Events): String = in.user + "姓名: "

  override def close(): Unit = {
    //一般是收尾工作,比如关闭连接,或者清空状态
  }
}

八、物理分区

1、shuffle 分区

非常的简单,我们只需要在读取完数据之后,然后我们调用 shuffle 方法,就可以直接输出了,我们可以把print 的并行度设置为4,这样方便看效果,他的分区是完全随机的,完全没有规律,所以叫shuffle分区

package chat01

import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment, createTypeInformation}

//shuffle 分区
class flink07_partition_shuffle {

}
object flink07_partition_shuffle{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    //读取自定义的数据源
    val data: DataStream[Events] = env.addSource(new ClickSource) //之前定义的自定义创造数据的

    //洗牌之后打印输出
    data.shuffle.print("shuffle 分区").setParallelism(4)  //因为是要shuffle我们把它的并行度设高一点

    env.execute()

  }
}

他这个分区是毫无规律,完全随机的
在这里插入图片描述

2、轮询分区(Round-Robin)

轮询分区也是一种常见的重分区方式,简单来首就是“发牌”,按照先后顺序将数据做依次分发。

package chat01

import org.apache.flink.streaming.api.scala._

//轮询分区,也就是发牌,第一个到第一个分区,第二个到第二个分区,这个样子的
class flink07_partition_Reblance {

}
object flink07_partition_Reblance{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    //读取自定义的数据源
    val data: DataStream[Events] = env.addSource(new ClickSource) //之前定义的自定义创造数据的

    //洗牌之后打印输出
    data.rebalance.print("轮询分区").setParallelism(4)  //rebalance 方法

    env.execute()

  }
}

这个轮询分区是完全按照顺序来的,第一个就是第一个分区,然后依次
在这里插入图片描述

3、重缩放分区(rescale)

和 轮询分区非常的相似,当调用 rescale() 方法时,其实底层也是使用 Round-Robin 算法进行轮询,可以看做是分组的轮询分区。

package chat01

import org.apache.flink.streaming.api.functions.source.{RichParallelSourceFunction, SourceFunction}
import org.apache.flink.streaming.api.scala._

//rescale 重缩放分区,分了组的轮询分区,节省资源
class flink07_partition_Rescale {

}
object flink07_partition_Rescale{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    //读取自定义的数据源
    val data: DataStream[Int] = env.addSource(new RichParallelSourceFunction[Int] {
      override def run(sourceContext: SourceFunction.SourceContext[Int]): Unit = {
          for (i <- 0 to 7){
            //利用运行时上下文的subTask的id信息,来控制数据由哪个并行子任务生成
            if(getRuntimeContext.getIndexOfThisSubtask == (i + 1) % 2) { //对2进行取余,根据余数进行分组

            }
            sourceContext.collect(i+1)
          }
      }

      override def cancel(): Unit = {

      }
    }).setParallelism(2)

    //洗牌之后打印输出
    data.rescale.print("rescale 分区").setParallelism(4) //rebalance 方法

    env.execute()

  }
}

只需要一个if 判断,偶数的一个分组进行分区,奇数的一个分组进行分区
在这里插入图片描述

4、广播(broadcast)分区

这种方式其实不应该叫做“重分区”,因为经过广播之后,数据会在不同的分区都保留一分,肯呢个进行重复处理,可以通过调用 DataStream 的 broadcast()方法,将输入数据复制并发送到下游算子的并行任务中去。
broadcast 方法,广播分区将相同的数据给每个分区都发送一份

package chat01

import org.apache.flink.streaming.api.scala._

//广播分区
class flink07_partition_Broadcast {

}
object flink07_partition_Broadcast{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    
    //读取自定义的数据源
    val data = env.addSource(new ClickSource)

    data.broadcast.print().setParallelism(4)  //分区输wield4 broadcast 方法,广播分区

    env.execute()


  }
}

5、全局分区(global)慎用!!

全局分区也是一种特殊的分区方式。这种做法非常极端,通过调用.global()方法,会将所
有的输入流数据都发送到下游算子的第一个并行子任务中去。这就相当于强行让下游任务并行
度变成了 1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力。
之前广播是把一份数据给每个分区发一遍,这个是把所有的数据全都发到第一个分区里面,这样会造成程序的压力非常的大。

package chat01

import org.apache.flink.streaming.api.scala._

//广播分区
class flink07_partition_Broadcast {

}
object flink07_partition_Broadcast{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    //读取自定义的数据源
    val data = env.addSource(new ClickSource)

    //data.broadcast.print().setParallelism(4)  //分区输wield4 broadcast 方法,广播分区

    data.global.print().setParallelism(4) //global 全局分区,会把并行度强行变为1
    env.execute()


  }
}

强行变为一个分区,慎用!!
请添加图片描述

6、自定义分区

概念:
当 Flink 提 供 的 所 有 分 区 策 略 都 不 能 满 足 用 户 的 需 求 时 , 我 们 可 以 通 过 使 用partitionCustom()方法来自定义分区策略。
在调用时,方法需要传入两个参数,第一个是自定义分区器(Partitioner)对象,第二个
是应用分区器的字段,它的指定方式与 keyBy 指定 key 基本一样:可以通过字段名称指定,
也可以通过字段位置索引来指定,还可以实现一个 KeySelector 接口。

九、输出算子(Sinkd)

Flink 没有类似于 spark 中 foreach 方法,让用户进行迭代的操作,虽有对外的输出操作都要利用Sink 完成。最后通过类似如下方式完成整个任务最终输出操作。

stream.addSink(new MySink())

大概有两种方式,去进行输出保存,一种是直接 writeAsText("") 方法,里面的参数是保存的路径,但是现在已经不推荐使用这种了,推荐使用 addSink() 方法,里面使用StreamingFileSink.forRowFormat().build() 方法,然后
StreamingFileSink.forRowFormat() 里面有两个参数,第一个是 new Path("") 里面就是要输出的路径,第二个参数是泛型还有字符集new SimpleStringEncoder[Events](),完整的是这个样子
dataStream.addSink( StreamingFileSink.forRowFormat( new Path("E:\\study\\BigDatas\\Flinks\\flink\\src\\main\\resources\\out2.txt"), new SimpleStringEncoder[Events]() ).build() )

package apitest

import org.apache.flink.api.common.serialization.{SimpleStringEncoder, SimpleStringSchema}
import org.apache.flink.core.fs.Path
import org.apache.flink.streaming.api.datastream.DataStreamSink
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink
import org.apache.flink.streaming.api.scala._

// 测试sink 输出的
class flink02_sinkTest {

}
object flink02_sinkTest{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val dataInpath = "E:\\study\\BigDatas\\Flinks\\flink\\src\\main\\resources\\sensor.txt"
    val data = env.readTextFile(dataInpath)

    val dataStream: DataStream[Events] = data.map( //必须要先用map 进行转换之后,然后包装为 DataStream[Events] 里面的泛型才能是样例类
      data => {
        var arr = data.split(",")
        Events(arr(0), arr(1), arr(2).toLong)
      }
    )
    //dataStream.print() //这样直接输出本身就是一个sink
    //输出保存到一个文件里 里面的参数是要保存的路径
    //他这样保存也会分流,这个没设置的话根据默认的线程数来决定
    //dataStream.writeAsText("E:\\study\\BigDatas\\Flinks\\flink\\src\\main\\resources\\out.txt")
    dataStream.addSink(        //这样进行Sink 是推荐的
      StreamingFileSink.forRowFormat(
        new Path("E:\\study\\BigDatas\\Flinks\\flink\\src\\main\\resources\\out2.txt"),
        new SimpleStringEncoder[Events]()
      ).build()
    )
    
    //执行任务
    env.execute()

  }
}

1、StreamingFileSink (写入文件)

概念:
最简单的输出方式,当然就是写入文件了。对应着读取文件作为输入数据源,Flink 本来
也有一些非常简单粗暴的输出到文件的预实现方法:如 writeAsText()、writeAsCsv(),可以直
接将输出结果保存到文本文件或 Csv 文件。目前这些简单的方法已经要被弃用。
Flink 为此专门提供了一个流式文件系统的连接器:StreamingFileSink,它继承自抽象类
RichSinkFunction,而且集成了 Flink 的检查点(checkpoint)机制,用来保证精确一次(exactly
once)的一致性语义。

package chat01

import org.apache.flink.api.common.serialization.SimpleStringEncoder
import org.apache.flink.core.fs.Path
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink
import org.apache.flink.streaming.api.scala._

//测试输出到文件
class flink08_sink_file {

}
object flink08_sink_file{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(4)

    val data: DataStream[Events] = env.fromElements(
      Events("Mary", "./home", 1000L),
      Events("Bob", "./cart", 2000L),
      Events("Alice", "./cart", 3000L),
      Events("Mary", "./prod?id=1", 4000L),
      Events("Mary", "./prod?id=2", 6000L),
      Events("Mary", "./prod?id=3", 5000L)
    )

//直接以文本形式分布式的写入到文件中  先用map 转换为String类型的
    data.map(data => data.toString).addSink( //调用StreamingFileSink.forRowFormat,两个参数一个是保存的路径,还有一个是字符编码
      StreamingFileSink.forRowFormat(new Path("./output"),new SimpleStringEncoder[String]("UTF-8"))
        .build()  //最后forRowFormat 外面调用一个build方法
    )


    env.execute()
  }
}

2、kafka

概念:
Flink 官方为 Kafka 提供了 Source 和 Sink 的连接器,我们可以用它方便地从 Kafka 读写数
据。Flink 与 Kafka 的连接器提供了端到端的精确一次(exactly once)语义保证,这在实际项
目中是最高级别的一致性保证
(1)添加 Kafka 连接器依赖
由于我们已经测试过从 Kafka 数据源读取数据,连接器相关依赖已经引入,这里就不重复
介绍了。
(2)启动 Kafka 集群
(3)编写输出到 Kafka 的示例代码
我们可以直接将用户行为数据保存为文件 clicks.csv,读取后不做转换直接写入 Kafka,主
题(topic)命名为“clicks”。

1) fink 输入 kafka 接收

先在linux 里面创建一个kafka消费者
输入命令:./bin/zkServer.sh start 启动zookeeper,三台机器都要启动
输入命令:./bin/kafka-server-start.sh -daemon ./config/server.properties 启动kafka
输入命令:./bin/kafka-console-consumer.sh --bootstrap-server 192.168.10.102:9092 --topic clicks 创建kafka消费者主题

package apitest

import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka._

import java.util.Properties

//测试 kafka Sink 写一段flink 代码,把处理好的输出,存到kafka里面
class flink03_kafkaSink extends Serializable {

}
object flink03_kafkaSink extends Serializable {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val dataInpath = "E:\\study\\BigDatas\\Flinks\\flink\\src\\main\\resources\\sensor.txt"
    val data = env.readTextFile(dataInpath)

    val dataStream = data.map( //必须要先用map 进行转换之后,然后包装为 DataStream[Events] 里面的泛型才能是样例类
      data => {
        var arr = data.split(",")
        Events(arr(0), arr(1), arr(2).toLong).toString //因为是要连接kafka,要进行序列化Events他看不懂,我们给他转换为String
      }
    )

//    val properties = new Properties()
//    properties.setProperty("bootstrap.servers", "192.168.10.102:9092")
//    properties.setProperty("group.id", "sinktest")
    //第一个参数是主机和端口号,然后第二个是 topic 主题,然后new 一个SimpleStringSchema()
    dataStream.addSink(
     new FlinkKafkaProducer[String]("192.168.10.102:9092","sinktest",new SimpleStringSchema())
    )


    env.execute()
  }
}

然后这边运行程序,flink这边发送完数据之后,
在这里插入图片描述
kafka 这边数据就接收到了
在这里插入图片描述

2) kafka 输入 kafka 接收

这种被称之为管道

package apitest

import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.{FlinkKafkaConsumer, FlinkKafkaProducer}

import java.util.Properties

class flink03_kafkaSink2 {

}
//这种是kafka输入,kafka接受
object flink03_kafkaSink2{
  def main(args: Array[String]): Unit = {
   val env = StreamExecutionEnvironment.getExecutionEnvironment

    //连接kafka的环境
    val properties = new Properties()
    properties.setProperty("bootstrap.servers","192.168.10.102:9092")
    //properties.setProperty("group.id","consumer.group")

    //先创建kafka的连接器,然后创建consumer生产者
    val data = env.addSource(new FlinkKafkaConsumer[String](
      "test",new SimpleStringSchema(),properties
    ))

    //将数据进行转换,分割
    val mapData = data.map(
      data => {
        val arr = data.split(",")
        Events(arr(0),arr(1),arr(2).toLong).toString  //因为将进行序列化,我们将他给转换为String会比较方便一点
      }
    )

    //创建kafka producer生产者
    mapData.addSink(
      new FlinkKafkaProducer[String]("test","192.168.10.102:9092",new SimpleStringSchema())
    )

    env.execute()

  }
}

3、Mysql

官方有Jdbc的连接器,我们就直接,data.addSink(new JdbcSink().sink()) 这个sink里面的第一个参数是sql 写入语句,比如插入
" insert into clicks(user,url) values(?,?)" 没有具体的数据,就先用占位符占着,然后还有第二个参数,new JdbcStatementBuilder[Events]{} 是Jdbc的一个Builder,然后里面有个泛型,就是当前的数据类型啦,然后里面要重写一个方法,要选取字段,还有获取我们连接mysql数据库的数据库和表,还有用户和密码等配置
先导入依赖

<dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.32</version>
    </dependency>

编写代码

package chat01

import org.apache.flink.connector.jdbc.{JdbcConnectionOptions, JdbcSink, JdbcStatementBuilder}
import org.apache.flink.streaming.api.scala._

import java.sql.PreparedStatement

class flink09_sink_mysql {

}
object flink09_sink_mysql{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    val stream = env.fromElements(

      Events("Mary", "./home", 1000L),
      Events("Bob", "./cart", 2000L),
      Events("Alice", "./prod?id=100", 3000L),
      Events("Alice", "./prod?id=200", 3500L),
      Events("Bob", "./prod?id=2", 2500L),
      Events("Alice", "./prod?id=300", 3600L),
      Events("Bob", "./home", 3000L),
      Events("Bob", "./prod?id=1", 2300L),
      Events("Bob", "./prod?id=3", 3300L)
    )
    stream.addSink(
      JdbcSink.sink(
        "INSERT INTO clicks (user, url) VALUES (?, ?)",
        new JdbcStatementBuilder[Events] {
          override def accept(t: PreparedStatement, u: Events): Unit = {
            t.setString(1, u.user)
            t.setString(2, u.url)
          }
        },
        new
            JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
          .withUrl("jdbc:mysql://192.168.10.102:3306/test")
          .withDriverName("com.mysql.cj.jdbc.Driver")
          .withUsername("root")
          .withPassword("p@ssw0rd")
          .build()
      )
    )
    env.execute()
  }
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值