Flink DataStream API(一)

目錄

 

一、基本介紹

二、DataStream API

三、數據源(Source)

(1)基於集合的預定義Source

(2)基於Socket的預定義Source

(3)基於文件的預定義Source

(4)自定義Source

四、數據轉換(Transformation)

1、基礎轉換操作

(1)Map轉換

(2)鍵值對操作

(3)stream合併操作

五、分區

1、自定義分區(partitionCustom)

2、隨機分區(shuffle)

3、rebalance

4、broadcast

5、rescale

六、結果數據接收器(Data sink)

1、寫入文本文件

2、寫入CSV文件

4、寫入Socket

5、指定輸出格式

6、結果打印

7、自定義輸出器


一、基本介紹

Flink提供了三層API,每層在簡潔性和表達性之間進行了不同的權衡。

 

ProcessFunction是Flink提供的最具表現力的功能接口,它提供了對時間和狀態的細粒度控制能夠任意修改狀態。所以ProcessFunction能夠爲許多有事件驅動的應用程序實現複雜的事件處理邏輯。

DataStream API爲許多通用的流處理操作提供原語,比如window。DataStream API適用於Java和Scala,它基於函數實現,比如map()、reduce()等。我們也可以自己擴展接口自定義函數。

SQL & Table API 這兩個都是關係API,是批處理和流處理統一的API。Table API和SQL利用Apache Calcite進行解析、驗證和查詢優化。它們可以與DataStream和DataSet API無縫集成,並支持用戶定義標量、聚合和表值函數。關係API(relational api)目標在於簡化數據分析、數據流水線(data pipelining)和ETL。

我們一般主要使用DataStream進行數據處理,下面介紹的API也是DataStream相關的API。

二、DataStream API

DataStream是Flink編寫流處理作業的API。我們前面說過一個完整的Flink處理程序應該包含三部分:數據源(Source)、轉換操作(Transformation)、結果接收(Sink)。下面我們從這三部分來看DataStream API。

三、數據源(Source)

Flink應用程序從數據源獲取要處理的數據,DataStream通過StreamExecutionEnvironment.addResource(SourceFunction) 來添加數據源。爲了方便使用,Flink預提幾類預定義的數據源,比如讀取文件的Source、通過Sockt讀取的Source、從內存中獲取的Source等。

(1)基於集合的預定義Source

基於集合的數據源一般是指從內存集合中直接讀取要處理的數據,StreamExecutionEnvironment提供了4類預定義方法。

1)、fromCollection

fromCollection是從給定的集合中創建DataStream,StreamExecutionEnvironment提供了4種重載方法:

  • fromCollection(Collection<T> data):通過給定的集合創建DataStream。返回數據類型爲集合元素類型。

  • fromCollection(Collection<T> data,TypeInformation<T> typeInfo):通過給定的非空集合創建DataStream。返回數據類型爲typeInfo。

  • fromCollection(Iterator<T> data,Class<T> type):通過給定的迭代器創建DataStream。返回數據類型爲type。

  • fromCollection(Iterator<T> data,TypeInformation<T> typeInfo):通過給定的迭代器創建DataStream。返回數據類型爲typeInfo。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val list = List(1,2,3,4)
val stream = env.fromCollection(list)
stream.print()
env.execute("FirstJob")

2)、fromParallelCollection

fromParallelCollection和fromCollection類似,但是是並行的從迭代器中創建DataStream。

  • fromParallelCollection(SplittableIterator<T> data,Class<T> type)

  • fromParallelCollection(SplittableIterator<T>,TypeInfomation typeInfo)

和Iterable中Spliterator類似,這是JDK1.8新增的特性,並行讀取集合元素。

3)、fromElements

從一個給定的對象序列中創建一個數據流,所有的對象必須是相同類型的

fromElements從給定的對象序列中創建DataStream,StreamExecutionEnvironment提供了2種重載方法:

  • fromElements(T... data):從給定對象序列中創建DataStream,返回的數據類型爲該對象類型自身。

  • fromElements(Class<T> type,T... data):從給定對象序列中創建DataStream,返回的數據類型type。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val list = List(1,2,3,4)
val stream = env.fromElement(list)
stream.print()
env.execute("FirstJob")

4)、generateSequence

generateSequence(long from,long to)從給定間隔的數字序列中創建DataStream,比如from爲1,to爲10,則會生成1~10的序列。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.generateSequence(1,10)
stream.print()
env.execute("FirstJob")

(2)基於Socket的預定義Source

我們還可以通過Socket來讀取數據,通過Socket創建的DataStream能夠從Socket中無限接收字符串,字符編碼採用系統默認字符集。當Socket關閉時,Source停止讀取。Socket提供了5個重載方法,但是有兩個方法已經標記廢棄。

  • socketTextStream(String hostname,int port):指定Socket主機和端口,默認數據分隔符爲換行符(\n)。

  • socketTextStream(String hostname,int port,String delimiter):指定Socket主機和端口,數據分隔符爲delimiter。

  • socketTextStream(String hostname,int port,String delimiter,long maxRetry):該重載方法能夠當與Socket斷開時進行重連,重連次數由maxRetry決定,時間間隔爲1秒。如果爲0則表示立即終止不重連,如果爲負數則表示一直重試。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.socketTextStream("localhost", 11111)
stream.print()
env.execute("FirstJob")

(3)基於文件的預定義Source

基於文件創建DataStream主要有兩種方式:readTextFile和readFile。(readFileStream已廢棄)。readTextFile就是簡單讀取文件,而readFile的使用方式比較靈活。

1)readTextFile

readTextFile提供了兩個重載方法:

  • readTextFile(String filePath):逐行讀取指定文件來創建DataStream,使用系統默認字符編碼讀取。

  • readTextFile(String filePath,String charsetName):逐行讀取文件來創建DataStream,使用charsetName編碼讀取。

//  獲取運行環境
    val env :StreamExecutionEnvironment =StreamExecutionEnvironment.getExecutionEnvironment
    val path ="text01.txt"
//    val stream =env.readTextFile(path)

// text01.txt內容
hello world scala
hello world hadoop
hello world yarn

2)readFile

按照指定的文件格式讀取文件。

readFile通過指定的FileInputFormat來讀取用戶指定路徑的文件。

對於指定路徑文件,我們可以使用不同的處理模式來處理:

  • FileProcessingMode.PROCESS_ONCE 模式只會處理文件數據一次.

  • FileProcessingMode.PROCESS_CONTINUOUSLY監控數據源文件是否有新數據,如果有新數據則會繼續處理

readFile(FileInputFormat<T> inputFormat,String filePath,
        FileProcessingMode watchType,long interval,TypeInformation typrInfo) 
參數 說明 實例
inputFormat 創建DataStream指定的輸入格式
filePath 讀取的文件路徑,爲URI格式。既可以讀取普通文件,可以讀取HDFS文件 file:///some/local/file 或hdfs://host:port/file/path
watchType 文件數據處理方式 FileProcessingMode.PROCESS_ONCE或FileProcessingMode.PROCESS_CONTINUOUSLY
interval 在週期性監控Source的模式下(PROCESS_CONTINUOUSLY),指定每次掃描的時間間隔 10

readFile提供了幾個便於使用的重載方法,但它們最終都是調用上面這個方法的。

  • readFile(FileInputFormat<T> inputFormat,String filePath):處理方式默認使用FileProcessingMode.PROCESS_ONCE。

  • readFile(FileInputFormat<T> inputFormat,String filePath,FileProcessingMode watchType,long interval):返回類型默認爲inputFormat類型。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val path = new Path("/opt/modules/test.txt")
val stream = env.readFile(new TextInputFormat(path), "/opt/modules/test.txt")
stream.print()
env.execute("FirstJob")

需要注意:在使用 FileProcessingMode.PROCESS_CONTINUOUSLY 時,當修改讀取文件時,Flink會將文件整體內容重新處理,也就是打破了"exactly-once"。

(4)自定義Source

除了預定義的Source外,我們還可以通過實現 SourceFunction 來自定義Source,然後通過StreamExecutionEnvironment.addSource(sourceFunction)添加進來。比如讀取Kafka數據的Source:

我們可以實現以下三個接口來自定義Source:

  • SourceFunction:創建非並行數據源。

  • ParallelSourceFunction:創建並行數據源。

  • RichParallelSourceFunction:創建並行數據源。

1)MySql source

import java.sql.{Connection, DriverManager, PreparedStatement, ResultSet}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.streaming.api.functions.source.{RichSourceFunction, SourceFunction}

object FlinkSelfDataSource {
  def main(args: Array[String]): Unit = {
    val  env = StreamExecutionEnvironment.getExecutionEnvironment
    env.addSource(new SourceFromMySql).print()
    env.execute("Flink add self data source")
  }
}

class  SourceFromMySql extends RichSourceFunction[Student]{
  var ps:PreparedStatement = _
  var connection:Connection = _
  /**
    * open() 方法中建立連接,這樣不用每次 invoke 的時候都要建立連接和釋放連接。
    * @param parameters
    * @throws Exception
    */
  override def open(parameters: Configuration): Unit = {
    super.open(parameters)
    connection = this.getConnection()
    val sql = "select * from student;"
    ps = this.connection.prepareStatement(sql)
  }

  def getConnection():Connection= {
    var con :Connection = null
    try {
      Class.forName("com.mysql.jdbc.Driver")
      con = DriverManager.getConnection("jdbc:mysql://ip:port                                                                        
        /test_sun?useUnicode=true&characterEncoding=UTF-8", "xxxx", "xxxx")
    } catch  {
      case  e:Exception =>System.out.println("-----------
              mysql get connection has exception , msg = "+ e.getMessage)
    }
    con
  }

  override def cancel() = {}
  /**
    * DataStream 調用一次 run() 方法用來獲取數據
    * @param sourceContext
    */
  override def run(sourceContext: SourceFunction.SourceContext[Student]) = {
    val  resultSet:ResultSet = ps.executeQuery();
    while (resultSet.next()) {
      val  student = new Student
      student.id = resultSet.getInt("id")
      student.name = resultSet.getString("name").trim()
      student.password = resultSet.getString("password").trim()
      student.age = resultSet.getInt("age")
      sourceContext.collect(student)
    }
  }
}
class Student {
  var id = 0
  var name: String = null
  var password: String = null
  var age = 0
  override def toString = s"Student($id, $name, $password, $age)"
}

2)Kafka source

import java.util.Properties
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer08
import org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartition

object FlinkKafkaTest {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val props = new Properties()
    props.put("bootstrap.servers", "Kafka-01:9092")
    props.put("zookeeper.connect", "localhost:2181")
    props.put("group.id", "flink_test_sxp")
//key 反序列化
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer") 
//value 反序列化
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    props.put("auto.offset.reset", "latest") 

    val myConsumer = new FlinkKafkaConsumer08[String]("topic", new SimpleStringSchema(),props)
    myConsumer.setStartFromEarliest()      // start from the earliest record possible
    myConsumer.setStartFromLatest()        // start from the latest record
    val specificStartOffsets = new java.util.HashMap[KafkaTopicPartition, java.lang.Long]()
    specificStartOffsets.put(new KafkaTopicPartition("topic", 0), 23L)
    myConsumer.setStartFromSpecificOffsets(specificStartOffsets)

    val stream = env.addSource(myConsumer)
      stream.print()

    env.execute("Flink kafka test")
  }
}

四、數據轉換(Transformation)

數據處理的核心就是對數據進行各種轉化操作,在Flink上就是通過轉換將一個或多個DataStream轉換成新的DataStream。

爲了更好的理解transformation函數,下面給出匿名類的方式來實現各個函數。

所有轉換函數都是依賴以下基礎:

//  獲取運行環境
    val env :StreamExecutionEnvironment =StreamExecutionEnvironment.getExecutionEnvironment
    val path ="text01.txt"
    val stream =env.readTextFile(path)

1、基礎轉換操作

(1)Map轉換

1)Map

接受一個元素,輸出一個元素。MapFunction<T,V>中T代表輸入數據類型(map方法的參數類型),V代表操作結果輸出類型(map方法返回數據類型)。

2)flatMap

輸入一個元素,輸出0個、1個或多個元素。FlatMapFunction<T,V>中T代表輸入元素數據類型(flatMap方法的第一個參數類型),V代表輸出集合中元素類型(flatMap中的Collector類型參數)

轉換前後數據類型:DataStream->DataStream。

3)filter

過濾指定元素數據,如果返回true則該元素繼續向下傳遞,如果爲false則將該元素過濾掉。FilterFunction<T>中T代表輸入元素的數據類型。

轉換前後數據類型:DataStream->DataStream。

object FlinkSource01 {
  def main(args: Array[String]): Unit = {
//  獲取運行環境
    val env :StreamExecutionEnvironment =StreamExecutionEnvironment.getExecutionEnvironment
    val path ="text01.txt"
    val stream =env.readTextFile(path)
//    val list =List(1,2,3,4)
//    val stream= env.fromCollection(list)
//    val stream =env.generateSequence(1,10)
    import org.apache.flink.api.scala._
    val mapStream = stream.map(_.split(" ").mkString("_"))
    val flatmapStream = mapStream.flatMap(_.split("_"))
    val filterStream = flatmapStream.filter(_.equals("hello"))
    filterStream.print()//6> hello world flink---->6> 線程編號
    env.execute("FlinkSource01")
  }
}

(2)鍵值對操作

1)keyBy

DataStream → KeyedStream:輸入必須是 Tuple 類型,邏輯地將一個流拆分成不相交的分區,每個分區包含具有相同 key 的元素,在內部以 hash 的形式實現的

    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val stream = env.readTextFile("test.txt")
    val streamFlatMap = stream.flatMap{
      x => x.split(" ")
    }
    val streamMap = streamFlatMap.map{
      x => (x,1)
    }
    val streamKeyBy = streamMap.keyBy(0)
    env.execute("FirstJob")

以下情況的元素不能作爲key使用:

  1. POJO類型,但沒有重寫hashCode(),而是依賴Object.hashCode()。
  2. 該元素是數組類型。

2)reduce

KeyedStream → DataStream:一個分組數據流的聚合操作,合併當前的元素和上次聚合的結果,產生一個新的值,返回的流中包含每一次聚合的結果,而不是隻返回最後一次聚合的最終結果。

轉換前後數據類型:KeyedStream->DataStream。

KeyBy和Reduce實例

val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.readTextFile("test.txt").flatMap(item => item.split(" ")).map(item =>
(item, 1)).keyBy(0)
val streamReduce = stream.reduce(
(item1, item2) => (item1._1, item1._2 + item2._2)
)
streamReduce.print()
env.execute("FirstJob")
object FlinkSource01 {
  def main(args: Array[String]): Unit = {
//  獲取運行環境
    val env :StreamExecutionEnvironment =StreamExecutionEnvironment.getExecutionEnvironment
    val path ="text01.txt"
    val stream =env.readTextFile(path)
    import org.apache.flink.api.scala._
    val keybyReduceStream = stream.flatMap(_.split(" "))
      .map(w=> (w,1))//把單詞轉成(word,1)這種形式
//      .keyBy(0)
      .keyBy(_._1)
      .reduce((x, y) => (x._1, x._2 + y._2))

    keybyReduceStream.print()
    env.execute("FlinkSource01")
  }
}

3)Fold(Deprecated)

KeyedStream → DataStream:一個有初始值的分組數據流的滾動摺疊操作,合併當前元素和前一次摺疊操作的結果,併產生一個新的值,返回的流中包含每一次摺疊的結果,而不是隻返回最後一次摺疊的最終結果。

該方法已經標記爲廢棄!

轉換前後數據類型:KeyedStream->DataStream。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.readTextFile("test.txt").flatMap(item => item.split(" ")).map(item =>
(item, 1)).keyBy(0)
val streamReduce = stream.fold(100)(
(begin, item) => (begin + item._2)
)
streamReduce.print()
env.execute("FirstJob")

4)Aggregations

滾動聚合具有相同key的數據流元素,我們可以指定需要聚合的字段(field)。DataStream<T>中的T爲聚合之後的結果。

KeyedStream → DataStream:分組數據流上的滾動聚合操作。 min 和 minBy 的區別是 min 返回的是一個最小值,而 minBy 返回的是其字段中包含最小值的元素(同樣原理適用於 max 和 maxBy),返回的流中包含每一次聚合的結果,而不是隻返回最後一次聚合的最終結果。

//對KeyedStream中元素的第一個Filed求和
DataStream<String> dataStream1 = keyedStream.sum(0);
//對KeyedStream中元素的“count”字段求和
keyedStream.sum("count");
//獲取keyedStream中第一個字段的最小值
keyedStream.min(0);
//獲取keyedStream中count字段的最小值的元素
keyedStream.minBy("count");
keyedStream.max("count");
keyedStream.maxBy(0);

min和minBy的區別是:min返回指定字段的最小值,而minBy返回最小值所在的元素。

轉換前後數據類型:KeyedStream->DataStream。

實例

object FlinkSource01 {
  def main(args: Array[String]): Unit = {
//  獲取運行環境
    val env :StreamExecutionEnvironment =StreamExecutionEnvironment.getExecutionEnvironment
    val path ="text01.txt"
    val stream =env.readTextFile(path)
//    val list =List(1,2,3,4)
//    val stream= env.fromCollection(list)
//    val stream =env.generateSequence(1,10)
    import org.apache.flink.api.scala._
    val keybyReduceStream = stream.flatMap(_.split(" "))
      .map(w=> (w,1))//把單詞轉成(word,1)這種形式
//      .keyBy(0)
      .keyBy(_._1)
//    reduce
      .reduce((x, y) => (x._1, x._2 + y._2))
//    Aggregations
//      .sum(1)
//      .min(1)
        .keyBy(0)
    .minBy(1)
    keybyReduceStream.print()
    env.execute("FlinkSource01")
  }
//  case class WordWithCount(word:String,count:Long)
}
val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.readTextFile("test02.txt").map(item => (item.split(" ")(0), item.split("
")(1).toLong)).keyBy(0)
val streamReduce = stream.sum(1)
streamReduce.print()
env.execute("FirstJob")

(3)stream合併操作

1)Connect

DataStream,DataStream → ConnectedStreams:連接兩個保持他們類型的數據流,兩個數據流被 Connect 之後,只是被放在了一個同一個流中, 內部依然保持各自的數據和形式不發生任何變化,兩個流相互獨立。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.readTextFile("test.txt")
val streamMap = stream.flatMap(item => item.split(" ")).filter(item =>
item.equals("hadoop"))
val streamCollect = env.fromCollection(List(1,2,3,4))
val streamConnect = streamMap.connect(streamCollect)
streamConnect.map(item=>println(item), item=>println(item))
env.execute("FirstJob")

2)CoMap,CoFlatMap

ConnectedStreams → DataStream:作用於 ConnectedStreams 上,功能與 map和 flatMap 一樣,對 ConnectedStreams 中的每一個 Stream 分別進行 map 和 flatMap處理。
 

val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream1 = env.readTextFile("test.txt")
val streamFlatMap = stream1.flatMap(x => x.split(" "))
val stream2 = env.fromCollection(List(1,2,3,4))
val streamConnect = streamFlatMap.connect(stream2)
val streamCoMap = streamConnect.map(
(str) => str + "connect",
(in) => in + 100
)
env.execute("FirstJob")
val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream1 = env.readTextFile("test.txt")
val stream2 = env.readTextFile("test1.txt")
val streamConnect = stream1.connect(stream2)
val streamCoMap = streamConnect.flatMap(
(str1) => str1.split(" "),
(str2) => str2.split(" ")
)
streamConnect.map(item=>println(item), item=>println(item))
env.execute("FirstJob")

3)split

DataStream → SplitStream:根據某些特徵把一個 DataStream 拆分成兩個或者多個 DataStream。
 

val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.readTextFile("test.txt")
val streamFlatMap = stream.flatMap(x => x.split(" "))
val streamSplit = streamFlatMap.split(
num =>
# 字符串內容爲 hadoop 的組成一個 DataStream,其餘的組成一個 DataStream
(num.equals("hadoop")) match{
case true => List("hadoop")
case false => List("other")
}
)
env.execute("FirstJob")

4)select

SplitStream→DataStream:從一個 SplitStream中獲取一個或者多個 DataStream。

    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val stream = env.readTextFile("test.txt")
    val streamFlatMap = stream.flatMap(x => x.split(" "))
    val streamSplit = streamFlatMap.split(num =>
      (num.equals("hadoop")) match{
        case true => List("hadoop")
        case false => List("other")
      }
    )
    val hadoop = streamSplit.select("hadoop")
    val other = streamSplit.select("other")
    hadoop.print()
    env.execute("FirstJob")

5)Union

DataStream → DataStream:對兩個或者兩個以上的 DataStream 進行 union 操作,產生一個包含所有 DataStream 元素的新 DataStream。注意 :如果你將一個DataStream 跟它自己做 union 操作,在新的 DataStream 中,你將看到每一個元素都
出現兩次。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream1 = env.readTextFile("test.txt")
val streamFlatMap1 = stream1.flatMap(x => x.split(" "))
val stream2 = env.readTextFile("test1.txt")
val streamFlatMap2 = stream2.flatMap(x => x.split(" "))
val streamConnect = streamFlatMap1.union(streamFlatMap2)
env.execute("FirstJob")

五、分區

1、自定義分區(partitionCustom)

使用用戶自定義的分區函數對指定key進行分區,partitionCustom只支持單分區。

dataStream.partitionCustom(new Partitioner<String>() {
            
            @Override
            public int partition(String key, int numPartitions) {
                return key.hashCode() % numPartitions;
            }
        },1);

轉換前後的數據類型:DataStream->DataStream

2、隨機分區(shuffle)

均勻隨機將元素進行分區。

dataStream.shuffle();

轉換前後的數據類型:DataStream->DataStream

3、rebalance

以輪詢的方式爲每個分區均衡分配元素,對於優化數據傾斜該方法非常有效。

dataStream.rebalance();

轉換前後的數據類型:DataStream->DataStream

4、broadcast

使用broadcast可以向每個分區廣播元素。

dataStream.broadcast();

轉換前後的數據類型:DataStream->DataStream

5、rescale

根據上下游task數進行分區。

dataStream.rescale();

轉換前後的數據類型:DataStream->DataStream

六、結果數據接收器(Data sink)

數據經過Flink處理之後,最終結果會寫到 file、socket、外部系統或者直接打印出來。數據接收器定義在DataStream類下,我們通過addSink()可以來添加一個接收器。同 Source,Flink 也提供了一些預定義的Data Sink讓我們直接使用。

1、寫入文本文件

將元素以字符串形式逐行寫入(TextOutputFormat),這些字符串通過調用每個元素的 toString()方法來獲取。

DataStream提供了兩個writeAsText重載方法,寫入格式會調用寫入對象的toString()方法。

  • writeAsText(String path):將DataStream數據寫入到指定文件。

  • writeAsText(String path,WriteMode writeMode):將DataStream數據寫入到指定文件,可以通過writeMode來指定如果文件已經存在應該採用什麼方式,可以指定OVERWRITE或NO_OVERWRITE。

2、寫入CSV文件

DataStream提供了三個寫入csv文件的重載方法,對於DataStream中的每個Filed,都會調用其對象的toString()方法作爲寫入格式。writeAsCsv只能用於元組(Tuple)的DataStream。

writeAsCsv(String path,WriteMode writeMode,String rowDelimiter,String fieldDelimiter)
參數 說明 實例
path 寫入文件路徑
writeMode 如果寫入文件已經存在,採用什麼方式處理 WriteMode.NO_OVERWRITE 或WriteMode.OVERWRITE
rowDelimiter 定義行分隔符
fieldDelimiter 定義列分隔符  

DataStream提供了兩個簡易重載方法:

  • writeAsCsv(String path):使用"\n"作爲行分隔符,使用","作爲列分隔符。

  • writeAsCsv(String path,WriteMode writeMode):使用"\n"作爲行分隔符,使用","作爲列分隔符。

4、寫入Socket

Flink提供了將DataStream作爲字節數組寫入Socket的方法,通過SerializationSchema來指定輸出格式。

writeToSocket(String hostName,int port,SerializationSchema<T> schema)

5、指定輸出格式

自定義文件輸出的方法和基類(FileOutputFormat),支持自定義對象到字節的轉換。

DataStream提供了自定義文件輸出的類和方法,我們能夠自定義對象到字節的轉換。

writeUsingOutputFormat(OutputFormat<T> format)

6、結果打印

DataStream提供了print和printToErr打印標準輸出/標準錯誤流。DataStream中的每個元素都會調用其toString()方法作爲輸出格式,我們也可以指定一個前綴字符來區分不同的輸出。

  • print():標準輸出

  • print(String sinkIdentifier):指定輸出前綴

  • printToErr():標準錯誤輸出

  • printToErr(String sinkIdentifier):指定輸出前綴

對於並行度大於1的輸出,輸出結果也將輸出任務的標識符作爲前綴。

7、自定義輸出器

我們一般會自定義輸出器,通過實現SinkFunction接口,然後通過DataStream.addSink(sinkFunction)來指定數據接收器。

addSink(SinkFunction<T> sinkFunction)

注意:對於DataStream中的writeXxx()方法一般都是用於測試使用,因爲他們並沒有參與 chaeckpoint,所以它們只有"at-last-once"也就是至少處理一次語義。

如果想要可靠輸出,想要使用"exactly-once"語義準確將結果寫入到文件系統中,我們需要使用flink-connector-filesystem此外,我們也可以通過addSink()自定義輸出器來使用Flink的checkpoint來完成"exactl-oncey"語義

(1)、MySql Sink

package DataStreamApi.sink.mysqlSink

import java.text.SimpleDateFormat
import java.util.Properties

import com.google.gson.{Gson, JsonParser}
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer08
import org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartition
object MySqlSinkApp {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val props = new Properties()
    props.put("bootstrap.servers", "broker")
    props.put("zookeeper.connect", "localhost:2181")
    props.put("group.id", "flink_test_sxp")
//key 反序列化
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer") 
//value 反序列化
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    props.put("auto.offset.reset", "latest") 

    val myConsumer = new FlinkKafkaConsumer08[String]("topic", new SimpleStringSchema(),props)
    //默認讀取上次保存的offset信息
    myConsumer.setStartFromGroupOffsets()
    // 從最早的數據開始進行消費,忽略存儲的offset信息
    myConsumer.setStartFromEarliest()
    // 從最新的數據進行消費,忽略存儲的offset信息
    myConsumer.setStartFromLatest()        
    val specificStartOffsets = new java.util.HashMap[KafkaTopicPartition, java.lang.Long]()
    //從指定位置進行消費
    specificStartOffsets.put(new KafkaTopicPartition("node-bullet-crawler-59", 0), 23L)
    myConsumer.setStartFromSpecificOffsets(specificStartOffsets)

    val dataStream = env.addSource(myConsumer)

    val dataStreamMaped=   ...
    dataStreamMaped.addSink(new SinkToMySql)
    env.execute("Flink kafka sink to mysql")
  }
}
package DataStreamApi.sink.mysqlSink

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

import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction


class SinkToMySql extends RichSinkFunction[MessageBean]{
  var ps:PreparedStatement = _
  var connection:Connection = _
  /**
    * open() 方法中建立連接,這樣不用每次 invoke 的時候都要建立連接和釋放連接
    * @param parameters
    * @throws Exception
    */
  override  def open(parameters:Configuration ) :Unit= {
    super.open(parameters)
    connection = getConnection()
    val sql = "insert into msg(x1, x2, x3, x3) values(?, ?, ?, ?);"
    ps = this.connection.prepareStatement(sql)
  }

  def getConnection():Connection= {
    var con :Connection = null
    try {
      Class.forName("com.mysql.jdbc.Driver")
      con = DriverManager.getConnection("jdbc:mysql://ip:3306/test_sun?useUnicode=true&
            characterEncoding=UTF-8", "userid", "password")
    } catch  {
      case  e:Exception =>System.out.println("-----------mysql connection has exception , 
            msg = "+ e.getMessage)
    }
    con
  }

  override def close() :Unit= {
    super.close()
    //關閉連接和釋放資源
    if (connection != null) {
      connection.close();
    }
    if (ps != null) {
      ps.close();
    }
  }


  /**
    * 每條數據的插入都要調用一次 invoke() 方法
    *
    * @param value
    * @throws Exception
    */
  @throws[Exception]
  def invoke(value: MessageBean): Unit = { //組裝數據,執行插入操作
//    println(value.toString)
    ps.setInt(1, value.x1.toInt)
    ps.setString(2, value.x2)
    ps.setString(3, value.x3)
    ps.setString(4, filterEmoji(value.x4))
    ps.executeUpdate
  }

  def filterEmoji(source: String): String = {
    if (source != null && source.length() > 0) {
      source.replaceAll("[\ud800\udc00-\udbff\udfff\ud800-\udfff]", "")//過濾Emoji表情
        .replaceAll("[\u2764\ufe0f]","")//過濾心形符號
    } else {
      source
    }
  }
}

 

 

 

 

 

 

 

 

 

 

 

 

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