大家好,我是後來,我會分享我在學習和工作中遇到的點滴,希望有機會我的某篇文章能夠對你有所幫助,所有的文章都會在公衆號首發,歡迎大家關注我的公衆號" 後來X大數據 ",感謝你的支持與認可。
文章目錄
通過前2篇flink的學習,已經基本掌握了flink的基本使用,但是關於flink真正內核的東西還沒開始說,那先簡單介紹一下,flink的核心亮點:
- 窗口
- 時間語義
- 精準一次性
我們在第一篇的學習瞭解到了flink的wordCount,以及在第二篇的API 中,我們也只是獲取到數據,進行簡單的轉換,就直接把數據輸出。
但是我們在之前都是以事件爲驅動,等於說是來了一條數據,我就處理一次,但是現在遇到的問題是:
我們可以簡單的把wordCount的需求比做公司的訂單金額,也就是訂單金額會隨着訂單的增加而只增不減,那麼如果運營部門提了以下需求:
- 每有1000條訂單就輸出一次這1000條訂單的總金額
- 每5分鐘輸出一次剛剛過去這5分鐘的訂單總金額
- 每3秒輸出一次最近5分鐘內的累計成交額
- 連續2條訂單的間隔時間超過30秒就按照這個時間分爲2組訂單,輸出前一組訂單的總金額
那麼面對這個需求,因爲時間一直是流動的,大家有什麼想法?
基於這些需求,我們來講一下flink的窗口。
窗口
窗口:無論是hive中的開窗函數,還是Spark中的批次計算中的窗口,還是我們這裏講的窗口,本質上都是對數據進行劃分,然後對劃分後的數據進行計算。
那麼Windows是處理無限流的核心。Windows將流分成有限大小的“存儲桶”,我們可以在其上應用計算。
在flink中,窗口式Flink程序一般有2類,
- 鍵控流
stream
.keyBy(...) <- keyed versus non-keyed windows
.window(...) <- required: "assigner"
[.trigger(...)] <- optional: "trigger" (else default trigger)
[.evictor(...)] <- optional: "evictor" (else no evictor)
[.allowedLateness(...)] <- optional: "lateness" (else zero)
[.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data)
.reduce/aggregate/fold/apply() <- required: "function"
[.getSideOutput(...)] <- optional: "output tag"
- 非鍵控流
stream
.windowAll(...) <- required: "assigner"
[.trigger(...)] <- optional: "trigger" (else default trigger)
[.evictor(...)] <- optional: "evictor" (else no evictor)
[.allowedLateness(...)] <- optional: "lateness" (else zero)
[.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data)
.reduce/aggregate/fold/apply() <- required: "function"
[.getSideOutput(...)] <- optional: "output tag"
唯一的區別是:對鍵控流的keyBy(…)調用window(…),而非鍵控流則是調用windowAll(…)。
窗口的生命週期
我們上面說窗口就是對數據進行劃分到不同的“桶”中,然後進行計算,那麼什麼開始有這個桶,什麼時候就算是分完了呢?
簡而言之,一旦應屬於該窗口的第一個元素到達,就會創建一個窗口,當時間超過用戶設置的時間戳時,flink將刪除這個窗口。
那我們來理解一下窗口的類型:
- CountWindow:按照指定的數據條數生成一個Window,與時間無關。
- TimeWindow:按照時間生成Window。
1. 滾動窗口
2. 滑動窗口
3. 會話窗口
從文字也不難看出,CountWindow就是按照數據條數生成窗口,樣例代碼如下:
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala._
object CountWindowsTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val wordDS = env.socketTextStream("master102",3456)
wordDS
.map((_,1))
.keyBy(0)
//累計單個Key中3條數據就進行處理
.countWindow(3)
.sum(1)
.print("測試:")
env.execute()
}
}
執行結果如下:
可以看出,不同的單詞根據keyby進入不同的窗口,然後當窗口中的單個key的數據個數達到3個之後進行輸出。
接下來,我們主要來說一下時間窗口,這些窗口的結束與開始都是根據數據的時間來判斷的,所以這裏就引出了我們今天的第二個重點:時間語義
時間語義
Flink 在流式傳輸程序中支持不同的時間概念:
- Event Time:事件時間是每個事件在其生產設備上發生的時間。它通常由事件中的時間戳描述,例如採集的日誌數據中,每一條日誌都會記錄自己的生成時間,Flink通過時間戳分配器訪問事件時間戳。
- Ingestion Time:攝取時間是數據進入Flink的時間。攝取時間從概念上講介於事件時間和處理時間之間。
- Processing Time:處理時間是是指正在執行相應算子操作的機器的系統時間,默認的時間屬性就是Processing Time。 處理時間是最簡單的時間概念,不能提供確定性,因爲它容易依賴數據到達系統(例如從消息隊列)到達系統的速度,以及數據在系統內部之間流動的速度。
我們根據業務的需求還判斷使用哪個時間類型,一般來說使用Event Time更多,比如:在統計最近5分鐘的訂單總金額時,我們需要的是真實的訂單時間,而不是進入flink的時間或者是處理時間。
在Flink的流式處理中,絕大部分的業務都會使用EventTime,一般只在EventTime無法使用時,纔會被迫使用ProcessingTime或者IngestionTime。默認情況下,Flink框架中處理的時間語義爲ProcessingTime,如果要使用EventTime,那麼需要引入EventTime的時間屬性,引入方式如下所示:
import org.apache.flink.streaming.api.TimeCharacteristic
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
這裏注意:如果要使用事件時間,那麼必須要爲數據定義事件時間,並且還要註冊水位線
好了,又是一個新的知識點:水位線
我們暫時先有這些概念,然後我們再返回來繼續說我們的窗口的類型。說完窗口類型,再詳細說水位線的應用。
所以這也爲後面的數據亂序埋下了坑,比如,2條訂單,它們的訂單時間差不多,一前一後,但是因爲先下單的這條訂單的網絡情況不好,導致後到達flink窗口,也就是我們常說的數據亂序,那麼這種情況該怎麼辦?我們後面再說這個問題
特別注意:窗口是左閉右開的。
滾動窗口
滾動窗口具有固定的尺寸和不重疊,例如,如果指定大小爲5分鐘的滾動窗口,則每五分鐘將啓動一個新窗口,如下圖所示。
樣例代碼如下:
import java.text.SimpleDateFormat
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
/**
* @description: ${description}
* @author: Liu Jun Jun
* @create: 2020-06-29 13:59
**/
object WindowTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val dataDS = env.socketTextStream("bigdata101", 3456)
val tsDS = dataDS.map(str => {
val strings = str.split(",")
(strings(0), strings(1).toLong, 1)
}).keyBy(0)
//窗口大小爲5s的滾動窗口
//.timeWindow(Time.seconds(5))和下面的這種寫法都是可以的
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.apply {
(tuple: Tuple, window: TimeWindow, es: Iterable[(String, Long, Int)], out: Collector[String]) => {
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
//out.collect(s"window:[${sdf.format(new Date(window.getStart))}-${sdf.format(new Date(window.getEnd))}]:{ ${es.mkString(",")} }")
out.collect(s"window:[${window.getStart}-${window.getEnd}]:{ ${es.mkString(",")} }")
}
}.print("windows:>>>")
env.execute()
}
}
通過運行,大概會發現,我們輸入的時間戳並不會起作用,默認使用的確實是處理時間:
同時,可以看出,滾動窗口的時間窗口不會有重疊,一條數據只會屬於一個窗口,而且,窗口是左閉右開的。
滑動窗口
滑動窗口也是固定長度的窗口,不過由於滑動的頻率,當滑動頻率小於窗口大小時,滑動窗口會重疊,在這種情況下,一個元素被分配到多個窗口。
例如:指定大小爲10分鐘的窗口滑動5分鐘。這樣,您每隔5分鐘就會得到一個窗口,其中包含最近10分鐘內到達的事件,如下圖所示。
接下來,我只貼改動代碼,其餘代碼和上面的滾動代碼是一樣的:
//滾動5秒,滑動3秒
//.window(SlidingProcessingTimeWindows.of(Time.seconds(5), Time.seconds(3)))和下面的這句話是一樣的
.timeWindow(Time.seconds(5),Time.seconds(3))
非常關鍵的是:大家發現,flink默認的分配窗口是從每秒從0開始數的,舉例:會把5秒的窗口分爲:
[0-5),[5,10),[10-15),…
3秒的窗口爲:
[0-3),[3,6),[6-9),…
會話窗口
與滾動窗口和滑動窗口相比,會話窗口不重疊且沒有固定的開始和結束時間。相反,會話窗口在一定時間段內未收到元素時(即,出現不活動間隙時)關閉。隨後的元素將分配給新的會話窗口。
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
可以看出,這次的窗口大小並不是固定的,那麼我在測試輸入的時候,輸完一些後等了一會兒才繼續輸入的,那麼就出現了第一個窗口,所以只要processtime間隔時間超過10s,就會輸出上一個窗口。
總結窗口的知識點:
- 以上所有的窗口的時間都可以更改爲EventTime,同時時間間隔可以指定爲Time.milliseconds(x),Time.seconds(x),Time.minutes(x)
如果使用timewindow()方法,那麼會隨着事件時間的指定會更改爲以事件時間爲標準的窗口,而如果使用window()方法,那麼其中的參數會發生變化。
//滾動窗口
//事件時間
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
//處理時間
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
//滑動窗口
//事件時間
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
//處理時間
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
//會話窗口
//事件時間
.window(EventTimeSessionWindows.withGap(Time.minutes(10)))
//處理時間
.window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
- flink默認的分配窗口是從每秒從0開始數的,舉例:會把5秒的窗口分爲:
[0-5),[5,10),[10-15),…
3秒的窗口爲:
[0-3),[3,6),[6-9),…
那麼可不可以做到窗口的劃分爲[1-6),[6,11)…
當然可以,flink有窗口偏移設置。一般用不到,我在這裏簡單貼一下使用方式:
//5秒的窗口偏移3秒
.window(TumblingProcessingTimeWindows.of(Time.seconds(5), Time.seconds(3)))
能從上圖看出,窗口從原來的80-85,偏移到了83-88。那我再把方法總結一下
//窗口偏移方法總結
//滾動窗口
//事件時間
.window(TumblingEventTimeWindows.of(Time.seconds(5),Time.seconds(3)))
//處理時間
.window(TumblingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(3)))
//滑動窗口
//事件時間
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5),Time.seconds(3)))
//處理時間
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5),Time.seconds(3)))
//會話窗口
//事件時間
.window(EventTimeSessionWindows.withGap(Time.minutes(10),Time.seconds(3)))
//處理時間
.window(ProcessingTimeSessionWindows.withGap(Time.minutes(10),Time.seconds(3)))
關於窗口的使用基本上差不多了,接下來只要說一說水位線
水位線WaterMark
WaterMark,叫做水位線,那它是幹啥的?支持事件時間的流處理器需要一種衡量事件時間進度的方法。
這裏要注意:使用事件時間必須要使用註冊水位線,而水位線也是事件時間專用的
例如,當以事件時間開窗1小時,目前窗口剛超過一個小時,需要通知構建每小時窗口的窗口操作員,關閉正在進行中的這個窗口程序。
那問題來了,怎麼衡量時間到了沒?所以Flink中用於衡量事件時間進度的機制是水位線。
強調:並不是每條數據都會生成水位線。水位線也是一條數據,是流數據的一部分,watermark是一個全局的值,不是某一個key下的值,所以即使不是同一個key的數據,其warmark也會增加。
同時,水位線還有一個重要作用,就是處理延遲數據,我們在文章開頭的部分也提到了,數據亂序怎麼處理,那麼有些數據因爲網絡的原因,延遲了幾秒,所以也可以把水位線看作是窗口最後的執行時間。
比如說,我們規定滾動窗口爲5秒,也就是[5-10),同時我們預測數據一般可能延遲3秒,所以我們希望窗口是當10s的數據到達後,繼續等待3秒,看這3秒內,還是否有原本是[5-10)中的數據,一起歸併到這個窗口中,等到出現了時間爲大於等於13s的數據時,就會觸發[5-10)這個窗口的數據執行。這就是延遲處理。(代碼案例看下面的週期性水位線)
那麼水位線怎麼生成呢?
有兩種分配時間戳和生成水位線的方法:
- 直接在數據流源中(我現在還不知道哪種數據源可以直接生成時間戳和水位線,所以這裏不討論了)
- 通過時間戳分配器/水位線生成器:在Flink中,時間戳分配器還定義要發送的水位線注意自1970-01-01T00:00:00Z的Java時代以來,時間戳和水位線都指定爲毫秒。(大部分使用情況)
Event Time的使用一定要指定數據源中的時間戳。否則程序無法知道事件的事件時間是什麼(數據源裏的數據沒有時間戳的話,就只能使用Processing Time了)。
那我們就利用第二種方式來生成水位線吧,注意要在事件時間的第一個操作(例如第一個窗口操作)之前指定分配器,例如:
我們發現註冊水位線的有2個接口可以實現:
- AssignerWithPeriodicWatermarks(週期性生成水位線)
- AssignerWithPunctuatedWatermarks(標記性生成水位線)
一個一個說,先說週期性生成水位線:
週期性水位線
//flink默認200ms(毫秒)生成一條水位線,那我們也可以修改
@PublicEvolving
public void setStreamTimeCharacteristic(TimeCharacteristic characteristic) {
this.timeCharacteristic = Preconditions.checkNotNull(characteristic);
if (characteristic == TimeCharacteristic.ProcessingTime) {
getConfig().setAutoWatermarkInterval(0);
} else {
getConfig().setAutoWatermarkInterval(200);
}
}
//單位是毫秒,所以我這裏模擬設置的爲10s
env.getConfig.setAutoWatermarkInterval(10000)
那麼這裏的時間間隔指的是系統時間的10s,可不是事件時間的10s,這個不要弄混,不相信的話可以等會看我的測試案例。
import java.text.SimpleDateFormat
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
/**
* @description: ${本案例模擬的是:以事件時間爲標準,窗口滾動時間爲5秒}
* @author: Liu Jun Jun
* @create: 2020-06-28 18:31
**/
object WaterMarkTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
//設置以事件時間爲基準
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
//並行度設置爲1。關於並行度的案例會在後面測試
env.setParallelism(1)
//設置10s生成一次水位線
env.getConfig.setAutoWatermarkInterval(10000)
val dataDS = env.socketTextStream("bigdata101", 3456)
val tsDS = dataDS.map(str => {
val strings = str.split(",")
(strings(0), strings(1).toLong, 1)
}).assignTimestampsAndWatermarks(
new AssignerWithPeriodicWatermarks[(String,Long,Int)]{
var maxTs :Long= 0
//得到水位線,週期性調用這個方法,得到水位線,我這裏設置的也就是延遲5秒
override def getCurrentWatermark: Watermark = new Watermark(maxTs - 5000)
//負責抽取事件事件
override def extractTimestamp(element: (String, Long, Int), previousElementTimestamp: Long): Long = {
maxTs = maxTs.max(element._2 * 1000L)
element._2 * 1000L
}
}
/*new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.seconds(5)) {
override def extractTimestamp(element: (String, Long, Int)): Long = element._2 * 1000
}*/
)
val result = tsDS
.keyBy(0)
//窗口大小爲5s的滾動窗口
.timeWindow(Time.seconds(5))
.apply {
(tuple: Tuple, window: TimeWindow, es: Iterable[(String, Long, Int)], out: Collector[String]) => {
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
//out.collect(s"window:[${sdf.format(new Date(window.getStart))}-${sdf.format(new Date(window.getEnd))}]:{ ${es.mkString(",")} }")
out.collect(s"window:[${window.getStart}-${window.getEnd}]:{ ${es.mkString(",")} }")
}
}
tsDS.print("water")
result.print("windows:>>>")
env.execute()
}
}
那麼從結果可以看出:
【10- 15)的窗口是20這條數據觸發的,在我輸入20這條數據等了幾秒後輸出了第一個窗口
證實:10s的間隔時間爲系統時間,同時水位線=當前時間戳 - 延遲時間 ,如果窗口的end time <= 水位線,則會觸發這個窗口的執行
【15- 20)的窗口是25這條數據觸發的,同樣符合窗口的end time <= 水位線
那麼如果數據的窗口已經觸發了,但還有一點數據還是遲到了怎麼辦?
所有還有個概念就是allowedLateness(允許接收延遲數據),並且還會繼續把數據放入對應的窗口。看代碼吧:
//其餘代碼和上面案例的一樣,只是在開窗之後多了一行
.keyBy(0)
.timeWindow(Time.seconds(5))
//具體這2秒代表什麼意思,看完測試結果案例就懂了
.allowedLateness(Time.seconds(2)
.apply{}
通過看圖應該能明白這裏allowedLateness(Time.seconds(2)是什麼意思了,只要是窗口觸發後,時間小於設定的延遲時間,收到的延遲數據都可以處理,但要是沒有設置allowedLateness(Time.seconds(2)),那麼窗口觸發後的延遲數據都不會處理。
數據的延遲總是不可完全預測的,假如時間已經超過了允許接收的延遲數據時間,還有一點點數據遲到,就是上圖中,在22這條數據之後我輸入的14這條數據,那怎麼辦?這種情況下,我們不能爲了偶爾的一點數據就把所有窗口的等待時間延遲很久,所有還有個概念就是側輸出流,將晚到的數據放置在側輸出流中。來看代碼:
//只加了3行,其餘的和之前的代碼一樣
val outputTag = new OutputTag[(String, Long, Int)]("lateData")//新加的
val result = tsDS
.keyBy(0)
.timeWindow(Time.seconds(5))
.allowedLateness(Time.seconds(2))
.sideOutputLateData(outputTag)//新加的
.apply {}
result.getSideOutput(outputTag).print("side>>>")//新加的
標記性水位線
知識很多東西是想通的,所以開始講延遲數據就巴拉巴拉一堆,再繼續說間標記水位線,爲什麼叫做標記呢?因爲這種水位線的生成與時間無關,而是決定於何時收到標記事件。
默認情況下,所有的數據都屬於標記事件,意味着每條數據都會生成水位線。
所以使用這種方式的時候,需要對某些特定事件進行標記。
object WaterMarkTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val dataDS = env.socketTextStream("bigdata101", 3456)
val tsDS = dataDS.map(str => {
val strings = str.split(",")
(strings(0), strings(1).toLong, 1)
}).assignTimestampsAndWatermarks(
new AssignerWithPunctuatedWatermarks[(String,Long,Int)] {
override def checkAndGetNextWatermark(lastElement: (String, Long, Int), extractedTimestamp: Long): Watermark = {
if (lastElement._1 .contains("later")){
println("間歇性生成了水位線.....")
// 間歇性生成水位線數據
new Watermark(extractedTimestamp)
}
return null
}
override def extractTimestamp(element: (String, Long, Int), previousElementTimestamp: Long): Long = {
element._2 * 1000L
}
}
)
val result = tsDS
.keyBy(0)
.timeWindow(Time.seconds(5))
.apply {
(tuple: Tuple, window: TimeWindow, es: Iterable[(String, Long, Int)], out: Collector[String]) => {
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
out.collect(s"window:[${window.getStart}-${window.getEnd}]:{ ${es.mkString(",")} }")
}
}
tsDS.print("water")
result.print("calc")
env.execute()
}
}
看一下我的測試結果:
當然即便我們設置了標記,在TPS很高的場景下依然會產生大量的Watermark,在一定程度上對下游算子造成壓力,所以只有在實時性要求非常高的場景纔會選擇Punctuated的方式進行Watermark的生成。
關於並行度與水位線
細心的小夥伴也會發現,我在上面的所有的案例中,使用的並行度都是1,但實際生產中肯定不是1啊,這個會有什麼變化麼?當然是有的。
我先說結論:
如果並行度不爲1,那麼在計算窗口時,是按照各自的並行度單獨計算的。只有當所有並行度中都觸發了同一個窗口,那麼這個窗口才會觸發。
口說無憑,我們來看案例,這次放完整代碼:
import java.text.SimpleDateFormat
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
/**
* @description: ${模擬多並行度下,窗口如何觸發}
* @author: Liu Jun Jun
* @create: 2020-06-28 18:31
**/
object WaterMarkTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
//注掉了並行度爲1,默認並行度=cpu核數,我這裏cpu爲4個
//env.setParallelism(1)
env.getConfig.setAutoWatermarkInterval(10000)
val dataDS = env.socketTextStream("bigdata101", 3456)
val tsDS = dataDS.map(str => {
val strings = str.split(",")
(strings(0), strings(1).toLong, 1)
}).assignTimestampsAndWatermarks(
new AssignerWithPeriodicWatermarks[(String,Long,Int)]{
var maxTs :Long= 0
override def getCurrentWatermark: Watermark = new Watermark(maxTs - 5000)
override def extractTimestamp(element: (String, Long, Int), previousElementTimestamp: Long): Long = {
maxTs = maxTs.max(element._2 * 1000L)
element._2 * 1000L
}
}
)
//該案例中,爲了簡單,去掉了allowedLateness和側輸出流
val result = tsDS
.keyBy(0)
.timeWindow(Time.seconds(5))
.apply {
(tuple: Tuple, window: TimeWindow, es: Iterable[(String, Long, Int)], out: Collector[String]) => {
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
//out.collect(s"window:[${sdf.format(new Date(window.getStart))}-${sdf.format(new Date(window.getEnd))}]:{ ${es.mkString(",")} }")
out.collect(s"window:[${window.getStart}-${window.getEnd}]:{ ${es.mkString(",")} }")
}
}
tsDS.print("water")
result.print("calc")
env.execute()
}
}
看一下測試結果吧:
好了,到這裏,窗口、時間語義以及水位線的基本原理就說完了,理解了這些再看看文章開頭提到了4個需求,是不是就有些想法了呢?
到目前爲止,我們只是對數據進行了開窗,但是數據在一個窗口內怎麼處理還沒有說,那麼下一章就來說處理函數,以及Flink的狀態編程。
在這次學習中發現的不錯的帖子:https://www.cnblogs.com/rossiXYZ/p/12286407.html