目錄
一、基本介紹
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使用:
- POJO類型,但沒有重寫hashCode(),而是依賴Object.hashCode()。
- 該元素是數組類型。
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
}
}
}