在讲窗口之前,我们回顾下Flink中的数据分类:有界数据流和无界数据流。

     无界数据流:指的是一旦开始生成后就会持续不断的产生新的数据,即数据没有时间边界,这种类型的数据一般适用于做ETL

    有界数据流:指的是输入的数据有始有终,一般这种类型的数据用于批处理,如统计过去一分钟的pv或者uv等类似聚合类操作。

    Flink又是实时流技术,那么如何支持有界数据流的聚合操作呢?这个时候就有了窗口的概念。

      窗口的作用就是为了周期性的获取数据,即把传入的无界数据流在逻辑上划分多个buckets,所以可以把窗口看作是从流到批的一个桥梁。

flink window 明细数据 flink 窗口数据存储在哪里_flink

如上图所示,在一个无界的数据流上,我们通过指定窗口各种属性来实现有界流的处理。因为有了窗口,使得flink成为流批一体的潮流大数据技术。

窗口生命周期

     通过以上的内容,我们应该知道了窗口的作用(主要是为了解决什么样的问题)。那么这个时候需要思考四个问题

  1. 数据元素是如何分配到对应窗口中的(也就是窗口的分配器)?
  2. 元素分配到对应窗口之后什么时候会触发计算(也就是窗口的触发器)?
  3. 在窗口内我们能够进行什么样的操作(也就是窗口内的操作)?
  4. 当窗口过期后是如何处理的(也就是窗口的销毁关闭)?

其实这四个问题从大体上可以理解为窗口的整个生命周期过程。接下来我们对每个环节进行讲解

窗口分配器

    在开始梳理窗口分配过程之前,我们应该先知道Flink中的窗口从大体上划分有2种类型:

根据时间划分窗口,也就是TimeWindow,按照时间来生成窗口。每个时间窗口都有一个开始时间和结束时间,表示一个左闭右开的时间段。根据时间窗口再进一步进行划分,有以下几种窗口分配类型:

  1. 滚动窗口(Tumbling Window)
  2. 滑动窗口(Sliding Window)
  3. 会话窗口(Session Window)

根据数据划分窗口,也就是GlobalWindow(CountWindow),根据数据条数来生成一个窗口,和时间无关。

由于基于数据条数来划分窗口是比较简单的,这里不再细说。接下来将针对时间窗口(实际生产中也是常用的)来进行讲述。

在讲述时间窗口之前,需要先了解一下在Flink中,关于时间又分为三种:

  1. Event Time:即事件产生的时间
  2. IngestionTime:即进入系统的时间,也就是数据进行flink的时间
  3. Processing Time:即数据被Operator算子处理的时间
    我们看下图,可以清晰的看出3种时间的出处。

flink window 明细数据 flink 窗口数据存储在哪里_数据_02

滚动窗口

    滚动窗口分配器会把每个元素分配到一个指定窗口大小的窗口中,且每个窗口之间没有重叠。例如当指定大小为5分钟的窗口,那么就会每5分钟启动一个新的窗口,如下图所示:

flink window 明细数据 flink 窗口数据存储在哪里_数据_03

该类窗口的特点:

时间对齐,默认情况下时间窗口会做一个对齐,比如设置一个一小时的窗口,那么窗口的起止时间是[0:00:00.000 - 0:59:59.999)

1.窗口长度固定

2.窗口没有重叠

时间间隔可以通过使用一个指定Time.milliseconds(x),Time.seconds(x), Time.minutes(x),Time.days(x)等等

适用场景:对最近一段时间段内进行统计(如某接口近几分钟的失败调用率)

滑动窗口

     滑动窗口分配器将元素分配到固定长度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数(size)来配置,另一个窗口滑动参数(slide)控制滑动窗口开始的频率。滑动窗口如果滑动参数小于窗口大小的话,窗口是可以重叠的,在这种情况下,元素会被分配到多个窗口下。例如,可以设置一个大小为10分钟的窗口,每5分钟滑动一次,那么每隔5分钟就会得到一个窗口,其中包含过去10分钟内到达的事件,如下图所示。

flink window 明细数据 flink 窗口数据存储在哪里_flink_04

该类窗口的特点:时间可以对齐、窗口长度固定、有重叠

适用场景:对最近一段时间段内进行统计(如某接口近几分钟的失败调用率)

会话窗口

     会话窗口由一系列事件组合一个指定时间长度的timeout间隙组成,类似于web应用的session,也就是一段时间没有接收到新数据就会生成新的窗口。

     session窗口分配器通过session活动来对元素进行分组,session窗口和滑动窗口和滚动窗口相比,不会有重叠和固定的开始时间和结束时间的情况。当它在一个固定的时间周期内不再接收元素,即非活动间隔产生,那个窗口就会关闭。

     一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的session关闭并且后续的元素将被分配到新的session窗口中去

     会话窗口就是根据上图中的session gap来切分不同的窗口,当一个窗口在大于session gap时间内没有接收到数据,窗口就会关闭,所以在这种模式下,窗口的长度是可变的,开始和结束时间也是不确定的,唯独可以设置定长的session gap.

flink window 明细数据 flink 窗口数据存储在哪里_数据_05

该类窗口的特点:

时间无对齐

当前系统时间-分组内最后一次的时间如果超时,则进行触发计算

全局窗口函数:

全局窗口分配程序将具有相同键的所有元素分配给同一个全局窗口。只有当您还指定了自定义触发器时,这个窗口模式才有用。否则将不会执行任何计算,因为全局窗口没有一个我们可以处理聚合元素的自然终点。

滚动窗口示例如下:

// 创建流处理的执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
//2.使用StreamExecutionEnvironment创建DataStream
// 接收一个socket文本流
DataStreamSource<String> lines = env.socketTextStream("localhost",8888);

// Transformation(s) 对数据进行转换处理统计,先分词,再按照word进行分组,最后进行聚合统计
DataStream<Tuple2<String, Integer>> windowCount = lines.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
    public void flatMap(String line, Collector<Tuple2<String, Integer>> collector) throws Exception {
        String[] words = line.split(" ");
        for (String word : words) {
            //将每个单词与 1 组合,形成一个元组
            Tuple2<String, Integer> tp = Tuple2.of(word, 1);
            //将组成的Tuple放入到 Collector 集合,并输出
            collector.collect(tp);
        }
    }
});
// 滚动窗口(Tumbling Windows)
//进行分组聚合(keyBy:将key相同的分到一个组中) //定义一个1分钟的翻滚窗口,每分钟统计一次
DataStream<Tuple2<String, Integer>> windowStream = windowCount.keyBy(0)
        .timeWindow(Time.minutes(1))
        .sum(1);

// 调用Sink (Sink必须调用)
windowStream.print("windows: ").setParallelism(1);
//timePoint+=30;
//启动(这个异常不建议try...catch... 捕获,因为它会抛给上层flink,flink根据异常来做相应的重启策略等处理)
try {
    env.execute("StreamWordCount");
} catch (Exception e) {
    e.printStackTrace();
}

执行结果: 

hello world

hello flink

hello spark

第一分钟:

windows: > (world,1)

windows: > (flink,1)

windows: > (hello,2)

第二分钟:

hello spark

windows: > (hello,1)

windows: > (spark,1)

滚动窗口:

DataStream<Tuple2<String, Integer>> sumed = windowCount.keyBy(0)
                .timeWindow(Time.minutes(1), Time.seconds(30))
                .sum(1);

第一个30秒输入

hello world

hello flink

第二个30秒输入

hello spark

flink window 明细数据 flink 窗口数据存储在哪里_Time_06

sessionWindows

SingleOutputStreamOperator<Tuple3<String, Long, Integer>> windowStream =
                textKeyStream.window(EventTimeSessionWindows.withGap(Time.milliseconds(5000L))).sum(2);