Flink 从 0 到 1 学习 —— 第五章 Flink 流处理 API

1. Flink流处理API

在这里插入图片描述

1.1 Environment

1.1.1 getExecutionEnvironment

创建一个执行环境,表示当前执行程序的上下文。如果程序是独立调用的,则此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回此集群的执行环境,也就是说,getExecutionEnvironment会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式。

val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

如果没有设置并行度,会以flink-conf.yaml中的配置为准,默认是1。

parallelism.default: 1

1.1.2 createLocalEnvironment

返回本地执行环境,需要在调用时指定默认的并行度。

val env: StreamExecutionEnvironment = StreamExecutionEnvironment.createLocalEnvironment(1)

1.1.3 createRemoteEnvironment

返回集群执行环境,将Jar提交到远程服务器。需要在调用时指定JobManager
的IP和端口号,并指定要在集群中运行的Jar包。

val env: StreamExecutionEnvironment = StreamExecutionEnvironment.createRemoteEnvironment("jobManagerHost",6123,"jarPath")

1.2 Source

1.2.1 从集合读取数据

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

case class SensorReading(id:String, timestamp: Long, temperature: Double)
object Sensor {
    
    

  def main(args: Array[String]): Unit = {
    
    
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    val strem: DataStream[SensorReading] = env.fromCollection(List(
      SensorReading("sensor_1", 1547718199, 35.80018327300259),
      SensorReading("sensor_6", 1547718201, 15.402984393403084),
      SensorReading("sensor_7", 1547718202, 6.720945201171228),
      SensorReading("sensor_10", 1547718205, 38.101067604893444)
    ))
    strem.print("从集合中读取数据")
    env.execute("Sensor")
  }
}

1.2.2 从文件中读取数据

env.readTextFile("/data.txt")

1.2.3 以kafka消息队列的数据作为来源

需要添加 kafka 依赖

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka-0.11_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

具体实现:

kafka消费者配置

def getPro():Properties = {
    
    
    val props = new Properties
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, broker_list)
    props.put(ConsumerConfig.GROUP_ID_CONFIG, "flink_consumer_group")
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer")
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer")
    props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true")
    //如果单条消息超过1 MB,建议设置为1。
    props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "1000")
    //设置比单条消息的大小略大一点。
    props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, "1024")
    props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, "1024")
    props
}
// 初始化环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 获取kafka配置
val properties = getPro()
// 创建kafka消费者
val consumer = new FlinkKafkaConsumer011(topic,new SimpleStringSchema,properties)
// 指定kafka消费位置
consumer.setStartFromEarliest()
// 添加数据源
val dataStream: DataStream[String] = env.addSource(consumer)
dataStream.print()
env.execute("flink_source_kafka")

1.2.4 自定义 Source

除了以上的source数据来源,我们还可以自定义source。需要做的,只是传入一个SourceFunction就可以。具体调用如下:

val stream = env.addSource(new MySensorSource() )
package com.flink.scala

import org.apache.flink.streaming.api.functions.source.SourceFunction

import scala.collection.immutable
import scala.util.Random

class MySensorSource extends SourceFunction[SensorReading]{
    
    

  var running: Boolean = true
  override def run(ctx: SourceFunction.SourceContext[SensorReading]): Unit = {
    
    
    // 初始化一个随机数发生器
    val rand = new Random()
    var curTemp: immutable.IndexedSeq[(String, Double)] = 1.to(10).map(
      i => ("sensor_" + i, 65 + rand.nextGaussian() * 20)
    )
    while (running) {
    
    
      // 更新温度值
      curTemp = curTemp.map(
        t => (t._1,t._2 + rand.nextGaussian())
      )

      //获取当前时间戳
      val curTime = System.currentTimeMillis()

      curTemp.foreach(
        t => ctx.collect(SensorReading(t._1, curTime, t._2))
      )
      Thread.sleep(100)
    }
  }

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

1.3 Transform 转换算子

1.3.1 map

在这里插入图片描述

val streamMap = steam.map(x => (x,1))

1.3.2 flatMap

flatMap的函数签名:def flatMap[A,B](as: List[A])(f: A⇒List[B]): List[B]
例如:flatMap(List(1,2,3))(i⇒List(i,i))
结果是List(1,1,2,2,3,3),
而List(“a b”, “c d”).flatMap(line⇒line.split(" "))
结果是List(a, b, c, d)。

val streamFlatMap = stream.flatMap (
    x => x.split(",")
)

1.3.3 Filter

在这里插入图片描述

val streamFilter = stream.filter(
    x => x == 1
)

1.3.4 keyBy

在这里插入图片描述

DataStream→KeyedStream:逻辑地将一个流拆分成不相交的分区,每个分
区包含具有相同key的元素,在内部以hash的形式实现的。

1.3.5 滚动聚合算子 (Rolling Aggregation)

这些算子可以针对KeyedStream的每一个支流做聚合。

  • sum()
  • min()
  • max()
  • minBy()
  • maxBy()

注意:max 和 maxBy之间的区别在于 max 返回流中最大的值,但 maxBy 返回具有最大值的元素,min 和 minBy 同理。

1.3.6 Reduce

KeyedStream→DataStream:一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。

val stream = env.readTextFile("/data.txt").map( data => {
    
    
    valdataArray = data.split(",")
    SensorReading(dataArray(0).trim, dataArray(1).trim.toLong,
dataArray(2).trim.toDouble)
})
.keyBy("id")
.reduce( (x, y) =>SensorReading(x.id, x.timestamp +1, y.temperature) )

1.3.7 split 和 select

1. split

在这里插入图片描述

DataStream→SplitStream:根据某些特征把一个DataStream拆分成两个或者多个DataStream。

2. select

在这里插入图片描述

SplitStream→DataStream:从一个SplitStream中获取一个或者多个DataStream。需求:传感器数据按照温度高低(以30度为界),拆分成两个流。

val splitStream = stream.split( sensorData => {
    
    
    if(sensorData.temperature >30)Seq("high")elseSeq("low")
})
val high = splitStream.select("high")
val low = splitStream.select("low")
val all = splitStream.select("high","low")

1.3.8 connect 和 coMap

在这里插入图片描述

图Connect算子
**DataStream,DataStream→ConnectedStreams**:连接两个保持他们类型的数据 流,两个数据流被Connect之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。

在这里插入图片描述

图CoMap/CoFlatMap
**ConnectedStreams → DataStream**:作用于ConnectedStreams上,功能与map 和flatMap一样,对ConnectedStreams中的每一个Stream分别进行map和flatMap 处理。
val warning = high.map( 
    sensorData => (sensorData.id,sensorData.temperature) 
)
val connected = warning.connect(low)
val coMap = connected.map(
    warningData => (warningData._1, warningData._2,"warning"),
    lowData => (lowData.id,"healthy")
)

1.3.9 union

在这里插入图片描述

图Union
**DataStream→DataStream**:对两个或者两个以上的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream。
// 合并以后打印
val unionStream: DataStream[StartUpLog] = appStoreStream.union(otherStream)unionStream.print("union:::")

connect 与 union 的区别:

  1. union之前两个流的类型必须是一样,connect可以不一样,在之后的coMap
    中再去调整成为一样的。
  2. connect只能操作两个流,union可以操作多个。

1.4 支持的数据类型

Flink流应用程序处理的是以数据对象表示的事件流。所以在Flink内部,我们需要能够处理这些对象。它们需要被序列化和反序列化,以便通过网络传送它们;或者从状态后端、检查点和保存点读取它们。为了有效地做到这一点,Flink需要明确知道应用程序所处理的数据类型。Flink使用类型信息的概念来表示数据类型,并为每个数据类型生成特定的序列化器、反序列化器和比较器。

Flink还具有一个类型提取系统,该系统分析函数的输入和返回类型,以自动获取类型信息,从而获得序列化器和反序列化器。但是,在某些情况下,例如lambda
函数或泛型类型,需要显式地提供类型信息,才能使应用程序正常工作或提高其性能。

Flink支持Java和Scala中所有常见数据类型。使用最广泛的类型有以下几种。

1.4.1 基础数据类型

Flink支持所有的Java和Scala基础数据类型,Int, Double, Long, String, …

val numbers:DataStream[Long]=env.fromElements(1L,2L,3L,4L)
numbers.map(n=>n+1)

1.4.2 Java 和 Scala 元组 (Tuples)

val persons:DataStream[(String,Integer)]=env.fromElements(
    ("Adam",17),
    ("Sarah",23)
)
persons.filter(p => p._2 > 18)

1.4.3 Scala 样例类 (case classes)

case class Person(name:String,age:Int)
valpersons:DataStream[Person]=env.fromElements(
    Person("Adam",17),
    Person("Sarah",23)
)
persons.filter(p => p.age > 18)

1.4.4 Java 简单对象

public class Person {
    
    
    public String name;
    public int age;
    public Person () {
    
    }
    public Person(String name, int age) {
    
    
        this.name = name;
        this.age = age;
    }
}

1.4.5 其他 (Arrays, List, Map, Enum 等等)

Flink对Java和Scala中的一些特殊目的的类型也都是支持的,比如Java的 ArrayList,HashMap,Enum等等。

1.5 实现 UDF 函数 —— 更细粒度的控制流

1.5.1 函数类 (Function Classes)

Flink暴露了所有 UDF 函数的接口(实现方式为接口或者抽象类)。例如 MapFunction, FilterFunction, ProcessFunction等等。

下面例子实现了FilterFunction接口:

class FilterFilter extends FilterFunction[String]{
    
    
    overridedeffilter(value:String):Boolean={
    
    
        value.contains("flink")
    }
}
val filterStream=stream.filter(new FlinkFilter)

还可以将函数实现成匿名类

val filterStream = stream.filter(
	new RichFilterFuntion[String] {
    
    
        override def filter(value:String):Boolean={
    
    
            value.contains("flink")
        }
    }
)

我们 filter 的字符串 “flink” 还可以当做参数传进去。

val filterStream = stream.filter(new KeywordFilter("flink"))
class KeywordFilter(keyWord:String) extends FilterFunction[String]{
    
    
    override def filter(value:String):Boolean={
    
    
        value.contains(keyWord)
    }
}

1.5.2 匿名函数 (Lambda Funtions)

val filterStream = stream.filter(_.contains("flink"))

1.5.3 富函数 (Rich Functions)

富函数”是DataStream API提供的一个函数类的接口,所有Flink函数类都有其Rich版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。

  • RichMapFunction
  • RichFlatMapFunction
  • RichFilterFunction

Rich Function有一个生命周期的概念。典型的生命周期方法有:

  • open()方法是 Rich Function 的初始化方法,当一个算子例如map或者filter被调用之前open()会被调用。
  • close()方法是生命周期中的最后一个调用的方法,做一些清理工作。
  • getRuntimeContext()方法提供了函数的RuntimeContext的一些信息,例如函数执行的并行度,任务的名字,以及state状态。
package com.flink.scala

import org.apache.flink.api.common.functions.RichFlatMapFunction
import org.apache.flink.configuration.Configuration
import org.apache.flink.util.Collector

class MyFlatMap extends RichFlatMapFunction[Int ,(Int,Int)] {
    
    
  var subTaskIndex = 0;

  override def open(config: Configuration): Unit = {
    
    
    subTaskIndex = getRuntimeContext.getIndexOfThisSubtask
    // todo 这里可以做一些初始化工作,例如建立一个 HDFS 的连接
  }

  override def flatMap(in: Int, collector: Collector[(Int, Int)]): Unit = {
    
    
    if (in % 2 == subTaskIndex) {
    
    
      collector.collect((subTaskIndex,in))
    }
  }

  override def close(): Unit = {
    
    
    // todo 这里可以做一些清理工作,比如关闭 HDFS 的连接
  }
}

1.6 Sink

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

stream.addSink(new MySink())

官方提供了一部分的框架的sink。除此以外,需要用户自定义实现sink。

在这里插入图片描述

在这里插入图片描述

1.6.1 Sink Kafka

pom 添加依赖:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka-0.11_2.11</artifactId>
    <version>1.10.0</version>
</dependency>

主函数添加 sink:

stream.addSink(new FlinkKafkaProducer011[String]("localhost:9092","sinkTopic",new SimpleStringSchema()))

def getProp: Properties = {
    
    
    val props = new Properties
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
    props.put(ProducerConfig.CLIENT_ID_CONFIG, "car_kafka")
    props.put(ProducerConfig.ACKS_CONFIG, "-1")
    props.put(ProducerConfig.RETRIES_CONFIG, "3")
    // 发到每个分区的消息缓存量,提高吞吐量
    props.put(ProducerConfig.BATCH_SIZE_CONFIG, 4096)
    // 每条消息待在缓存中的最长时间,控制延迟
    props.put(ProducerConfig.LINGER_MS_CONFIG, 10000)
    // 所有缓存消息的总体大小超过这个数值后,就会触发把消息发往服务器
    // 此时会忽略batch.size和linger.ms的限制。
    // 默认数值是32 MB
    props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 40960)
    props
  }
val producerConfig = getProp
stream.addSink(new FlinkKafkaProducer011[String]("sinkTopic",new SimpleStringSchema(),producerConfig))

1.6.2 Sink Redis

pom添加依赖:

<dependency>
    <groupId>org.apache.bahir</groupId>
    <artifactId>flink-connector-redis_2.11</artifactId>
    <version>1.0</version>
</dependency>

定义一个 redis 的 Mapper 类,用于定义保存到 redis 时调用的命令:

class MyRedisMapper() extends RedisMapper[SensorReading] {
    
    
    /**
    *  定义保存到数据的 redis 命令
    * @return
    */
    override def getCommandDescription: RedisCommandDescription = {
    
    
        new RedisCommandDescription(RedisCommand.HSET,"aaaa")
    }
    
    /**
    * 定义保存到redis的 key
    * @param t
    * @return
    */
    override def getKeyFromData(t: SensorReading): String = t.id
    
    /**
    * 定义保存到redis 的 value
    * @param t
    * @return
    */
    override def getValueFromData(t: SensorReading): String = t.temperature.toString
}

主函数中调用:

val conf = new FlinkJedisPoolConfig.Builder()
      .setHost("localhost")
      .setPort(6379)
      .setPassword("123456")
      .build()

stream.addSink(new RedisSink[SensorReading](conf,new MyRedisMapper))

1.6.4 Sink Elasticsearch

pom添加依赖:

<!-- flink-connector-elasticsearch6 -->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-elasticsearch6_2.11</artifactId>
    <version>1.10.0</version>
</dependency>

在主函数中调用:

package com.flink.scala

import java.util

import org.apache.flink.api.common.functions.RuntimeContext
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.elasticsearch.{
    
    ElasticsearchSinkFunction, RequestIndexer}
import org.apache.flink.streaming.connectors.elasticsearch6.ElasticsearchSink
import org.apache.http.HttpHost
import org.elasticsearch.client.Requests


object ElasticSearchSinkTest {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(10)
    val stream = env.fromCollection(List(
      new SensorReading("sensor_1", 1547718199, 35.80018327300259),
      new SensorReading("sensor_6", 1547718201, 15.402984393403084),
      new  SensorReading("sensor_7", 1547718202, 6.720945201171228),
      new SensorReading("sensor_10", 1547718205, 38.101067604893444)
    ))

    val httpHosts = new util.ArrayList[HttpHost]()
    httpHosts.add(new HttpHost("localhost",9200))
    val builder = new ElasticsearchSink.Builder[SensorReading](httpHosts, new ElasticsearchSinkFunction[SensorReading]() {
    
    
      override def process(t: SensorReading, runtimeContext: RuntimeContext, requestIndexer: RequestIndexer): Unit = {
    
    
        println("saving data: " + t)
        val json = new util.HashMap[String, String]()
        json.put("data", t.toString)
        val indexRequest =
          Requests.indexRequest().index("sensor").`type`("info").source(json)
        requestIndexer.add(indexRequest)
        println("saved successfully")
      }
    }).build()

    stream.addSink(builder)

    env.execute("es sink")
  }
}

1.6.4 JDBC 自定义 sink

pom 添加依赖:

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

自定义mysql sink:

package com.flink.scala

import java.sql.{
    
    Connection, DriverManager, PreparedStatement}

import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.{
    
    RichSinkFunction, SinkFunction}

class MysqlSink extends RichSinkFunction[SensorReading] {
    
    

  var conn : Connection = _
  var insertStmt : PreparedStatement = _
  var updateStmt: PreparedStatement = _

  override def open(parameters: Configuration): Unit = {
    
    
    super.open(parameters)
    val url = "jdbc:mysql://localhost:3306/db"
    val username = "root"
    val password = "123456"
    conn = DriverManager.getConnection(url,username,password)
    val insertSql = "insert into temperatures (sensor, temp) values (?,?)"
    insertStmt = conn.prepareStatement(insertSql)
    val updateSql = "update temperatures set temp = ? where sensor = ?"
    updateStmt = conn.prepareStatement(updateSql)
  }

  override def invoke(value: SensorReading, context: SinkFunction.Context[_]): Unit = {
    
    
    updateStmt.setDouble(1,value.temperature)
    updateStmt.setString(2,value.id)
    updateStmt.execute()
    if (updateStmt.getUpdateCount == 0) {
    
    
      insertStmt.setString(1,value.id)
      insertStmt.setDouble(2,value.temperature)
      insertStmt.execute()
    }
  }


  override def close(): Unit = {
    
    
    insertStmt.close()
    updateStmt.close()
    conn.close()
  }

}

在 main 方法中添加:

dataStream.addSink(new MysqlSink())

猜你喜欢

转载自blog.csdn.net/dwjf321/article/details/109068212