Flink流處理API代碼詳解,含多種Source、Transform、Sink案例,Flink學習入門(二)

大家好,我是後來,我會分享我在學習和工作中遇到的點滴,希望有機會我的某篇文章能夠對你有所幫助,所有的文章都會在公衆號首發,歡迎大家關注我的公衆號" 後來X大數據 ",感謝你的支持與認可。

又是一週沒更文了,上週末回運城看牙去了,一直都在路上,太累了。說回正題,關於flink的入門在上一篇已經講過了。

今天主要說一下關於流處理的API,這一篇所有的代碼都是scala。

那麼我們還得回到上次的WordCount代碼,Flink程序看起來像轉換數據集合的常規程序。每個程序都包含相同的基本部分:

  1. 獲得execution environment
  2. 加載/創建初始數據
  3. 指定對此數據的轉換
  4. 指定將計算結果放在何處
  5. 觸發程序執行
    在這裏插入圖片描述

獲取執行環境

所以要想處理數據,還得從獲取執行環境來說起。StreamExecutionEnvironment是所有Flink程序的基礎,所以我們來獲取一個執行環境。有以下3種靜態方法

  1. getExecutionEnvironment()
  2. createLocalEnvironment()
  3. createRemoteEnvironment(host: String, port: Int, jarFiles: String*)
 //獲取上下文環境
val contextEnv = StreamExecutionEnvironment.getExecutionEnvironment
//獲取本地環境
val localEnv = StreamExecutionEnvironment.createLocalEnvironment(1)
//獲取集羣環境
val romoteEnv = StreamExecutionEnvironment.createRemoteEnvironment("bigdata101",3456,2,"/ce.jar")

但一般來說,我們只需要使用第一種getExecutionEnvironment(),因爲**它將根據上下文執行正確的操作;**也即是說,它會根據查詢運行的方式決定返回什麼樣的運行環境,你是IDE執行,它會返回本地執行環境,如果你是集羣執行,它會返回集羣執行環境。

預定義的數據流源

好了,獲取到環境之後,我們就開始獲取數據源,flink自身是支持多數據源的,首先來看幾個預定義的數據流源

  • 基於文件
    1. readTextFile(path)- TextInputFormat 逐行讀取文本文件,並將其作爲字符串返回,只讀取一次。
    2. readFile(fileInputFormat, path) -根據指定的文件輸入格式讀取文件,只讀取一次。

但事實上,上面的2個方法內部都是調用的readFile(fileInputFormat, path, watchType, interval, pathFilter, typeInfo)
我們來看看源碼:
在這裏插入圖片描述
在這裏插入圖片描述
我們選擇其中第一個比較簡單的方法進入,就看到了下圖,發現其實上述的2種方法最終都會落到這個readFile(fileInputFormat, path, watchType, interval, pathFilter)方法上,只不過後面的參數都是默認值了。
在這裏插入圖片描述
所以,這些參數當然也可以自己指定。好了,這個方法大家也不常用,所以就簡單介紹下,有需要的小夥伴自己試試這些後面的參數。

val wordDS: DataStream[String] = contextEnv.socketTextStream("bigdata101",3456)

套接字是抽象的,只是爲了表示TCP連接而存在。

  • 基於集合
  1. fromCollection(Seq)-從Java Java.util.Collection創建數據流。集合中的所有元素必須具有相同的類型。
  2. fromCollection(Iterator)-從迭代器創建數據流。該類指定迭代器返回的元素的數據類型。
  3. fromElements(elements: _*)-從給定的對象序列創建數據流。所有對象必須具有相同的類型。
  4. fromParallelCollection(SplittableIterator)-從迭代器並行創建數據流。該類指定迭代器返回的元素的數據類型。
  5. generateSequence(from, to) -並行生成給定間隔中的數字序列。

這些預設的數據源使用的也不是很多,可以說是幾乎不用。所以大家可以自己嘗試一下。
當然注意,如果使用 fromCollection(Seq),因爲是從Java.util.Collection創建數據流,所以如果你是用scala編程,那麼就需要引入 隱式轉換

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

獲取數據源Source

大家也能發現,以上的方法幾乎都是從一個固定的數據源中獲取數據,適合自己測試,但在生產中肯定是不能使用的,所以我們來看看正兒八經的數據源:
官方支持的source與sink如下:

  1. Apache Kafka(源/接收器)
  2. Apache Cassandra(接收器)
  3. Amazon Kinesis Streams(源/接收器)
  4. Elasticsearch(接收器)
  5. Hadoop文件系統(接收器)
  6. RabbitMQ(源/接收器)
  7. Apache NiFi(源/接收器)
  8. Twitter Streaming API(源)
  9. Google PubSub(源/接收器)

加粗的3個日常中比較常用的,那麼也發現其實數據源只有kafka,sink有ES和HDFS,那麼我們先來說說kafka Source,關於Kafka的安裝部署這裏就不講了,自行Google。我們來貼代碼與分析。

Kafka Source

  1. 在pom.xml中導入kafka依賴
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-connector-kafka-0.11_2.11</artifactId>
  <version>1.7.2</version>
</dependency>

這裏就涉及到了版本的問題:大家可以根據自己的版本進行調試。但是注意:目前flink 1.7版本開始,通用Kafka連接器被視爲處於BETA狀態,並且可能不如0.11連接器那麼穩定。所以建議大家使用flink-connector-kafka-0.11_2.11

  1. 貼測試代碼
import java.util.Properties
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka. FlinkKafkaConsumer011
import org.apache.flink.streaming.api.scala._

/**
  * @description: ${kafka Source測試}
  * @author: Liu Jun Jun
  * @create: 2020-06-10 10:56
  **/
object kafkaSource {

  def main(args: Array[String]): Unit = {
//獲取執行環境
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val properties = new Properties()
    //配置kafka連接器
    properties.setProperty("bootstrap.servers", "bigdata101:9092")
    properties.setProperty("group.id", "test")
    //設置序列化方式
    properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    //設置offset消費方式:可設置的參數爲:earliest,latest,none
    properties.setProperty("auto.offset.reset", "latest")
    //earliest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,從頭開始消費
    //latest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,消費新產生的該分區下的數據
    //none:topic各分區都存在已提交的offset時,從offset後開始消費;只要有一個分區不存在已提交的offset,則拋出異常

    val kafkaDS: DataStream[String] = env.addSource(
      new FlinkKafkaConsumer011[String](
        "test1",
        new SimpleStringSchema(),
        properties
      )
    )
    kafkaDS.print("測試:")
    env.execute("kafkaSource")
  }
}

在這裏插入圖片描述
在這個裏面,要注意的是消費offset的方式,3個參數的區別:

  1. 如果存在已經提交的offest時,不管設置爲earliest 或者latest 都會從已經提交的offest處開始消費
  2. 如果不存在已經提交的offest時,earliest 表示從頭開始消費,latest 表示從最新的數據消費,也就是新產生的數據.
  3. none :topic各分區都存在已提交的offset時,從提交的offest處開始消費;只要有一個分區不存在已提交的offset,則拋出異常

關於kafka序列化的設置這個需要根據實際的需求配置。

上面只是簡單的使用Kafka作爲了數據源獲取到了數據,至於怎麼做檢查點以及精準一次性消費這類共性話題,我們之後單獨拿出來再講,這次先把基本的API過一下

Transform 算子

說完了Source,接下來就是Transform,這類的算子可以說是很多了,官網寫的非常全,我把鏈接貼在這裏,大家可以直接看官網:flink官網的轉換算子介紹

而我們常用的也就是下面這些,功能能spark的算子可以說是幾乎一樣。所以我們簡單看一下:

  1. Map 映射-----以元素爲單位進行映射,會生成新的數據流;DataStream → DataStream
//輸入單詞轉換爲(word,1)
wordDS.map((_,1))
  1. FlatMap 壓平,DataStream → DataStream
//輸入的一行字符串按照空格切分單詞
dataStream.flatMap(_.split(" "))
  1. Filter 過濾,DataStream → DataStream
//過濾出對2取餘等於0的數字
dataStream.filter(_ % 2 == 0
  1. KeyBy 分組
//計算wordCount,按照單詞分組,這裏的0指的是tuple的位數,因爲(word,1)這類新的流,按照word分組,而word就是第0位
wordDS.map((_,1)).keyBy(0)
  1. reduce 聚合
//對上述KeyBy後的(word,count)做聚合,合併當前的元素和上次聚合的結果,實現了wordCount
wordDS.map((_,1)).keyBy(0).reduce{
      (s1,s2) =>{
        (s1._1,s1._2 + s2._2)
      }
    }

關於雙流join與窗口的算子我在下一站會着重說,這一篇先了解一些常用的簡單的API目的就達到了。

函數

在flink中,對數據處理,除了上述一些簡單的轉換算子外,還經常碰到一些無法通過上述算子解決的問題,於是就需要我們自定義實現UDF函數
關於UDF,UDTF,UDAF
UDF:User Defined Function,用戶自定義函數,一進一出
UDAF:User- Defined Aggregation Funcation 用戶自定義聚合函數,多進一出
UDTF: User-Defined Table-Generating Functions,用戶定義表生成函數,用來解決輸入一行輸出多行

UDF函數類

其實在我們常用的算子如map、filter等都暴露了對應的接口,可以自定義實現:
舉例如map:

 val StuDS: DataStream[Stu] = kafkaDS.map(
 //在內部我們可以自定義實現MapFunction,從而實現類型的轉換
      new MapFunction[ObjectNode, Stu]() {
        override def map(value: ObjectNode): Stu = {
          JSON.parseObject(value.get("value").toString, classOf[Stu])
        }
      }
    )

富函數Rich Functions

除了上述的函數外,使用多的就還有富函數Rich Functions,所有Flink函數類都有其Rich版本。它與常規函數的不同在於,可以獲取運行環境的上下文,並擁有一些生命週期方法, open,close,getRuntimeContext,和 setRuntimeContext,所以可以實現更復雜的功能,比如累加器和計算器等。

那我們來簡單實現一個累加器

累加器是具有加法運算和最終累加結果的簡單結構,可在作業結束後使用。最簡單的累加器是一個計數器:您可以使用Accumulator.add(V value)方法將其遞增 。在工作結束時,Flink將彙總(合併)所有部分結果並將結果發送給客戶端。

import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.api.common.accumulators.IntCounter
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.configuration.Configuration

/**
  * @description: ${description}
  * @author: Liu Jun Jun
  * @create: 2020-06-12 17:55
  **/
object AccumulatorTest {

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

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

    val dataDS = env
        .readTextFile("input/word.txt")
      //.socketTextStream("bigdata101", 3456)


    val resultDS: DataStream[String] = dataDS.map(new RichMapFunction[String, String] {

      //第一步:定義累加器
      private val numLines = new IntCounter

      override def open(parameters: Configuration): Unit = {
        super.open(parameters)
        //註冊累加器
        getRuntimeContext.addAccumulator("num-lines", this.numLines)
      }

      override def map(value: String): String = {
        this.numLines.add(1)
        value
      }

      override def close(): Unit = super.close()
    })
    resultDS.print("單詞輸入")

    val jobExecutionResult = env.execute("單詞統計")
    //輸出單詞個數
    println(jobExecutionResult.getAccumulatorResult("num-lines"))
  }
}

注意:這個案例中,我使用的是有限流,原因是這個累加器的值只有在最後程序的結束的時候才能打印出來,或者是可以直接在Flink UI中體現。

那麼如何實現隨時打印打印出累加器的值呢?那就需要我們自定義實現累加器了:

而實現自定義的累加器我還沒寫完。。。。。

Sink

那麼當我們通過flink對數據處理結束後,要把結果數據放到相應的數據存放點,也就是sink了,方便後續通過接口調用做報表統計。

那麼數據放哪裏呢?

  1. ES
  2. redis
  3. Hbase
  4. MYSQL
  5. kafka

ES sink

關於ES的介紹,我也發過一篇文章,只不過是入門級別的,有需要的可以看看,貼鏈接如下:ES最新版快速入門詳解

來吧,貼代碼,注意看其中的註釋

import org.apache.flink.api.common.functions.RuntimeContext
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.datastream.DataStream
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.elasticsearch.{ElasticsearchSinkFunction, RequestIndexer}
import org.apache.flink.streaming.connectors.elasticsearch6.ElasticsearchSink
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011
import org.apache.http.HttpHost
import org.elasticsearch.action.index.IndexRequest
import org.elasticsearch.client.Requests

/**
  * @description: ${description}
  * @author: Liu Jun Jun
  * @create: 2020-06-01 11:44
  **/
object flink2ES {
  def main(args: Array[String]): Unit = {
    // 1.獲取執行環境
    val env: StreamExecutionEnvironment =
      StreamExecutionEnvironment.getExecutionEnvironment
      //2. 設置並行度爲2
    env.setParallelism(2)
    //3. 設置關於kafka數據源的配置,主題,節點,消費者組,序列化,消費offset形式
    val topic = "ctm_student"
    val properties = new java.util.Properties()
    properties.setProperty("bootstrap.servers", "bigdata101:9092")
    properties.setProperty("group.id", "consumer-group")
    properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("auto.offset.reset", "latest")

    // 從kafka中獲取數據
    val kafkaDS: DataStream[String] =
      env.addSource(
        new FlinkKafkaConsumer011[String](
          topic,
          new SimpleStringSchema(),
          properties) )

	//添加ES連接
    val httpHosts = new java.util.ArrayList[HttpHost]()
    httpHosts.add(new HttpHost("bigdata101", 9200))
    
    //創建ESSink對象,在其中對數據進行操作
    val esSinkBuilder = new ElasticsearchSink.Builder[String](
    httpHosts, new ElasticsearchSinkFunction[String] {
        def createIndexRequest(element: String): IndexRequest = {
          val json = new java.util.HashMap[String, String]
          json.put("data", element)

          return Requests.indexRequest()
            .index("ws")
            .`type`("readingData")
            .source(json)
        }
        //重寫process方法,對輸入的數據進行處理,runtimeContext爲上下文環境,requestIndexer爲操作index的對象
     override def process(t: String, runtimeContext: RuntimeContext, requestIndexer: RequestIndexer): Unit = {

		//在add方法中,參數可以爲:增、刪、改、行動請求
       requestIndexer.add(createIndexRequest(t))
       println("saved successfully")
      }
    })
    //測試時這句代碼一定要寫,意思時:每一個請求都進行刷新
    //否則在測試的適合,kafka生產幾條數據,但ES中卻查不到,默認ES是5000條消息刷新一次,這就涉及到了ES的架構中索引的刷新頻率,下面會寫相應的配置。
    esSinkBuilder.setBulkFlushMaxActions(1)
    //真正將數據發送到ES中
    kafkaDS.addSink(esSinkBuilder.build())
	//觸發執行
    env.execute()
  }
}

我們在上面的代碼中,初步的通過kafka來獲取數據,然後直接寫到了ES中,但模擬的只是執行單個索引請求,我們在日常的生產中,肯定不是說一次請求刷新一次,這對ES來說,壓力太大了,所以會有批量提交的配置。

  • bulk.flush.max.actions:刷新前要緩衝的最大操作數。
  • bulk.flush.max.size.mb:刷新前要緩衝的最大數據大小(以兆字節爲單位)。
  • bulk.flush.interval.ms:刷新間隔,無論緩衝操作的數量或大小如何。

對於ES現在的版本,還支持配置重試臨時請求錯誤的方式:

  • bulk.flush.backoff.enable:如果刷新的一個或多個操作由於臨時原因而失敗,是否對刷新執行延遲退避重試EsRejectedExecutionException。
  • bulk.flush.backoff.type:退避延遲的類型,可以是CONSTANT或EXPONENTIAL
  • bulk.flush.backoff.delay:延遲的延遲量。對於恆定的退避,這只是每次重試之間的延遲。對於指數補償,這是初始基準延遲。
  • bulk.flush.backoff.retries:嘗試嘗試的退避重試次數

Redis Sink

關於Redis大家應該很熟悉了,我們來模擬一下數據處理結束後存入Redis,我這裏模擬的是redis單點。

注意:因爲我們是在IDE中要訪問遠程的redis,所以redis的redis.conf配置文件中,需要修改2個地方:

  1. 註釋 bind 127.0.0.1,否則只能安裝redis的本機連接,其他機器不能訪問
    在這裏插入圖片描述
  2. 關閉保護模式
    protected-mode no
    在這裏插入圖片描述
    然後就可以啓動redis啦,這裏再把幾個簡單的命令貼一下,做到全套服務,哈哈
  • 啓動redis服務:redis-server /usr/local/redis/redis.conf

  • 進入redis:
    進入的命令:redis-cli
    指定IP:redis-cli -h master102
    redis中文顯示問題:redis-cli -h master -raw(也就是多加一個 -raw)
    多個Redis同時啓動,則需指定端口號訪問 redis-cli -p 端口號

  • 關閉redis服務:

    1. 單實例關閉
      如果還未通過客戶端訪問,可直接 redis-cli shutdown
      如果已經進入客戶端,直接 shutdown即可.
    2. 多實例關閉
      指定端口關閉 redis-cli -p 端口號 shutdown
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.datastream.DataStream
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011
import org.apache.flink.streaming.connectors.redis.RedisSink
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig
import org.apache.flink.streaming.connectors.redis.common.mapper.{RedisCommand, RedisCommandDescription, RedisMapper}

/**
  * @description: ${連接單節點redis測試}
  * @author: Liu Jun Jun
  * @create: 2020-06-12 11:23
  **/
object flink2Redis {

  def main(args: Array[String]): Unit = {
    // 轉換
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(2)

    val topic = "test1"
    val properties = new java.util.Properties()
    properties.setProperty("bootstrap.servers", "bigdata101:9092")
    properties.setProperty("group.id", "consumer-group")
    properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("auto.offset.reset", "latest")

    // 從kafka中獲取數據
    val kafkaDS: DataStream[String] =
      env.addSource(
        new FlinkKafkaConsumer011[String](
          topic,
          new SimpleStringSchema(),
          properties) )

    kafkaDS.print("data:")

    val conf = new FlinkJedisPoolConfig.Builder().setHost("bigdata103").setPort(6379).build()

    kafkaDS.addSink(new RedisSink[String](conf, new RedisMapper[String] {
      override def getCommandDescription: RedisCommandDescription = {
        new RedisCommandDescription(RedisCommand.HSET,"sensor")
      }

      override def getKeyFromData(t: String): String = {
        t.split(",")(0)
      }

      override def getValueFromData(t: String): String = {
        t.split(",")(1)
      }
    }))

    env.execute()
  }
}

flink to Hbase

有的時候,還需要把Flink處理過的數據寫到Hbase,那我們也來簡單試試。

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

/**
  * @description: ${flink to Hbase}
  * @author: Liu Jun Jun
  * @create: 2020-05-29 17:53
  **/
object flink2Hbase {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

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

    val stuDS = env
        .socketTextStream("bigdata101",3456)
      .map(s => {
      //這裏我創建了一個student樣例類,只有name和age
        val stu: Array[String] = s.split(",")
        student(stu(0),stu(1).toInt)
      })
   //這裏我們模擬的比較簡單,沒有對數據進行處理,直接寫入到Hbase,這個HBaseSink是自己寫的類,往下看
    val hBaseSink: HBaseSink = new HBaseSink("WordCount","info1")

    stuDS.addSink(hBaseSink)

    env.execute("app")
  }
}

case class student(name : String,age : Int)

/**
  * @description: ${封裝Hbase連接}
  * @author: Liu Jun Jun
  * @create: 2020-06-01 14:41
  **/
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.{RichSinkFunction, SinkFunction}
import org.apache.hadoop.hbase.{HBaseConfiguration, HConstants, TableName}
import org.apache.hadoop.hbase.client._
import org.apache.hadoop.hbase.util.Bytes

class HBaseSink(tableName: String, family: String) extends RichSinkFunction[student] {

  var conn: Connection = _
//創建連接
  override def open(parameters: Configuration): Unit = {
    conn = HbaseFactoryUtil.getConn()
  }

//調用
  override def invoke(value: student): Unit = {
    val t: Table = conn.getTable(TableName.valueOf(tableName))

    val put: Put = new Put(Bytes.toBytes(value.age))
    put.addColumn(Bytes.toBytes(family), Bytes.toBytes("name"), Bytes.toBytes(value.name))
    put.addColumn(Bytes.toBytes(family), Bytes.toBytes("age"), Bytes.toBytes(value.age))
    t.put(put)
    t.close()
  }
  override def close(): Unit = {
  }
}

好了,到這裏flink的一些基本流處理API已經差不多說完了,但是關於flink特別重要的 窗口、精準一次性、狀態編程、時間語義等重點還沒說,所以下一篇關於flink的文章就再聊聊這些關鍵點。

掃碼關注公衆號“後來X大數據”,回覆【電子書】,領取超多本pdf 【java及大數據 電子書】

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章