Flink筆記03——一文了解DataStream

前言

在前面的博客Flink筆記01——入門篇中,我們提到了Flink 常用的API,如下圖所示:
在這裏插入圖片描述
這篇博客,南國主要講述一下Flink的DataStream。

DataStream的編程模型

DataSTream的編程模型包括4個部分:Environment,DataSource,Transformation,SInk。
在這裏插入圖片描述
構建上下文環境Environment 比較簡單,而且之前南國也說過了,主要分爲構建DataStream 實時計算和DataSet做批計算。

DataStream的數據源

基於文件的Source

這裏還可以分爲讀取本地文件系統的數據和基於HDFS中的數據,一般而言我們會把數據源放在HDFS中。

爲了內容的全面,南國這裏簡單寫了個讀取本地文件的demo:

def main(args: Array[String]): Unit = {
    //1.初始化flink 流計算的環境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //2.導入隱式轉換
    import org.apache.flink.streaming.api.scala._
    //3.讀取數據
    val stream = streamEnv.readTextFile("/wordcount.txt")
    //DataStream類同於sparkStreaming中的DStream
    //4.轉換和處理數據
    val result = stream.flatMap(_.split(" "))
      .map((_, 1))
      .keyBy(0) //分組算子 0或者1代表前面DataStream的下標,0代表單詞 1代表出現的次數
      .sum(1) //聚合累加
    //5.打印結果
    result.print("結果")
    //6.啓動流計算程序
    streamEnv.execute("wordcount")
  }

關於讀取HDFS的數據源,首先需要再項目工程文件中加入Hadoop相關的依賴:

	    <!--hadoop相關依賴-->
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>${hadoop.version}</version>
        </dependency>

demo:

def main(args: Array[String]): Unit = {
    //1.初始化flink 流計算的環境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //2.導入隱式轉換
    import org.apache.flink.streaming.api.scala._
    //3.讀取數據
    val stream = streamEnv.readTextFile("hdfs://hadoop101:9000/wc.txt")
    //DataStream類同於sparkStreaming中的DStream
    //4.轉換和處理數據
    val result = stream.flatMap(_.split(" "))
      .map((_, 1))
      .keyBy(0) //分組算子 0或者1代表前面DataStream的下標,0代表單詞 1代表出現的次數
      .sum(1) //聚合累加
    //5.打印結果
    result.print("結果")
    //6.啓動流計算程序
    streamEnv.execute("wordcount")
  }

看完 大家是否發現 基於文件的數據源的代碼幾乎一樣,只是在stream.readTextFile(“path”),更改了path。

基於集合的source

def main(args: Array[String]): Unit = { 
	//初始化Flink的Streaming(流計算)上下文執行環境 
	val streamEnv = StreamExecutionEnvironment.getExecutionEnvironment 
	//導入隱式轉換,
	import org.apache.flink.streaming.api.scala._  //讀取數據 
	var dataStream =streamEnv.fromCollection(Array( 
		new StationLog("001","186","189","busy",1577071519462L,0), 
		new StationLog("002","186","188","busy",1577071520462L,0), 
		new StationLog("003","183","188","busy",1577071521462L,0), 
		new StationLog("004","186","188","success",1577071522462L,32) 
	))
	dataStream.print() 
	streamEnv.execute() 
}

簡單來說,就是在代碼中手動創建集合來作爲DataStream的數據源 進行測試。

基於kafka的數據源

首先添加Kafka的相關依賴:

      <!--kafka相關依賴-->
      <dependency>
          <groupId>org.apache.flink</groupId>
          <artifactId>flink-connector-kafka_2.11</artifactId>
          <version>1.9.1</version>         
      </dependency>

1.讀取kafka中的string類型數據

package com.flink.primary.DataSource

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.FlinkKafkaConsumer
import org.apache.kafka.common.serialization.StringDeserializer

/**
  * 讀取kafka中的普通數據 String
  * flink連接kafka比較 sparkStreaming來說更加簡單(SparkStreaming來凝結kafka有Receiver Direct兩種模式等等)
  * @author xjh 2020.4.5
  */
object kafka_Source_String {
  def main(args: Array[String]): Unit = {
    //1.初始化flink 流計算的環境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //2.導入隱式轉換
    import org.apache.flink.streaming.api.scala._
    //連接kafka
    val properties = new Properties()
    properties.setProperty("bootstrap.servers", "m1:9092,m2:9092,m3:9093")
    properties.setProperty("groupid", "Flink_project")
    properties.setProperty("key.deserializer", classOf[StringDeserializer].getName)
    properties.setProperty("value.deserializer", classOf[StringDeserializer].getName)
    properties.setProperty("auto.offset.reset", "latest")

    //設置kafka數據源,這裏kafka中的數據是String
    val stream = streamEnv.addSource(new FlinkKafkaConsumer[String]("t_test", new SimpleStringSchema(), properties))
    stream.print()
    streamEnv.execute()
  }
}

2.讀取kafka中的鍵值對數據

package com.flink.primary.DataSource

import java.util.Properties

import org.apache.flink.api.common.typeinfo.TypeInformation
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka.{FlinkKafkaConsumer, KafkaDeserializationSchema}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.flink.streaming.api.scala._
/**
  * 讀取kafka中的key/value數據
  * @author xjh 2020.4.6
  */
object kafka_Source_keyValue {
  def main(args: Array[String]): Unit = {
    val environment = StreamExecutionEnvironment.getExecutionEnvironment

    //連接kafka
    val properties = new Properties()
    properties.setProperty("bootstrap.servers", "m1:9092,m2:9092,m3:9093")
    properties.setProperty("groupid", "Flink_project")
    properties.setProperty("key.deserializer", classOf[StringDeserializer].getName)
    properties.setProperty("value.deserializer", classOf[StringDeserializer].getName)
    properties.setProperty("auto.offset.reset", "latest")

    val stream = environment.addSource(new FlinkKafkaConsumer[(String, String)](
      "t_topic", new MyKafkaReader, properties
    ))
    stream.print()
    environment.execute("Kafka_keyValue")
  }
}
package com.flink.primary.DataSource

import org.apache.flink.api.common.typeinfo.TypeInformation
import org.apache.flink.streaming.api.scala.{createTuple2TypeInformation, createTypeInformation}
import org.apache.flink.streaming.connectors.kafka.KafkaDeserializationSchema
import org.apache.kafka.clients.consumer.ConsumerRecord

/**
  * 自定義一個用於讀取kafka中key value數據的flink程序輸入源
  * @author xjh 2020.4.6
  */
class MyKafkaReader extends KafkaDeserializationSchema[(String, String)] {
  //流是否結束
  override def isEndOfStream(t: (String, String)): Boolean = false

  //反序列化
  override def deserialize(consumerRecord: ConsumerRecord[Array[Byte], Array[Byte]]): (String, String) = {
    if (consumerRecord != null) {
      var key = ""
      var value = ""
      if (consumerRecord.key() != null) {
        key = new String(consumerRecord.key(), "utf-8")
      }
      if (consumerRecord.value() != null) {
        value = new String(consumerRecord.value(), "utf-8")
      }
      (key, value)
    } else {
      // 如果kafka中的數據爲空,擇返回一個固定的二元組
      ("", "")
    }
  }

  //指定類型
  override def getProducedType: TypeInformation[(String, String)] = {
    createTuple2TypeInformation(createTypeInformation[String], createTypeInformation[String])
    //指定Key value都是String
  }
}

自定義數據源

除非上述提到的集種數據源,開發者還可以自定義數據源,有兩種方式實現:

  • 通過實現 SourceFunction 接口來自定義無並行度(也就是並行度只能爲 1)的 Source。
  • 通過實現 ParallelSourceFunction 接口或者繼承 RichParallelSourceFunction 來自 定義有並行度的數據源。

這裏,我們寫了一個實現SourceFunction接口的demo。

package com.flink.primary.DataSource

import java.util.Random

import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala._

/**
  * 用戶自定義數據源
  * 自定義的source。需求是每隔兩秒鐘,生成10條隨機基站通話日誌數據
  * @author xjh 2020.4.6
  */
class MyCustomerSource extends SourceFunction[StationLog] {
  var flag = true

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

  /**
    * 啓動一個Source,並從中返回數據
    * @param sourceContext
    */
  override def run(sourceContext: SourceFunction.SourceContext[StationLog]): Unit = {
    val random = new Random()
    var types = Array("fail", "busy", "barring", "success")
    while (flag) {
      1.to(10).map(i => {
        var callOut = "1320000%04d".format(random.nextInt(10000)) //主叫號碼
        var callIn = "1860000%04d".format(random.nextInt(10000)) //主叫號碼
        //生成一條數據
        new StationLog(sid = "station_" + random.nextInt(), callOut, callIn, types(random.nextInt(4)), System.currentTimeMillis(), random.nextInt(10))
      })
        .foreach(sourceContext.collect(_)) //發送數據到流
      Thread.sleep(2000)
    }
  }
  //終止數據流 
  override def cancel(): Unit = {
  	flag=false
  }
}

object CustomerSource {
  def main(args: Array[String]): Unit = {
    val environment = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val stream = environment.addSource(new MyCustomerSource)
    stream.print()
    environment.execute()
  }
}

DataStream的轉換算子

轉換算子transformation表示通過一個或者多個DataStream生成新的DataStream的過程,理解上其實和RDD中的轉換操作是一樣的。
DataStream在轉換過程中,每種操作類型被定義爲不同的 Operator,Flink 程序能夠將多個 Transformation 組成一個 DataFlow 的拓撲。

map和flatMap算子

這兩個算子的功能作用和Spark RDD中是一樣的。
對於map,源碼中如下所示:

/**
   * Creates a new DataStream by applying the given function to every element of this DataStream.
   */
  def map[R: TypeInformation](fun: T => R): DataStream[R] = {
    if (fun == null) {
      throw new NullPointerException("Map function must not be null.")
    }
    val cleanFun = clean(fun)
    val mapper = new MapFunction[T, R] {
      def map(in: T): R = cleanFun(in)
    }
    map(mapper)
  }

調 用 用 戶 定 義 的 MapFunction 對 DataStream[T] 數 據 進 行 處 理 , 形 成 新 的 DataStream[R],其中數據格式可能會發生變化,常用作對數據集內數據的清洗和轉換。

對於flatMap,源碼如下:

/**
   * Creates a new DataStream by applying the given function to every element and flattening
   * the results.
   */
  def flatMap[R: TypeInformation](fun: T => TraversableOnce[R]): DataStream[R] = {
    if (fun == null) {
      throw new NullPointerException("FlatMap function must not be null.")
    }
    val cleanFun = clean(fun)
    val flatMapper = new FlatMapFunction[T, R] {
      def flatMap(in: T, out: Collector[R]) { cleanFun(in) foreach out.collect }
    }
    flatMap(flatMapper)
  }

該算子主要應用處理輸入一個元素產生一個或者多個元素的計算場景,比較常見的是在 經典例子 WordCount 中,將每一行的文本數據切割,生成單詞序列如在圖所示,對於輸入 DataStream[String]通過 FlatMap 函數進行處理,字符串數字按逗號切割,然後形成新的整 數數據集。

需要注意的是,這裏提到的map和flatMap的源碼函數只是一些重寫函數中的一個。也就是說相同函數名 根據他參數的不同 有多個函數。推薦大家再看源碼的時候,結合自己平時經常使用的那個來進行分析。

filter算子

該算子將按照條件對輸入數據集進行篩選操作,將符合條件的數據集輸出,將不符合條 件的數據過濾掉。例如:val filter= dataStream.filter { _ % 2 == 0 }

源碼如下:

/**
   * Creates a new DataStream that contains only the elements satisfying the given filter predicate.
   */
  def filter(fun: T => Boolean): DataStream[T] = {
    if (fun == null) {
      throw new NullPointerException("Filter function must not be null.")
    }
    val cleanFun = clean(fun)
    val filterFun = new FilterFunction[T] {
      def filter(in: T) = cleanFun(in)
    }
    filter(filterFun)
  }

keyBy算子

源碼如下:

/**
   * Groups the elements of a DataStream by the given key positions (for tuple/array types) to
   * be used with grouped operators like grouped reduce or grouped aggregations.
   */
  def keyBy(fields: Int*): KeyedStream[T, JavaTuple] = asScalaStream(stream.keyBy(fields: _*))

該算子根據指定的 Key 將輸入的 DataStream[T]數據格式轉換爲 KeyedStream[T],也就 是在數據集中執行 Partition 操作,將相同的 Key 值的數據放置在相同的分區中。
該算子的功能和RDD中的groupByKey很類似。

舉了例子如下:

val dataStream = env.fromElements((1, 5),(2, 2),(2, 4),(1, 3)) 
//指定第一個字段爲分區Key 
val keyedStream: KeyedStream[(String,Int), Tuple] = dataStream.keyBy(0)

Reduce算子

我們首先找一個reduce的源碼來看:

/**
   * Creates a new [[DataStream]] by reducing the elements of this DataStream
   * using an associative reduce function. An independent aggregate is kept per key.
   */
  def reduce(fun: (T, T) => T): DataStream[T] = {
    if (fun == null) {
      throw new NullPointerException("Reduce function must not be null.")
    }
    val cleanFun = clean(fun)
    val reducer = new ReduceFunction[T] {
      def reduce(v1: T, v2: T) : T = { cleanFun(v1, v2) }
    }
    reduce(reducer)
  }

該算子和MapReduce Spark中的原理基本一致,主要目的是將輸入的 DataStream 通過 關聯的Reduce Function 進 行數據聚合處理,其中定義的 ReduceFunciton 必須滿足運算結合律和交換律。通過源碼可以看到reduce算子的特點是DataStream的類型不變,得到的返回值類型也是 DataStream[T]。它可用於實時聚合。

下面的樣例對傳入DataStream中相同的key值的數據獨立進行求和運算,得到每個 key 所對應的求和值。

	val dataStream =streamEnv.fromElements(("a", 3), ("d", 4), ("c", 2), ("c",5), ("a", 5)) //指定第一個字段爲分區Key
    val keyedStream= dataStream.keyBy(0) //滾動對第二個字段進行reduce相加求和
    val reduceStream = keyedStream.reduce { (t1, t2) => (t1._1, t1._2 + t2._2) }.print("reduce result:")

Aggregation算子

Aggregations 是 KeyedDataStream 接口提供的聚合算子,根據指定的字段進行聚合操 作,滾動地產生一系列數據聚合結果。其實是將 Reduce 算子中的函數進行了封裝,封裝的聚合操作有sum,min,max 等,這樣就不需要用戶自己定義 Reduce 函數。

如下代碼所示,指定數據集中第一個字段作爲 key,用第二個字段作爲累加字段,然後滾動 地對第二個字段的數值進行累加並輸出。

val KeyedStream=dataStream.keyBy(0)
val sumRes=KeyedStream.sum(1)
sumRes.print()

Union算子

Union 算子主要是將兩個或者多個輸入的數據集合併成一個數據集,需要保證兩個數據 集的格式一致,輸出的數據集的格式和輸入的數據集格式保持一致。

//創建不同的數據集 
val dataStream1: DataStream[(String, Int)] = env.fromElements(("a", 3), ("d", 4), ("c", 2), ("c", 5), ("a", 5)) val dataStream2: DataStream[(String, Int)] = env.fromElements(("d", 1), ("s", 2), ("a", 4), ("e", 5), ("a", 6)) val dataStream3: DataStream[(String, Int)] = env.fromElements(("a", 2), ("d", 1), ("s", 2), ("c", 3), ("b", 1)) //合併兩個DataStream數據集 
val unionStream = dataStream1.union(dataStream_02) 
//合併多個DataStream數據集 
val allUnionStream = dataStream1.union(dataStream2, dataStream3)

Cnnect CoMap CoFlatMap

Connect 算子主要是爲了合併兩種或者多種不同數據類型的數據集,合併後會保留原來 數據集的數據類型。例如:dataStream1 數據集爲(String, Int)元祖類型,dataStream2 數據集爲 Int 類型,通過 connect 連接算子將兩個不同數據類型的流結合在一起,形成格式 爲 ConnectedStreams 的數據集,其內部數據爲[(String, Int), Int]的混合數據類型,保 留了兩個原始數據集的數據類型。

//創建不同數據類型的數據集 
val dataStream1: DataStream[(String, Int)] = env.fromElements(("a", 3), ("d", 4), ("c", 2), ("c", 5), ("a", 5)) val dataStream2: DataStream[Int] = env.fromElements(1, 2, 4, 5, 6) 
//連接兩個DataStream數據集 
val connectedStream: ConnectedStreams[(String, Int), Int] = dataStream1.connect(dataStream2)

需要注意的是,對於ConnectedStreams 類型的數據集不能直接進行類似 Print()的操 作,需要再轉換成 DataStream 類型數據集,在 Flink中ConnectedStreams 提供的 map()方法和flatMap()

object ConnectTransformation{ 
	def main(args: Array[String]): Unit = { 
		//初始化Flink的Streaming(流計算)上下文執行環境 
		val streamEnv = StreamExecutionEnvironment.getExecutionEnvironment 
		//導入隱式轉換
		import org.apache.flink.streaming.api.scala._ 
		//創建不同數據類型的數據集 
		val dataStream1: DataStream[(String, Int)] = streamEnv.fromElements(("a", 3), ("d", 4), ("c", 2), ("c", 5), ("a", 5)) 
		val dataStream2: DataStream[Int] = streamEnv.fromElements(1, 2, 4, 5, 6) 
		//連接兩個DataStream數據集 
		val connectedStream: ConnectedStreams[(String, Int), Int] = dataStream1.connect(dataStream2) //coMap函數處理 
		val result: DataStream[(Any, Int)] = connectedStream.map( 
			//第一個處理函數 
			t1 => { 
			(t1._1, t1._2)
			 },
			 //第二個處理函數 
			 t2 => { 
			 (t2, 0) 
			 } 
		 )
		 result.print() 
		 streamEnv.execute() 
	 } 
 }

注意Union和Cnnect的區別:

  • Union 之前兩個流的類型必須是一樣,Connect 可以不一樣,在之後的 coMap 中再去調 整成爲一樣的。
  • Connect 只能操作兩個流,Union 可以操作多個。

Split 和 select

Split 算子是將一個 DataStream 數據集按照條件進行拆分,形成兩個數據集的過程, 也是 union 算子的逆向實現。每個接入的數據都會被路由到一個或者多個輸出數據集中。

在使用 split 函數中,需要定義 split 函數中的切分邏輯,通過調用 split 函數,然後指定條件判斷函數。
demo:將根據第二個字段的奇偶性將數據集標記出來,如 果是偶數則標記爲 even,如果是奇數則標記爲 odd,然後通過集合將標記返回,最終生成格 式 SplitStream 的數據集。

//創建數據集 
val dataStream1: DataStream[(String, Int)] = env.fromElements(("a", 3), ("d", 4), ("c", 2), ("c", 5), ("a", 5)) //合併兩個DataStream數據集 
val splitedStream: SplitStream[(String, Int)] = dataStream1.split(t => if (t._2 % 2 == 0) Seq("even") else Seq("odd"))

split 函數本身只是對輸入數據集進行標記,並沒有將數據集真正的實現切分,因此需 要藉助 Select 函數根據標記將數據切分成不同的數據集。如下代碼所示,通過調用 SplitStream 數據集的 select()方法,傳入前面已經標記好的標籤信息,然後將符合條件的 數據篩選出來,形成新的數據集。

//篩選出偶數數據集 
val evenStream: DataStream[(String, Int)] = splitedStream.select("even") 
//篩選出奇數數據集 
val oddStream: DataStream[(String, Int)] = splitedStream.select("odd") 
//篩選出奇數和偶數數據集 
val allStream: DataStream[(String, Int)] = splitedStream.select("even", "odd")

函數類和富函數類

上一小節中提到的算子幾乎都可以自定義爲一個函數類、富函數類。下面是關於二者的簡單對比:

函數接口 富函數接口
MapFunction RichMapFunction
FlatMapFunction RichFlatMapFunction
ReduceFunction RichFilterFunction

富函數接口它其他常規函數接口的不同在於:可以獲取運行環境的上下文,在上下文環境中可以管理狀態,並擁有一些生命週期方法,所以可以實現更復雜的功能

1.普通函數demo:按照指定的時間格式輸出每個通話的撥號時間和結束時間

數據如下所示:
在這裏插入圖片描述

//按照指定的時間格式輸出每個通話的撥號時間和結束時間 
object FunctionClassTransformation { 
	def main(args: Array[String]): Unit = { 
		//初始化Flink的Streaming(流計算)上下文執行環境 
		val streamEnv: StreamExecutionEnvironment =StreamExecutionEnvironment.getExecutionEnvironment 
		//導入隱式轉換
		import org.apache.flink.streaming.api.scala._ //讀取文件數據 
		val data =streamEnv.readTextFile(getClass.getResource("/station.log").getPath) 
		.map(line=>{
		var arr =line.split(",") 
		new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.to Long) }) 
		//定義時間輸出格式 
		val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 
		//過濾那些通話成功的 
		data.filter(_.callType.equals("success")) 
			.map(new CallMapFunction(format)) 
			.print() 
		streamEnv.execute() 
	}

	//自定義的函數類 
	class CallMapFunction(format: SimpleDateFormat) extends MapFunction[StationLog,String]{ 	 	  
	   override def map(t: StationLog): String = { 
		 var strartTime=t.callTime; var endTime =t.callTime + t.duration*1000 "主叫號碼:"+t.callOut +",被叫號碼:"+t.callIn+",呼叫起始時 間:"+format.format(new Date(strartTime))+",呼叫結束時間:"+format.format(new Date(endTime)) 
		 } 
		} 
	}

2.富函數類demo:把呼叫成功的通話信息轉化成真實的用戶姓名,通話用戶對應的用戶表 (在 Mysql 數據中)爲:
在這裏插入圖片描述
由於需要從數據庫中查詢數據,就需要創建連接,創建連接的代碼必須寫在生命週期的 open 方法中。所以需要使用富函數類。
Rich Function 有一個生命週期的概念。典型的生命週期方法有

  • open()方法是 rich function 的初始化方法,當一個算子例如 map 或者 filter 被調用 之前 open()會被調用。
  • close()方法是生命週期中的最後一個調用的方法,做一些清理工作。
  • getRuntimeContext()方法提供了函數的 RuntimeContext 的一些信息,例如函數執行的 並行度,任務的名字,以及 state 狀態
//轉換電話號碼的真實姓名 
object RichFunctionClassTransformation { 
	def main(args: Array[String]): Unit = { 
		//初始化Flink的Streaming(流計算)上下文執行環境 
		val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment 
		//導入隱式轉換 
		import org.apache.flink.streaming.api.scala._ 
		//讀取文件數據 
		val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath) 
			.map(line=>{ 
				var arr =line.split(",") 
				new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.to Long) 
				})
			//過濾那些通話成功的 
			data.filter(_.callType.equals("success")) 
				.map(new CallRichMapFunction()) 
				.print() 
			streamEnv.execute() 
		}

   //自定義的富函數類 
   class CallRichMapFunction() extends RichMapFunction[StationLog,StationLog]{ 
	   var conn:Connection =_ var pst :PreparedStatement =_ 
	   //生命週期管理,初始化的時候創建數據連接 
	   override def open(parameters: Configuration): Unit = { 
	   	conn=DriverManager.getConnection("jdbc:mysql://localhost/test","root","123123") 		
	   	pst=conn.prepareStatement("select name from t_phone where phone_number=?") 
       }
   
	   override def map(in: StationLog): StationLog = { 
		   //查詢主叫用戶的名字 
		   pst.setString(1,in.callOut) val set1: ResultSet = pst.executeQuery() if(set1.next()){ in.callOut=set1.getString(1) }
		   //查詢被叫用戶的名字 
		   pst.setString(1,in.callIn) 
		   val set2: ResultSet = pst.executeQuery() 
		   if(set2.next()){ 
		   in.callIn=set2.getString(1) 
		   }
		  in 
	   }
		//關閉連接 
		override def close(): Unit = { 
			pst.close()
			conn.close() 
			} 
		} 
  }

數據輸出Sink

Flink 針對 DataStream 提供了大量的已經實現的數據目標(Sink),包括文件、Kafka、 Redis、HDFS、Elasticsearch 等等。

基於HDFS的Sink

首先配置支持Hadoop FileSystem的依賴

<dependency> 
	<groupId>org.apache.flink</groupId>
	<artifactId>flink-connector-filesystem_2.11</artifactId> 
	<version>1.9.1</version> 
</dependency>

Streaming File Sink 能把數據寫入 HDFS 中,還可以支持分桶寫入,每一個分桶就對應HDFS中的一個目錄。默認按照小時來分桶,在一個桶內部,會進一步將輸出基於滾動策略切分成更小的文件。這有助於防止桶文件變得過大。滾動策略也是可以配置的,默認 策 略會根據文件大小和超時時間來滾動文件,超時時間是指沒有新數據寫入部分文件(part file)的時間。

我們來看下面這個demo

package com.flink.primary.Sink

import com.flink.primary.DataSource.{MyCustomerSource, StationLog}
import com.typesafe.sslconfig.ssl.ClientAuth.Default
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.functions.sink.filesystem.rollingpolicies.DefaultRollingPolicy
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
/**
  * 將自定義的Source作爲數據源,把基站入職數據寫入HDFS並且每兩秒鐘生成一個文件
  * @author xjh 2020.4.6
  */
object HDFS_Sink {
  def main(args: Array[String]): Unit = {
    val environment = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    //讀取數據
    val stream = environment.addSource(new MyCustomerSource)
    //分桶:默認一個小時一個目錄
    //設置一個滾動策略
    val builder: DefaultRollingPolicy[StationLog, String] = DefaultRollingPolicy.create()
      .withInactivityInterval(2000) //不活動的分桶事件
      .withRolloverInterval(2000) //每個兩秒鐘生成一個文件
      .build() //創建

    //創建HDFS的Sink
    val hdfsSink = StreamingFileSink.forRowFormat[StationLog](
      new Path("hdfs://m1:9000/textSink001/"),
      new SimpleStringEncoder[StationLog]("utf-8"))
      .withBucketCheckInterval(2000) //檢查間隔時間
      .build()

    stream.addSink(hdfsSink)
    environment.execute()
  }
}

基於Redis的Sink

首先配置Redis的依賴:

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

將單詞技術的結果寫入鍵值數據庫Redis中:

def main(args: Array[String]): Unit = { 
	//初始化Flink的Streaming(流計算)上下文執行環境 
	val streamEnv= StreamExecutionEnvironment.getExecutionEnvironment 
	streamEnv.setParallelism(1) 
	//導入隱式轉換 
	import org.apache.flink.streaming.api.scala._ 	
	//讀取數據 
	val stream = streamEnv.socketTextStream("hadoop101",8888) 
	//轉換計算 
	val result = stream.flatMap(_.split(",")) .map((_, 1)) .keyBy(0) .sum(1)
	//連接redis的配置 
	val config = new FlinkJedisPoolConfig.Builder()
		.setDatabase(1).setHost("hadoop101").setPort(6379).build() 
	//寫入redis 
	result.addSink(new RedisSink[(String, Int)](config,new RedisMapper[(String, Int)] { 
		override def getCommandDescription = new RedisCommandDescription(RedisCommand.HSET,"t_wc") 
		override def getKeyFromData(data: (String, Int)) = { data._1 //單詞 
	  }
	override def getValueFromData(data: (String, Int)) = { 
	data._2+"" //單詞出現的次數 
	} 
	})) 
	streamEnv.execute() 
}

基於kafka的sink

demo1:

package com.flink.primary.Sink

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

/**
  * 把netcat數據源中每個單詞(String)寫入kafka中
  * @author xjh 2020.4.6
  */
object kafka_Sink_String {
  def main(args: Array[String]): Unit = {
    val environment = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val stream = environment.socketTextStream("m1", 8888)
    val wc= stream.flatMap(_.split(" "))
    wc.addSink(new FlinkKafkaProducer[String]("m1:9092,m2:9092,m3:9092","t_2020",new SimpleStringSchema()))
    environment.execute("kafka_sink")
  }
}

demo2:

package com.flink.primary.Sink

import java.lang
import java.util.Properties

import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka.{FlinkKafkaProducer, KafkaSerializationSchema}
import org.apache.kafka.clients.producer.ProducerRecord
/**
  * 把netcat作爲輸入源,將每個單詞的統計結果(key/value)寫入kafka中
  * @author xjh 2020.4.6
  */
object kafka_sink_keyvalue {
  def main(args: Array[String]): Unit = {
    val environment = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val stream = environment.socketTextStream("m1", 8888)
    val result= stream.flatMap(_.split(" ")).map((_,1)).keyBy(0).sum(1)

    //創建連接kafka的屬性
    val properties = new Properties()
    properties.setProperty("bootstrap.servers","m1:9092,m2:9092,m3:9092")
    //創建一個sink
    val sink = new FlinkKafkaProducer[(String, Int)](
      "t_topic",
      new KafkaSerializationSchema[(String, Int)] { //自定義的匿名內部類
        override def serialize(t: (String, Int), aLong: lang.Long): ProducerRecord[Array[Byte], Array[Byte]] = {
          new ProducerRecord("t_topic", t._1.getBytes, (t._2 + "").getBytes())
        }
      },
      properties,
      FlinkKafkaProducer.Semantic.EXACTLY_ONCE //精確一次
    )
    result.addSink(sink)
    environment.execute("kafka_keyValue")
  }
}

自定義的sink

定義 Sink有兩種實現方式:1、實現 SinkFunction 接口。2、實現 RichSinkFunction 類。
後者增加了生命週期的管理功能。比如需要在 Sink 初始化的時候創 建連接對象,則最好使用第二種。

案例需求:把 StationLog 對象寫入 Mysql 數據庫中。
首先還是要添加相應的依賴:

       <!--配置mysql連接器-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>

demo:

package com.flink.primary.Sink

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

import com.flink.primary.DataSource.{MyCustomerSource, StationLog}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.{RichSinkFunction, SinkFunction}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
/**
  * 隨機生成stationLog對象,寫入mysql數據庫中
  * @author xjh 2020.4.6
  */
object CustomerJDBCSink {
  def main(args: Array[String]): Unit = {
    val environment = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val stream = environment.addSource(new MyCustomerSource)
    //數據寫入mysql 需要創建一個自定義的sink
    stream.addSink(new MyCustomerJDBCSink)
    environment.execute("JDBC_sink")
  }
}

/**
  * 自定義一個Sink寫入MySQL
  */
class MyCustomerJDBCSink extends RichSinkFunction[StationLog]{
  var conn:Connection =_
  var pst :PreparedStatement =_
  //生命週期管理,在Sink初始化的時候調用
  override def open(parameters: Configuration): Unit = {
    conn=DriverManager.getConnection("jdbc:mysql://localhost:3305/xjh","root","123456")
    pst=conn.prepareStatement("insert into t_station_log " +
      "(sid,call_out,call_in,call_type,call_time,duration) values (?,?,?,?,?,?)") //這裏的?是佔位符,invoke函數用於給他賦值
  }
  //把StationLog 寫入到表t_station_log
  override def invoke(value: StationLog, context: SinkFunction.Context[_]): Unit = {
    pst.setString(1,value.sid)
    pst.setString(2,value.callOut)
    pst.setString(3,value.callIn)
    pst.setString(4,value.callType)
    pst.setLong(5,value.callTime)
    pst.setLong(6,value.duration)
    pst.executeUpdate()
  }
  override def close(): Unit = {
    pst.close()
    conn.close()
  }
}

參考資料:
1.尚學堂大數據技術Flink教案

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