Flink的視窗處理機制(一)

doublexi發表於2021-12-23

一、為什麼需要 window ?

在流處理應用中,資料是連續不斷的,即資料是沒有邊界的,因此我們不可能等到所有資料都到了才開始處理。當然我們可以每來一個訊息就處理一次,但是有時我們需要做一些聚合類的處理,例如:在過去的1分鐘內有多少使用者點選了我們的網頁。在這種情況下,我們必須定義一個視窗,用來收集最近一分鐘內的資料,並對這個視窗內的資料進行計算。

流上的聚合需要由 window 來劃定範圍,比如 “計算過去的5分鐘” ,或者 “最後100個元素的和” 。

window是一種可以把無限資料流切割為有限資料塊的手段。

Flink 認為 Batch 是 Streaming 的一個特例,所以 Flink 底層引擎是一個流式引擎,在上面實現了流處理和批處理。

而視窗(window)就是從 Streaming 到 Batch 的一個橋樑。

二、視窗的生命週期

視窗的生命週期,就是指視窗從建立、觸發執行、到銷燬的過程。

那麼這個時候需要思考四個問題

1、資料元素是如何分配到對應視窗中的(也就是視窗的分配器)?

2、元素分配到對應視窗之後什麼時候會觸發計算(也就是視窗的觸發器)?

3、在視窗內我們能夠進行什麼樣的操作(也就是視窗內的操作)?

4、當視窗過期後是如何處理的(也就是視窗的銷燬關閉)?

其實這四個問題從大體上可以理解為視窗的整個生命週期過程。接下來我們對每個環節進行講解

建立:當屬於該視窗的第一個元素到達時就會建立該視窗

銷燬:當時間(事件或處理時間)超過視窗的結束時間戳加上使用者指定的允許延遲時間,視窗將被完全刪除。 Flink保證僅刪除基於時間的視窗而不是其他型別的視窗,例如全域性視窗。

例如,使用基於事件時間的視窗策略,每5分鐘建立一個不重疊(或翻滾)的視窗並允許延遲1分鐘,當具有落入該間隔的時間戳的第一個元素到達時,Flink將為12:00到12:05之間的間隔建立一個新視窗,當水位線(watermark)到12:06時間戳時它將刪除它。【這裡同時我們也可以明白watermark的作用】

Trigger觸發器:指定了視窗函式在什麼條件下可被觸發,觸發器還可以決定在建立和刪除視窗之間的任何時間清除視窗的內容。在這種情況下,清除僅限於視窗中的元素,而不是視窗後設資料。這意味著新資料仍然可以新增到該視窗中。

例如:當視窗中的元素個數超過4個時 或者 當水印達到視窗的邊界時―觸發計算

Window的函式:函式裡定義了應用於視窗(Window)內容的計算邏輯

Evictor(驅逐器):將在觸發器觸發之後或者在函式被應用前後,清除視窗中的元素

三、Keyed vs Non-Keyed Windows

在定義視窗之前,首先要指定你的流是否應該被keyBy()分割槽,這個必須要視窗定義前確定。使用 keyBy(...) 後,不同的 key 會被劃分到不同的流裡面,每個流可以被一個單獨的 task 處理。而相同的key將會被分配到同一個keyed Stream,被同一個task處理。

如果 不使用 keyBy ,所有資料會被劃分到一個視窗裡,彙總到一個task處理,並行度是1.

PS:最大並行度=container個數 * 每個container上最大slot數

api呼叫如下:

Keyed Windows

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/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"

Non-Keyed Windows

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/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"

四、Window Assigners 視窗指派器

資料經過控制流的處理之後,無論是keyed Stream還是Non-keyed Stream,兩種控制流都需要指定一個window Assinger,負責將每個傳入的元素分配給一個或多個視窗,有了window Assinger,才會建立出各種形式的window來覆蓋我們所需的各種場景,對我們開發來說不需要關注window本身,只需要關注Window Assinger的分類即可,所以很多關於Flink的視訊都沒有講解控制流的概念,只講了Window的分類。

Flink中的視窗從大體上劃分有以下幾個大類:

第一種是基於時間劃分的視窗,叫TimeWindow。(比如每30秒)

第二種是基於資料數量劃分的視窗,叫CountWindow。(比如每100個元素)

第三種是全域性視窗,不劃分的。

還有就是自定義視窗型別。(通過繼承WindowAssigner類來實現自定義視窗分配器邏輯)

api介紹:

當input的Stream進行keyBy()之後,就會生成一個KeyedStream,而KeyedStream實現了timeWindow()、countWindow()、window()等方法。原始碼如下圖:

clipboard

如果dataStream沒有經過keyBy(),就是Non-keyed Stream,就是原生的dataStream的話,其實它也可以呼叫視窗函式。api原始碼如下:

clipboard

我們發現Non-keyed Stream相比keyed Stream,Window Assigner的呼叫方式上,只是多了個All。

下面先基於常用的KeyedStream來介紹常用的window Assigner

1、TimeWindow

TimeWindow按照時間來生成視窗。每個時間視窗都有一個開始時間和結束時間,表示一個左閉右開的時間段,表示了視窗的區間大小。

(程式設計技巧:可以通過TimeWindow物件的getStart()、getEnd()方法來獲取視窗的開始時間和結束時間的時間戳,也可以通過maxTimestamp() 方法來獲取視窗內的最大時間戳。)

根據不同的業務場景,Time Window 也可以分為三種型別,

分別是滾動視窗(Tumbling Window)、滑動視窗(Sliding Window)和會話視窗(Session Window)

我們知道Flink中的時間型別可以劃分為三種:

1、Event Time:事件時間,即事件產生的時間

2、IngestionTime:攝入時間,事件進入流處理系統的時間,也就是資料進入flink的時間

3、Processing Time:處理時間,訊息被flink計算框架處理的時間

這裡主要考慮事件時間和處理時間,所以上面的每種視窗又可分別基於processing time和event time。


首先,我們來檢視TimeWindow的api,這個視窗指派器需要緊跟在資料流後面。它是KeyedStream下的方法。

方式一:直接使用KeyedStream下的timeWindow()方法。

裡面接一個引數的就表示是滾動時間視窗,接兩個引數的就表示是滑動時間視窗。

在這裡處理的是事件時間還是處理時間,取決於env設定的TimeCharacteristic引數。

env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

呼叫如下:

// Stream of (sensorId, carCnt )
val vehicleCnts: DataStream[(Int, Int)] = ...

// timeWindow後面接一個引數就表示是滾動時間視窗
val tumblingCnts: DataStream[ (Int, Int)] = vehicleCnts 
  // key stream by sensorId
  .keyBy(0)
  // tumbling time window of 1 minute length
  .timeWindow(Time.minutes(1))
  // compute sum over carCnt
  .sum(1)

// timeWindow後面接兩個引數就表示是滑動時間視窗
val slidingCnts: DataStream[ (Int, Int)] = vehicleCnts
  .keyBy(0)
// sliding time window of 1 minute Length and 30 secs trigger interval 
  .timeWindow(Time.minutes(1), Time.seconds(30))
  .sum(1 )

方式二:使用KeyedStream下的window()方法

需要在引數裡指明使用哪種時間視窗型別。

這也是官方文件指定的方式。

支援滾動視窗、滑動視窗、會話視窗和全域性視窗。

inputStream.keyBy()
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    
// window裡面的視窗型別可以換成:
1、TumblingEventTimeWindows()    滾動事件時間視窗
2、TumblingProcessingTimeWindows()    滾動處理時間視窗
3、SlidingEventTimeWindows()          滑動事件時間視窗
4、SlidingProcessingTimeWindows()     滑動處理時間視窗
5、EventTimeSessionWindows()          事件時間會話視窗
6、ProcessingTimeSessionWindows()     處理時間會話視窗
7、GlobalWindows.create()             全域性視窗


1.1、Tumbling Window(滾動視窗)

滾動視窗的概念:

  • 滾動視窗能將資料流切分成不重疊的視窗,每一個事件只能屬於一個視窗
  • 滾動窗具有固定的尺寸,不重疊。

滾動視窗的劃分,可以基於時間戳來進行劃分視窗,也可以基於到來的事件元素數量來劃分視窗。

因為我們這裡考慮的是TimeWindow,所以這裡考慮基於時間戳來進行視窗劃分。

例如,如果您指定了一個大小為5分鐘的滾動視窗,那麼每5分鐘將會啟動一個新視窗,

如下圖:

clipboard

滾動時間視窗api。

方式一:直接使用.timeWindow()

// inputStream進行keyby後,呼叫.timeWindow方法,
// 滾動timeWindow裡面就一個引數,指明每10秒劃分一個時間視窗
keyedStream.timeWindow(Time.seconds(10));

注意:這種方式,如果需要按照處理時間劃分視窗,需要在env指明TimeCharacteristic時間型別。

例如:

// 預設就是EventTime,ProcessingTime需要顯式指定
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)

方式二:使用window()

使用window()的方式,就不需要在env裡單獨指定TimeCharacteristic時間型別,因為在window()的引數裡需要傳入指定的引數。

val input: DataStream[T] = ...

// tumbling event-time windows
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>)

// tumbling processing-time windows
input
    .keyBy(<key selector>)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>)

// 這種方式可以指定視窗的對齊方式,
// daily tumbling event-time windows offset by -8 hours.
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
    .<windowed transformation>(<window function>)

如上段程式碼中最後一個例子展示的那樣,tumbling window assigners包含一個可選的offset引數,我們可以用它來改變視窗的對齊方式。

如果我們指定了一個15分鐘的視窗,那麼每個小時內,每個視窗的開始時間和結束時間為:

[00:00,00:15)

[00:15,00:30)

[00:30,00:45)

[00:45,01:00)

如果我們指定了一個5分鐘的offset,那麼每個視窗的開始時間和結束時間為:

[00:05,00:20)

[00:20,00:35)

[00:35,00:50)

[00:50,01:05)

一個實際的應用場景是,我們可以使用 offset 使我們的時區以0時區為準。比如我們生活在中國,時區是 UTC+08:00,可以指定一個 Time.hour(-8),使時間以0時區為準。


滾動視窗適用場景:

適用場景:適合做每個時間段的聚合計算,BI分析。例如統計某頁面每分鐘點選的pv。

場景1:我們需要統計每一分鐘中使用者購買的商品的總數,需要將使用者的行為事件按每一分鐘進行切分,這種切分被成為翻滾時間視窗(Tumbling Time Window)。


應用案例:

編寫程式碼模擬:

下面程式碼僅僅是模擬,每5秒劃分一個視窗,列印輸出資訊。跟上面購買商品的場景無關。

package com.lagou.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Random;

/**
 * @author doublexi
 * @date 2021/10/30 11:37
 * @description 基於時間的滾動時間視窗
 * 1、獲取流資料來源
 * 2、獲取視窗
 * 3、操作視窗資料
 * 4、輸出視窗資料
 */
public class WindowDemo {
    public static void main(String[] args) throws Exception {
        // 獲取資料來源
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 使用匿名內部類的方式新增自定義資料來源
        DataStreamSource<String> data = env.addSource(new SourceFunction<String>() {
            int count = 0;
            
            // 每1秒產生一個數字,拼接字串作為資料來源事件傳送出去。
            @Override
            public void run(SourceContext<String> ctx) throws Exception {
                while (true) {
                    ctx.collect(count + "號資料來源");
                    count++;
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });

        // 對輸入的流的資料進行轉換封裝
        SingleOutputStreamOperator<Tuple3<String, String, String>> maped = data.map(new MapFunction<String, Tuple3<String, String, String>>() {
            @Override
            public Tuple3<String, String, String> map(String value) throws Exception {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                long l = System.currentTimeMillis();
                String dataTime = sdf.format(l);
                Random random = new Random();
                int randomNum = random.nextInt(5);
                return new Tuple3<>(value, dataTime, String.valueOf(randomNum));
            }
        });
        // 為了增加並行度,進行keyBy聚合操作,相同key資料會進入同一個分割槽,給同一個subtask任務
        KeyedStream<Tuple3<String, String, String>, String> keyByed = maped.keyBy(value -> value.f0);
        
        // 2、獲取視窗
        // 基於時間驅動, 每5s割出一個視窗
        WindowedStream<Tuple3<String, String, String>, String, TimeWindow> timeWindow = keyByed.timeWindow(Time.seconds(5));
        // 基於事件驅動, 每相隔3個事件(即三個相同key的資料), 劃分一個視窗進行計算
        // WindowedStream<Tuple2<String, Integer>, Tuple, GlobalWindow> countWindow = keyedStream.countWindow(3);


        // 3、操作視窗資料
        // apply是視窗的應用函式,即apply裡的函式將應用在此視窗的資料上。
        // 第一個引數Tuple3是視窗輸入進來的資料型別,第二個引數Object是輸出的資料型別,第三個引數String是資料來源中key的資料型別,第四個引數指明當前處理的視窗是什麼型別的視窗
        SingleOutputStreamOperator<String> applyed = timeWindow.apply(new WindowFunction<Tuple3<String, String, String>, String, String, TimeWindow>() {
            // s就是上面一行一行的資料來源,window代表當前視窗,
            // 一個視窗中資料來源可能是相同的,根據keyBy分組的,如果有兩個資料來源相同,就會放入這個input迭代器裡,
            // out將處理結果往外傳送
            @Override
            public void apply(String s, TimeWindow window, Iterable<Tuple3<String, String, String>> input, Collector<String> out) throws Exception {
                Iterator<Tuple3<String, String, String>> iterator = input.iterator();
                // new 一個StringBuilder去做字串拼接
                StringBuilder sb = new StringBuilder();
                while (iterator.hasNext()) {
                    // 這個next就是一個一個Tuple3資料
                    Tuple3<String, String, String> next = iterator.next();
                    sb.append(next.f0 + "..." + next.f1 + "..." + next.f2);
                }
                // 拼接輸出的資訊,
                String s1 = s + "..." + window.getStart() + "..." + sb;
                out.collect(s1);
            }
        });

        applyed.print();
        
        // 轉換運算元都是lazy init的, 最後要顯式呼叫 執行程式
        env.execute();

    }
}

上面timeWindow.apply()方法裡面是使用匿名內部類的方式,實現WindowFunction介面。

我們也可以通過自定義類實現WindowFunction方式都可以。

執行結果:

因為我們將時間視窗設定為5s,所以它是隔一段時間輸出一次。

輸出內容中:

這裡第一個就是s資料來源本身,

第二個就是window.getStart(),視窗的開始時間,

第三個欄位資料就是input裡的Tuple3裡的第一個引數value,資料來源本身,

第四個欄位資料就是處理時間,當時用的system.currentTimemills,

第五個欄位資料就是一個5以內的隨機數

window.getStart()時間相同,表示它是同一個視窗裡的資料。

2021-10-30 12:29:15.295資料處理時間不一樣,因為它是屬於不同的任務槽,它是併發執行的,哪個任務槽先處理完就先輸出哪個。

5> 0號資料來源...1635568150000...0號資料來源...2021-10-30 12:29:14.395...1


6> 1號資料來源...1635568155000...1號資料來源...2021-10-30 12:29:15.295...0
8> 3號資料來源...1635568155000...3號資料來源...2021-10-30 12:29:17.308...2
4> 2號資料來源...1635568155000...2號資料來源...2021-10-30 12:29:16.302...0
1> 4號資料來源...1635568155000...4號資料來源...2021-10-30 12:29:18.315...0
1> 5號資料來源...1635568155000...5號資料來源...2021-10-30 12:29:19.322...1


8> 8號資料來源...1635568160000...8號資料來源...2021-10-30 12:29:22.342...1
2> 6號資料來源...1635568160000...6號資料來源...2021-10-30 12:29:20.329...4
2> 10號資料來源...1635568160000...10號資料來源...2021-10-30 12:29:24.355...0
5> 9號資料來源...1635568160000...9號資料來源...2021-10-30 12:29:23.349...2
4> 7號資料來源...1635568160000...7號資料來源...2021-10-30 12:29:21.334...0


8> 13號資料來源...1635568165000...13號資料來源...2021-10-30 12:29:27.377...4
5> 12號資料來源...1635568165000...12號資料來源...2021-10-30 12:29:26.369...1
6> 14號資料來源...1635568165000...14號資料來源...2021-10-30 12:29:28.384...4
4> 11號資料來源...1635568165000...11號資料來源...2021-10-30 12:29:25.361...0
4> 15號資料來源...1635568165000...15號資料來源...2021-10-30 12:29:29.388...1


4> 18號資料來源...1635568170000...18號資料來源...2021-10-30 12:29:32.306...1
3> 16號資料來源...1635568170000...16號資料來源...2021-10-30 12:29:30.395...3
6> 17號資料來源...1635568170000...17號資料來源...2021-10-30 12:29:31.301...0
3> 20號資料來源...1635568170000...20號資料來源...2021-10-30 12:29:34.320...3
3> 19號資料來源...1635568170000...19號資料來源...2021-10-30 12:29:33.313...0

Process finished with exit code -1


1.2、Sliding Window(滑動視窗)

滑動視窗的概念:

滑動視窗的劃分同滾動一樣,可以基於時間戳來進行劃分視窗,也可以基於到來的事件元素數量來劃分視窗。因為我們這裡考慮的是TimeWindow,所以這裡考慮基於時間戳來進行滑動視窗劃分。


概念:

滑動視窗也是一種比較常見的視窗型別,其特點是在滾動視窗基礎之上增加了視窗滑動時間(Slide Time),且允許視窗資料發生重疊。

當 Windows size 固定之後,視窗並不像滾動視窗按照 Windows Size 向前移動,而是根據設定的 Slide Time 向前滑動。

視窗之間的資料重疊大小根據 Windows size 和 Slide time 決定,

  • 當 Slide time 小於 Windows size便會發生視窗重疊,
  • Slide size 大於 Windows size 就會出現視窗不連續,資料可能不能在任何一個視窗內計算,
  • Slide size 和 Windows size 相等時,Sliding Windows 其實就是Tumbling Windows。


滑動視窗是滾動視窗的更廣義的一種形式,滑動視窗由固定的視窗長度和滑動間隔組成

特點:

  • 視窗長度固定,可以有重疊,可以有空隙。
  • 一個元素可以對應多個視窗,也可以不屬於任意一個視窗,看slide size而定。

比如下圖這樣,設定了一個10分鐘大小的滑動視窗,它的滑動引數(slide)為5分鐘。這樣的話,每5分鐘將會建立一個新的視窗,並且這個視窗中包含了一部分來自上一個視窗的元素。

clipboard

基於時間的滑動視窗

場景: 我們可以每30秒計算一次最近一分鐘使用者購買的商品總數。

基於事件的滑動視窗

場景: 每10個 “相同”元素計算一次最近100個元素的總和.


滑動視窗適用場景:

適用場景:對最近一段時間段內進行統計(如某介面近幾分鐘的失敗呼叫率)

比如:每隔3秒計算最近5秒內,每個基站的日誌數量

每30秒計算一次最近一分鐘使用者購買的商品總數。


滑動時間視窗呼叫api:

也分為timeWindow()和window()兩種呼叫方式。

方式一:直接使用.timeWindow()

// inputStream進行keyby後,呼叫.timeWindow方法,
// 滑動timeWindow裡面比滾動多一個引數,視窗滑動間隔slide time
// 增加了一個Time.seconds(2),表示一個步長,向右滑動2秒後,生成一個新的視窗。
keyedStream.timeWindow(Time.seconds(5), Time.seconds(2));

注意:這種方式,如果需要按照處理時間劃分視窗,也需要在env指明TimeCharacteristic時間型別。

例如:

// 預設就是EventTime,ProcessingTime需要顯式指定
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)

方式二:使用window()

使用window()的方式,就不需要在env裡單獨指定TimeCharacteristic時間型別,因為在window()的引數裡需要傳入指定的引數。

DataStream<T> input = ...;

// sliding event-time windows
input
    .keyBy(<key selector>)
    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .<windowed transformation>(<window function>);

// sliding processing-time windows
input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .<windowed transformation>(<window function>);

// sliding processing-time windows offset by -8 hours
input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
    .<windowed transformation>(<window function>);

同樣,我們可以通過offset引數來為視窗設定偏移量。


應用案例:

這裡根據1.1的滾動時間視窗案例改編,資料來源、計算邏輯都不變,只是單純的增加了一個滑動時間間隔,就變成了滑動時間視窗了。

下面是每5秒劃分一個視窗間隔,滑動間隔為2秒。

keyByed.timeWindow(Time.seconds(5), Time.seconds(2));

意思就是每2秒統計一下最近5秒內的資料情況,我們這裡直接列印輸出了。

完整程式碼如下:

package com.lagou.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Random;

/**
 * @author doublexi
 * @date 2021/10/30 11:37
 * @description 基於時間的滑動時間視窗
 * 1、獲取流資料來源
 * 2、獲取視窗
 * 3、操作視窗資料
 * 4、輸出視窗資料
 */
public class WindowDemoSlide {
    public static void main(String[] args) throws Exception {
        // 獲取資料來源
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.addSource(new SourceFunction<String>() {
            int count = 0;

            @Override
            public void run(SourceContext<String> ctx) throws Exception {
                while (true) {
                    ctx.collect(count + "號資料來源");
                    count++;
                    Thread.sleep(1000);
                }
            }

            @Override
            public void cancel() {

            }
        });
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

        // 2、獲取視窗
        SingleOutputStreamOperator<Tuple3<String, String, String>> maped = data.map(new MapFunction<String, Tuple3<String, String, String>>() {
            @Override
            public Tuple3<String, String, String> map(String value) throws Exception {
                long l = System.currentTimeMillis();
                String dataTime = sdf.format(l);
                Random random = new Random();
                int randomNum = random.nextInt(5);
                return new Tuple3<>(value, dataTime, String.valueOf(randomNum));
            }
        });
        // 為了增加並行度,進行keyBy聚合操作,相同key資料會進入同一個分割槽,給同一個subtask任務
        KeyedStream<Tuple3<String, String, String>, String> keyByed = maped.keyBy(value -> value.f0);
        // 每5s割出一個視窗,並且每2秒向前移動,滑動間隔為2s。
        WindowedStream<Tuple3<String, String, String>, String, TimeWindow> timeWindow = keyByed.timeWindow(Time.seconds(5), Time.seconds(2));

        // 3、操作視窗資料
        // 第一個引數Tuple3是視窗輸入進來的資料型別,第二個引數Object是輸出的資料型別,第三個引數String是資料來源中key的資料型別,第四個引數指明當前處理的視窗是什麼型別的視窗
        SingleOutputStreamOperator<String> applyed = timeWindow.apply(new WindowFunction<Tuple3<String, String, String>, String, String, TimeWindow>() {
            // s就是上面一行一行的資料來源,window代表當前視窗,
            // 一個視窗中資料來源可能是相同的,根據keyBy分組的,如果有兩個資料來源相同,就會放入這個input迭代器裡,
            // out將處理結果往外傳送
            @Override
            public void apply(String s, TimeWindow window, Iterable<Tuple3<String, String, String>> input, Collector<String> out) throws Exception {
                Iterator<Tuple3<String, String, String>> iterator = input.iterator();
                // new 一個StringBuilder去做字串拼接
                StringBuilder sb = new StringBuilder();
                while (iterator.hasNext()) {
                    // 這個next就是一個一個Tuple3資料
                    Tuple3<String, String, String> next = iterator.next();
                    sb.append(next.f0 + "..." + next.f1 + "..." + next.f2);
                }
                // 拼接輸出的資訊,
                String s1 = s + "..." + sdf.format(window.getStart()) + "..." + sdf.format(window.getEnd()) + "..." + sb;
                out.collect(s1);
            }
        });

        applyed.print();
        env.execute();

    }
}

執行結果如下:

6> 1號資料來源...2021-10-30 14:07:54.000...2021-10-30 14:07:59.000...1號資料來源...2021-10-30 14:07:58.695...3
5> 0號資料來源...2021-10-30 14:07:54.000...2021-10-30 14:07:59.000...0號資料來源...2021-10-30 14:07:57.694...4


6> 1號資料來源...2021-10-30 14:07:56.000...2021-10-30 14:08:01.000...1號資料來源...2021-10-30 14:07:58.695...3
4> 2號資料來源...2021-10-30 14:07:56.000...2021-10-30 14:08:01.000...2號資料來源...2021-10-30 14:07:59.700...4
5> 0號資料來源...2021-10-30 14:07:56.000...2021-10-30 14:08:01.000...0號資料來源...2021-10-30 14:07:57.694...4
8> 3號資料來源...2021-10-30 14:07:56.000...2021-10-30 14:08:01.000...3號資料來源...2021-10-30 14:08:00.706...2


4> 2號資料來源...2021-10-30 14:07:58.000...2021-10-30 14:08:03.000...2號資料來源...2021-10-30 14:07:59.700...4
1> 4號資料來源...2021-10-30 14:07:58.000...2021-10-30 14:08:03.000...4號資料來源...2021-10-30 14:08:01.711...3
8> 3號資料來源...2021-10-30 14:07:58.000...2021-10-30 14:08:03.000...3號資料來源...2021-10-30 14:08:00.706...2
6> 1號資料來源...2021-10-30 14:07:58.000...2021-10-30 14:08:03.000...1號資料來源...2021-10-30 14:07:58.695...3
1> 5號資料來源...2021-10-30 14:07:58.000...2021-10-30 14:08:03.000...5號資料來源...2021-10-30 14:08:02.715...4


4> 7號資料來源...2021-10-30 14:08:00.000...2021-10-30 14:08:05.000...7號資料來源...2021-10-30 14:08:04.726...2
1> 5號資料來源...2021-10-30 14:08:00.000...2021-10-30 14:08:05.000...5號資料來源...2021-10-30 14:08:02.715...4
2> 6號資料來源...2021-10-30 14:08:00.000...2021-10-30 14:08:05.000...6號資料來源...2021-10-30 14:08:03.721...0
8> 3號資料來源...2021-10-30 14:08:00.000...2021-10-30 14:08:05.000...3號資料來源...2021-10-30 14:08:00.706...2
1> 4號資料來源...2021-10-30 14:08:00.000...2021-10-30 14:08:05.000...4號資料來源...2021-10-30 14:08:01.711...3


5> 9號資料來源...2021-10-30 14:08:02.000...2021-10-30 14:08:07.000...9號資料來源...2021-10-30 14:08:06.738...4
2> 6號資料來源...2021-10-30 14:08:02.000...2021-10-30 14:08:07.000...6號資料來源...2021-10-30 14:08:03.721...0
4> 7號資料來源...2021-10-30 14:08:02.000...2021-10-30 14:08:07.000...7號資料來源...2021-10-30 14:08:04.726...2
8> 8號資料來源...2021-10-30 14:08:02.000...2021-10-30 14:08:07.000...8號資料來源...2021-10-30 14:08:05.732...1
1> 5號資料來源...2021-10-30 14:08:02.000...2021-10-30 14:08:07.000...5號資料來源...2021-10-30 14:08:02.715...4

觀察結果會發現:

我們這裡相比滾動時間視窗,便於觀察,我們增加了視窗的結束時間的列印。

並且視窗的開始時間與結束時間都不再使用時間戳,使用sdf格式化,轉成了年月日時分秒的格式。

同一個視窗的開始時間都是一樣的,不同視窗之間的滑動間隔,步長為2秒,並且同一個視窗內的時間仍然是5秒。

因為滑動間隔小於視窗大小,我們會發現有些資料會出現在多個視窗上。


1.3、Session Window (會話視窗)

會話視窗的概念:

會話視窗(Session Windows)主要是將某段時間內活躍度較高的資料聚合成一個視窗進行計算,視窗的觸發的條件是 Session Gap,是指在規定的時間內如果沒有資料活躍接入,則認為視窗結束,然後觸發視窗計算結果。

需要注意的是如果資料一直不間斷地進入視窗,也會導致視窗始終不觸發的情況。

與滑動視窗、滾動視窗不同的是,Session Windows 不需要有固定 windows size 和 slide time,只需要定義 session gap,來規定不活躍資料的時間上限即可。

特點:

  • 會話視窗根據會話的間隔來把資料分配到不同的視窗。
  • 會話視窗不重疊,沒有固定的開始時間和結束時間。
  • 與翻滾視窗和滑動視窗相反, 當會話視窗在一段時間內(session gap)沒有接收到元素時會關閉會話視窗。後續的元素將會被分配給新的會話視窗

如下圖所示:

clipboard

會話視窗就是根據上圖中的session gap來切分不同的視窗,當一個視窗在大於session gap時間內沒有接收到資料,視窗就會關閉,所以在這種模式下,視窗的長度是可變的,開始和結束時間也是不確定的,唯獨可以設定定長的session gap.

該類視窗的特點:

  • 時間無對齊
  • 當前系統時間-分組內最後一次的時間如果超時,則進行觸發計算

會話視窗分配器可以直接配置一個靜態常量會話間隔,也可以通過函式來動態指定會話間隔時間。

我們可以設定定長的Session gap,也可以使用SessionWindowTimeGapExtractor動態地確定Session gap的長度。


適用場景:

在這種使用者互動事件流中,我們首先想到的是將事件聚合到會話視窗中(一段使用者持續活躍的週期),由非活躍的間隙分隔開。

場景一:如上圖所示,就是需要計算每個使用者在活躍期間總共購買的商品數量,如果使用者30秒沒有活動則視為會話斷開(假設raw data stream是單個使用者的購買行為流)。

場景二:3秒內如果沒有資料進入,則計算每個基站的日誌數量

場景三:比如音樂 app 聽歌的場景,我們想統計一個使用者在一個獨立的 session 中聽了多久的歌曲(如果超過15分鐘沒聽歌,那麼就是一個新的 session 了)

我們可以用 spark Streaming ,每一個小時進行一次批處理,計算使用者session的資料分佈,但是 spark Streaming 沒有內建對 session 的支援,我們只能手工寫程式碼來維護每個 user 的 session 狀態,裡面仍然會有諸多的問題。

我們使用 flink 來解決這個問題

(1)讀取 kafka 中的資料

(2)基於使用者的 userId,設定 一個 session window 的 gap,在同一個session window 中的資料表示使用者活躍的區間

(3)最後使用一個自定義的 window Function

參考:https://cloud.tencent.com/developer/article/1539537


會話視窗api呼叫:

這裡沒有像timeWindow()類似的直接的api,要通過window方法指定視窗指派器的方式生成sessionWindow。

方式如下:

// 獲取Session視窗
// 這裡沒有像TimeWindow類似的直接的api,要通過window方法指定視窗指派器的方式生成sessionWindow
// 這裡會話視窗間隔為10s
WindowedStream<String, String, TimeWindow> sessionWindow = keyByed.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)));

我們仔細研究TimeWindow的原始碼,會發現,其實TimeWindow的本質也是通過這種方式去生成一個TimeWindow視窗的。

注意:這裡的時間也分為處理時間(ProcessingTime)和事件時間(EventTime)。

clipboard

window方法,也就是將資料流放到WindowedStream裡,裡面包含的都是一些根據key進行分組的資料,

元素是根據windowAssigner來往裡面放的。

clipboard

WindowAssigner就是指派0個或多個window給到元素。我們將哪些元素放到哪個window當中。

視窗指派器是指以怎樣的規則將元素髮給哪個window,哪些規則也就是視窗要包含哪些元素。

並且這些元素還都是根據key分組好的元素。

clipboard

我們會發現withGap就是建立了一個新的SessionWindows的WindowAssigner。

clipboard

參照官網,api呼叫方式總結如下:

主要分EventTime與ProcessingTime,定長gap與不定長gap。

DataStream<T> input = ...;

// event-time session windows with static gap
input
    .keyBy(<key selector>)
    .window(EventTimeSessionWindows.withGap(Time.minutes(10)))
    .<windowed transformation>(<window function>);
    
// event-time session windows with dynamic gap
input
    .keyBy(<key selector>)
    .window(EventTimeSessionWindows.withDynamicGap((element) -> {
        // determine and return session gap
    }))
    .<windowed transformation>(<window function>);
// 或者這種方式也行:
// event-time session windows with dynamic gap
input
    .keyBy(...)
    .window(DynamicEventTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[T] {
      override def extract(element: T): Long = {
        // determine and return session gap
      }
    }))
    .<window function>(...)



// processing-time session windows with static gap
input
    .keyBy(<key selector>)
    .window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
    .<windowed transformation>(<window function>);
    
// processing-time session windows with dynamic gap
input
    .keyBy(<key selector>)
    .window(ProcessingTimeSessionWindows.withDynamicGap((element) -> {
        // determine and return session gap
    }))
    .<windowed transformation>(<window function>);
// 動態的這種類也行
// processing-time session windows with dynamic gap
input
    .keyBy(...)
    .window(DynamicProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[T] {
      override def extract(element: T): Long = {
        // determine and return session gap
      }
    }))
    .<window function>(...)    

如上,固定大小的會話間隔可以通過Time.milliseconds(x),Time.seconds(x),Time.minutes(x)來指定,動態會話間隔通過實現SessionWindowTimeGapExtractor介面來指定。

注意:由於會話視窗沒有固定的開始結束時間,它的計算方法與滾動視窗、滑動視窗有所不同。在一個會話視窗運算元內部會為每一個接收到的元素建立一個新的視窗,如果這些元素之間的時間間隔小於定義的會話視窗間隔,則將阿門合併到一個視窗。為了能夠進行視窗合併,我們需要為會話視窗定義一個Tigger函式和Window Function函式(例如ReduceFunction, AggregateFunction, or ProcessWindowFunction. FoldFunction不能用於合併)。


應用案例:

模擬案例:

這裡資料來源為:通過nc每秒傳送一個數字1,如果10秒內沒有收到數字,則視為會話斷開,統計上個視窗裡的所有數字1,拼接為一個字串。

案例程式碼如下:

package com.lagou.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.ProcessingTimeSessionWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

/**
 * @author doublexi
 * @date 2021/10/30 14:19
 * @description 基於會話的視窗
 */
public class WindowDemoSession {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.socketTextStream("linux121", 7777);
        SingleOutputStreamOperator<String> maped = data.map(new MapFunction<String, String>() {
            @Override
            public String map(String value) throws Exception {
                // 這裡對資料基本沒處理,原模原樣傳出
                return value;
            }
        });

        // 這裡指定根據value自身來進行聚合
        KeyedStream<String, String> keyByed = maped.keyBy(value -> value);
        // 獲取Session視窗
        // 這裡沒有像TimeWindow類似的直接的api,要通過window方法指定視窗指派器的方式生成sessionWindow
        // 這裡是以當前事件處理時間為會話視窗開始時間,間隔為10s,形成一個Session視窗
        WindowedStream<String, String, TimeWindow> sessionWindow = keyByed.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)));
        // 第一個引數為輸入資料的型別,第二個引數為輸出資料的型別,第三個引數為key的資料型別,第四個引數為視窗型別,發現這裡也是時間視窗
        SingleOutputStreamOperator<String> applyed = sessionWindow.apply(new WindowFunction<String, String, String, TimeWindow>() {
            @Override
            public void apply(String s, TimeWindow window, Iterable<String> input, Collector<String> out) throws Exception {
                StringBuilder sb = new StringBuilder();
                for (String str : input) {
                    sb.append(str);
                }
                out.collect(sb.toString());
            }
        });

        applyed.print();
        env.execute();
    }
}

啟動nc:輸入資料來源

[root@linux121 ~]# nc -lp 7777
1
1
1

執行flink程式:

資料來源輸入完畢後,等待10s,也就是等待結束這次會話,然後就會看到SessionWindow觸發執行了,列印結果,輸出結果如下:

4> 111

如果在這個session gap內,也就是連續10秒,都沒有接收到新元素,則會關閉上一個視窗,觸發視窗計算。


2、CountWindow (計數視窗)

CountWindow是根據到來的元素的個數來生成視窗的。與時間無關。

CountWindow也分滾動視窗(Tumbling Window)和滑動視窗(Sliding Window)

這裡是根據事件數量來劃分的,所以也可以稱為滾動計數視窗,和滑動計數視窗。


CountWindow沒有像時間視窗那樣豐富的api呼叫。

這裡主要就是使用.countWindow()這一個api,根據引數的不同來設定不同的指派器。


2.1、Tumbling Window(滾動計數視窗)

滾動視窗的概念:

  • 滾動視窗能將資料流切分成不重疊的視窗,每一個事件只能屬於一個視窗
  • 滾動窗具有固定的尺寸,不重疊。

我們這裡是基於元素數量來劃分的。

clipboard

滾動計數視窗的api:

// Stream of (userId, buyCnts)
val buyCnts: DataStream[(Int, Int)] = ...

val tumblingCnts: DataStream[(Int, Int)] = buyCnts
  // key stream by sensorId
  .keyBy(0)
  // tumbling count window of 100 elements size
  .countWindow(100)
  // compute the buyCnt sum 
  .sum(1)

適用場景:

當我們想要每100個使用者購買行為事件統計購買總數,那麼每當視窗中填滿100個元素了,就會對視窗進行計算,這種視窗我們稱之為翻滾計數視窗(Tumbling Count Window)

單詞每出現三次統計一次,統計最近三次的資料?


應用案例:

這是一個模擬的案例。

輸入資料來源是通過nc進行輸入資料的,通過socketTextStream監聽nc的資料來源。

nc上會輸入一些數字,當接收到3個相同的數字之後,就會觸發window關閉,開始window的計算。

這裡的視窗函式主要是將視窗中的資料來源進行拼接列印輸出。

程式碼如下:

基於事件(資料來源數量)的滾動計數視窗:

package com.lagou.window;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.windows.GlobalWindow;
import org.apache.flink.util.Collector;

import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Random;

/**
 * @author doublexi
 * @date 2021/10/30 13:36
 * @description 基於事件(資料來源數量)的滾動視窗
 */
public class WindowDemoCount {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> data = env.socketTextStream("linux121", 7777);
        // 2、獲取視窗
        SingleOutputStreamOperator<Tuple3<String, String, String>> maped = data.map(new MapFunction<String, Tuple3<String, String, String>>() {
            @Override
            public Tuple3<String, String, String> map(String value) throws Exception {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                long l = System.currentTimeMillis();
                String dataTime = sdf.format(l);
                Random random = new Random();
                int randomNum = random.nextInt(5);
                return new Tuple3<>(value, dataTime, String.valueOf(randomNum));
            }
        });
        // 為了增加並行度,進行keyBy聚合操作,相同key資料會進入同一個分割槽,給同一個subtask任務
        KeyedStream<Tuple3<String, String, String>, String> keyByed = maped.keyBy(value -> value.f0);
        // 根據事件數量去劃分視窗,每3個資料來源劃分為一個視窗
        WindowedStream<Tuple3<String, String, String>, String, GlobalWindow> countWindow = keyByed.countWindow(3);

        // 3、操作視窗資料
        // 第一個引數Tuple3是視窗輸入進來的資料型別,第二個引數Object是輸出的資料型別,第三個引數String是資料來源中key的資料型別,第四個引數指明當前處理的視窗是什麼型別的視窗,這裡是GlobalWindow
        // 這裡的GlobalWindow沒有太多的操作介面,無法獲取window相關資訊,所以我們就不拿了
        SingleOutputStreamOperator<String> applyed = countWindow.apply(new WindowFunction<Tuple3<String, String, String>, String, String, GlobalWindow>() {
            @Override
            public void apply(String s, GlobalWindow window, Iterable<Tuple3<String, String, String>> input, Collector<String> out) throws Exception {
                Iterator<Tuple3<String, String, String>> iterator = input.iterator();
                StringBuilder sb = new StringBuilder();
                while (iterator.hasNext()) {
                    Tuple3<String, String, String> next = iterator.next();
                    sb.append(next.f0 + ".." + next.f1 + ".." + next.f2);
                }
                out.collect(sb.toString());
            }
        });

        applyed.print();
        env.execute();
    }
}

啟動nc:

[root@linux121 ~]# nc -lp 7777

執行程式,觀察輸出:

在nc上輸入資料

[root@linux121 ~]# nc -lp 7777
1
2
3
4
5
6
1
1

程式輸出結果:

4> 1..2021-10-30 13:50:19.178..11..2021-10-30 13:50:28.126..31..2021-10-30 13:50:28.729..2

我們發現輸入123456後,都沒有輸出,直到遇上了第三個1,才有一個輸出結果。那是因為這是根據事件的滾動視窗,我們上面設定了3個資料來源才會劃分一個視窗。

上面的keyBy是將相同的key的資料來源交給同一個任務槽去執行。

視窗機制裡呼叫了這個keyBy,相同的key就會呼叫到相同的槽,同一個槽裡又進行了countWindow操作,就是在這一個槽裡開啟了視窗。

因為進行了keyBy分組,就會把123456分發到不同的任務槽裡,每一個數字都處於單獨的任務槽。

1任務槽裡感知到了有3個資料來源後,3個1,就會去觸發執行window裡的操作,就會列印。


2.2、Sliding Window(滑動計數視窗)

滑動視窗的概念:

因為我們這裡考慮的是基於元素的數量來進行滑動視窗劃分。

概念:

滑動視窗也是一種比較常見的視窗型別,其特點是在滾動視窗基礎之上增加了視窗滑動間隔(Slide size),且允許視窗資料發生重疊。

當 Windows size 固定之後,視窗並不像滾動視窗按照 Windows Size 向前移動,而是根據設定的 Slide size 向前滑動。

視窗之間的資料重疊大小根據 Windows size 和 Slide size 決定,

  • 當 Slide size 小於 Windows size便會發生視窗重疊,
  • Slide size 大於 Windows size 就會出現視窗不連續,資料可能不能在任何一個視窗內計算,
  • Slide size 和 Windows size 相等時,Sliding Windows 其實就是Tumbling Windows。

如下圖:

clipboard

滑動計數視窗的適用場景:(關鍵詞:最近)

例如計算每10個元素計算一次最近100個元素的總和,

每隔5s計算一下最近10s的資料


滑動計數視窗的api:

val slidingCnts: DataStream[(Int, Int)] = vehicleCnts
  .keyBy(0)
  // sliding count window of 100 elements size and 10 elements trigger interval
  .countWindow(100, 10)
  .sum(1)


應用案例:

基於事件的滑動計數視窗:

也很簡單,在之前滾動計數視窗程式碼的基礎上稍加改動即可。

# 根據事件源數量來設定視窗,這裡設定步長為1
keyByed.countWindow(3, 1);

開啟一個nc:

[root@linux121 ~]# nc -lp 7777
1
1
1
1

啟動程式,檢視執行結果:

4> 1..2021-10-30 14:14:25.243..2
4> 1..2021-10-30 14:14:25.243..21..2021-10-30 14:14:26.547..3
4> 1..2021-10-30 14:14:25.243..21..2021-10-30 14:14:26.547..31..2021-10-30 14:14:27.956..1
4> 1..2021-10-30 14:14:26.547..31..2021-10-30 14:14:27.956..11..2021-10-30 14:14:29.364..4

這裡我們發現,每輸入一個1就會輸出一條資料,

因為它的步長為1,來一個元素後就會向右滑動,形成一個新的視窗。


3、GlobalWindows (全域性視窗)

概念介紹:

全域性視窗分配器會將具有相同key值的所有元素分配在同一個視窗。這種視窗模式下需要我們設定一個自定義的Trigger,否則將不會執行任何計算,這是因為全域性視窗中沒有一個可以處理聚合元素的自然末端。所有相同keyed的元素分配到一個視窗裡,這種視窗很少使用。

clipboard

適用場景:

全域性視窗的應用場景幾乎是沒有的。


全域性視窗的api呼叫:

val input: DataStream[T] = ...

input
    .keyBy(<key selector>)
    .window(GlobalWindows.create())
    .<windowed transformation>(<window function>)

應用案例:

引用自:https://blog.csdn.net/weixin_45764675/article/details/104818931

package com.baizhi.jsy.windowProcessTime
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.windowing.assigners.{GlobalWindows, ProcessingTimeSessionWindows, SlidingProcessingTimeWindows, TumblingProcessingTimeWindows}
import org.apache.flink.streaming.api.windowing.triggers.CountTrigger
import org.apache.flink.streaming.api.windowing.windows.{GlobalWindow, TimeWindow}
import org.apache.flink.util.Collector
object FlinkWindowProcessGlobal   {
  def main(args: Array[String]): Unit = {
    //1.建立流計算執⾏行行環境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    //2.建立DataStream - 細化
    val text = env.socketTextStream("Centos",9999)
    //3.執⾏行行DataStream的轉換算⼦
    val counts = text.flatMap(line=>line.split("\\s+"))
      .map(word=>(word,1))
      .keyBy(word=>(word._1))
      .window(GlobalWindows.create())
      .trigger(CountTrigger.of(4))
      .apply(new UserDefineGlobalWindowFunction)
      .print()
    //5.執⾏行行流計算任務
    env.execute("Tumbling Window Stream WordCount")
  }
}
class UserDefineGlobalWindowFunction extends WindowFunction[(String,Int),(String,Int),String,GlobalWindow]{
  override def apply(key: String,
                     window: GlobalWindow,
                     input: Iterable[(String, Int)],
                     out: Collector[(String, Int)]): Unit = {
    val sum = input.map(_._2).sum
    out.collect((s"${key}",sum))
  }
}

輸出結果:

clipboard

GlobalWindow與GlobalWindows的區別:

注意:直接使用GlobalWindows指派器的場景很少,幾乎沒有。但是我們卻經常在視窗實現函式裡看到GlobalWindow。經常容易看混淆。注意,它們是不一樣的。

GlobalWindow是一種視窗型別,GlobalWindows是一種視窗指派器。

GlobalWindow:

首先,GlobalWindow繼承自Window,它是一種視窗型別。

clipboard

同樣繼承自Window的有GlobalWindow和TimeWindow

clipboard

GlobalWindow與TimeWindow它們都繼承了父類的maxTimeStamp()方法。

它的maxTimestamp方法與TimeWindow不同,TimeWindow有start和end屬性,其maxTimestamp方法返回的是end-1;而GlobalWindow的maxTimestamp方法返回的是Long.MAX_VALUE;GlobalWindow定義了自己的Serializer

GlobalWindows

GlobalWindows是一種視窗指派器。

clipboard

  • GlobalWindows繼承了WindowAssigner,key型別為Object,視窗型別為GlobalWindow
  • GlobalWindows的assignWindows方法返回的是GlobalWindow;getDefaultTrigger方法返回的是NeverTrigger;getWindowSerializer返回的是GlobalWindow.Serializer();isEventTime返回的為false
  • NeverTrigger繼承了Trigger,其onElement、onProcessingTime、onProcessingTime返回的TriggerResult均為TriggerResult.CONTINUE;該行為就是不做任何觸發操作;如果需要觸發操作,則需要在定義window操作時設定自定義的trigger,覆蓋GlobalWindows預設的NeverTrigger

4、Non-keyed Window

當你的stream過來之後,第一件事需要明確的是你的stream需要keyed或者不需要。這個必須要視窗定義前確定。使用keyBy(...)將會把你的無盡的stream切割成邏輯的keyed stream。比如 keyBy(...)沒有被呼叫,你的stream將不會keyed。

在已經keyed stream中,你寫進來的事件任意屬性attribute可以使用key。由於使用了keyed stream可以允許你的windowd 計算在並行的多工的模式下執行,每一個邏輯的keyed stream可以相互獨立的執行而相互沒有影響。所有具有相同key的元素會被髮射到相同的並行任務上執行。

如果在non-keyed streams中,你原有的stream不會分割成不同的邏輯stream並且所有的window邏輯只會執行在一個單獨的任務上使用併發度為1。(也就說所有的資料會彙總到一個task上執行)

對於KeyedStream,我們直接按照上面1、2、3的方式去呼叫api就可以了。

注意:

1、Non-keyed Stream都有windowAll()視窗函式

當一個dataStream經過keyBy()之後,就會形成一個KeyedStream,keyedStream後面可以接著呼叫視窗等函式。api類似如下:

clipboard

裡面就是我們上面1、2、3的方式去使用視窗指派器。

如果dataStream沒有經過keyBy(),就是Non-keyed Stream,就是原生的dataStream的話,其實它也可以呼叫視窗函式。api如下:

clipboard

我們發現Non-keyed Stream相比keyed Stream,Window Assigner的呼叫方式上,只是多了個All。

因為KeyedStream是並行任務,根據key的不同,會有不同的task在並行執行。

相同的key的元素會劃分到同一個task上執行。

而Non-keyed Stream不會劃分,只有一個單獨的任務,並行度為1,所有的資料會彙總到一個task上執行,所以Non-keyed Stream的視窗api都是帶All的,因為它們要處理所有的資料元素。

注意:這裡和KeyedStream後的GlobalWindow是不一樣的,前者是對分完區後,同一個task上的資料的global。而後者Non-keyed Stream是不分割槽的,針對所有的元素。


2、Non-keyed Stream也可以劃分為滾動視窗、滑動視窗。

Non-keyed Stream上也有timeWindowAll、countWindowAll、windowAll的方法呼叫。

它們也可以實現滾動時間視窗、滑動時間視窗、滾動計數視窗、滑動計數視窗,以及自己指定視窗指派器。不同的是,它是非並行的,所有的元素都會經過同一個運算元。

原始碼如下:

clipboard

視窗指派器總結如下圖:

clipboard

參考引用:

官方文件:https://nightlies.apache.org/flink/flink-docs-release-1.14/docs/dev/datastream/operators/windows/

https://segmentfault.com/a/1190000022106275

https://blog.csdn.net/qq_28680977/article/details/113531672

相關文章