文章目录
- 前言
- 一、窗口(Window)是什么?
-
- (一)窗口的概念
- (二)窗口的分类
-
- 1.按照驱动类型分类
- 2.按照窗口分配数据的规则分类
-
- [(1)滚动窗口(Tumbling Windows)](#(1)滚动窗口(Tumbling Windows))
- [(2)滑动窗口(Sliding Windows)](#(2)滑动窗口(Sliding Windows))
-
- [滑动窗口 vs 滚动窗口(考点对比表)](#滑动窗口 vs 滚动窗口(考点对比表))
- [💥 重点考点总结(答辩/考试必背)](#💥 重点考点总结(答辩/考试必背))
- [💥 重点考点总结(答辩/考试必背)](#💥 重点考点总结(答辩/考试必背))
-
- [(3)会话窗口(Session Windows)](#(3)会话窗口(Session Windows))
- [(4)全局窗口(Global Windows)](#(4)全局窗口(Global Windows))
- 二、Windows窗口API
-
- (一)按键分区和非按键分区
-
- [1.按键分区窗口(Keyed Windows)](#1.按键分区窗口(Keyed Windows))
- [2.非按键分区(Non-Keyed Windows)](#2.非按键分区(Non-Keyed Windows))
- (二)代码中窗口API的调用
- (三)窗口函数
-
- [1.增量聚合函数(ReduceFunction / AggregateFunction)](#1.增量聚合函数(ReduceFunction / AggregateFunction))
- [2.全窗口函数(full window functions)](#2.全窗口函数(full window functions))
- 三、时间语义
- 四、Flink对于延迟数据的处理
-
- [(一)allowedLateness(lateness: Time)](#(一)allowedLateness(lateness: Time))
- (二)侧输出-SideOutput
- 五、总结
-
- (一)窗口的本质
- (二)窗口的分类
-
- 1.按驱动类型分
- 2.按分配规则分
-
- [3.窗口 API 与函数:](#3.窗口 API 与函数:)
- 4.时间语义与水位线:
- 5.延迟数据的处理策略:
前言
-
在批处理系统中,我们可以等待一批数据全部到达后再统一处理,这种方式简单直接。然而,在实时流处理场景下**,数据是源源不断、无穷无尽的------这就是所谓的无界数据流。**
-
面对无界流,我们无法像批处理那样"等所有数据到齐"。那么,如何对实时到达的每一条数据进行有意义的统计呢?答案就是窗口。
-
窗口本质上是对无限数据流进行的一种"切片"操作:我们将无限的数据按照时间或数量划分成有限大小的"数据块",每个数据块就是一个窗口。
-
在 Flink 等流计算框架中,窗口可以被理解为一个"桶",每个到达的数据根据其时间戳或计数被分配到对应的桶中。
-
当窗口的结束条件满足时(例如时间到达窗口边界,或收集到足够数量的数据),我们就对这个桶内的所有数据进行一次计算,输出结果,然后销毁窗口,准备迎接下一个窗口。
-
窗口的引入,使得实时统计成为可能------无论是每分钟的 PV、最近 10 秒的温度滑动平均值,还是用户行为会话分析,都可以通过合适的窗口类型来优雅实现。
-
本文将系统介绍 Flink 中窗口的核心概念、分类、API 使用,以及支撑窗口正确运行的关键机制------时间语义与水位线(Watermark),并探讨如何处理不可避免的延迟数据。掌握这些知识,是构建健壮实时数据应用的基础。
一、窗口(Window)是什么?
(一)窗口的概念
- 1.Flink是一种流式计算引擎,主要是来处理无界数据流的 ,数据源源不断、无穷无尽。想要更加方便高效地处理无界流,一种方式就是将无限数据切割成有限的"数据块"进行处理,这就是所谓的"窗口"(Window)。
- 2.具体实例理解:在Flink中 ,窗口其实并不是一个"框",应该把窗口理解成一个"桶" 。在Flink中,窗口可以把流切割成有限大小的多个"存储桶"(bucket) ;每个数据都会分发到对应的桶中,当到达窗口结束时间时,就对每个桶中收集的数据进行计算处理。

- 注意: Flink中窗口并不是静态准备好的,而是动态创建------当有落在这个窗口区间范围的数据达到时,才创建对应的窗口。另外,这里我们认为到达窗口结束时间时,窗口就触发计算并关闭,事实上"触发计算"和"窗口关闭"两个行为也可以分开,这部分内容我们会在后面详述。
(二)窗口的分类
1.按照驱动类型分类
窗口本身是截取有界数据的一种方式 ,所以窗口一个非常重要的信息其实就是"怎样截取数据"。换句话说,就是以什么标准来开始和结束数据的截取,我们把它叫作窗口的"驱动类型"。
- (1)时间窗口(Time Window)
时间窗口以时间点来定义窗口的开始(start)和结束(end) ,所以截取出的就是某一时间段的数据。到达结束时间时,窗口不再收集数据,触发计算输出结果,并将窗口关闭销毁。所以可以说基本思路就是=="定点发车"。== - (2)记数窗口(Count Window)
计数窗口基于元素的个数来截取数据,到达固定的个数时就触发计算并关闭窗口。每个窗口截取数据的个数,就是窗口的大小。基本思路是=="人齐发车"。==

2.按照窗口分配数据的规则分类
(1)滚动窗口(Tumbling Windows)
滚动窗口有固定的大小 (如下图所示,window size固定),是一种对数据进行**"均匀切片"的划分方式。窗口之间没有重叠,也不会有间隔**,是"首尾相接"的状态。这是最简单的窗口形式,每个数据都会被分配到一个窗口,而且只会属于一个窗口。
滚动窗口应用非常广泛,它可以对每个时间段做聚合统计,很多BI分析指标都可以用它来实现。

滚动窗口可以基于时间定义 ,也可以基于数据个数定义 ;需要的参数只有一个,就是窗口的大小(window size)。
比如:我们可以定义一个长度为1小时的滚动时间窗口,那么每个小时就会进行一次统计;或者定义一个长度为10的滚动计数窗口,就会每10个数进行一次统计。
(2)滑动窗口(Sliding Windows)
- 滑动窗口的大小也是固定 的。但是窗口之间并不是首尾相接的,而是可以"错开"一定的位置。
- 定义滑动窗口的参数 有两个:除去窗口大小(window size)之外,还有一个"滑动步长"(window slide) ,它其实就代表了窗口计算的频率。窗口在结束时间触发计算输出结果,那么滑动步长就代表了计算频率。

- 当滑动步长小于窗口大小时,滑动窗口就会出现重叠,这时数据也可能会被同时分配到多个窗口中。而具体的个数,就由窗口大小和滑动步长的比值(size/slide)来决定。
- 滚动窗口也可以看作是一种特殊的滑动窗口------窗口大小等于滑动步长(size = slide)。滑动窗口适合计算结果更新频率非常高的场景
滑动窗口 vs 滚动窗口(考点对比表)
| 对比维度 | 滚动窗口(Tumbling Window) | 滑动窗口(Sliding Window) | 考点重要性 |
|---|---|---|---|
| 核心区别 | 窗口之间严格不重叠,首尾相接 | 窗口之间可以重叠(当滑动步长 < 窗口长度时) | ⭐⭐⭐⭐⭐ |
| 触发频率 | 等于窗口长度(例如 5s 触发一次) | 等于滑动步长(例如每 5s 触发一次,但窗口长度 10s) | ⭐⭐⭐⭐⭐ |
| 数据重复计算 | 每条数据只属于一个窗口 | 每条数据可能属于多个窗口(重叠部分重复计算) | ⭐⭐⭐⭐ |
| 窗口数量 | 时间段长度 / 窗口长度 | 时间段长度 / 滑动步长(通常会更多) | ⭐⭐⭐ |
| 适用场景 | 周期统计(每分钟 PV、每小时活跃用户) | 实时趋势分析(每 5s 看最近 10s 的滑动平均) | ⭐⭐⭐⭐ |
| 内存/状态开销 | 较小(每个数据只在一个窗口内) | 较大(需保留滑动步长内的状态,可能重复存储) | ⭐⭐⭐ |
| 数学关系 | 滑动窗口的特例(当窗口长度 = 滑动步长) | 滚动窗口的泛化 | ⭐⭐ |
| 典型 API | .window(TumblingProcessingTimeWindows.of(Time.seconds(5))) |
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5))) |
⭐⭐⭐⭐ |
💥 重点考点总结(答辩/考试必背)
- 最核心区别 :滚动窗口不重叠,滑动窗口可以重叠(重叠程度由滑动步长决定)。
- 触发频率公式 :
- 滚动窗口触发频率 = 窗口长度
- 滑动窗口触发频率 = 滑动步长
- 数据重复性 :滑动窗口在重叠部分会重复计算数据,滚动窗口每条数据只计算一次。
- 实际例子 :
- 滚动:每 5 秒统计一次这 5 秒内的数据(干净不重叠)。
- 滑动:每 5 秒统计一次最近 10 秒的数据(窗口之间有 5 秒重叠)。
- 常见问题 :
- 何时用滑动窗口? → 需要平滑趋势、避免窗口边界突变时(如股票滑动平均)。
- 何时用滚动窗口? → 简单的周期性统计,对边界不敏感时。
- 滑动步长大于窗口长度会发生什么? → 会出现数据间隙(部分数据不被任何窗口包含),一般不用。
- 记忆口诀 :
"滚动不重叠,滑动常重叠;滚动触发等窗长,滑动触发看步长;重叠数据重复算,趋势平滑滑动强。"
💥 重点考点总结(答辩/考试必背)
- 最核心区别 :滚动窗口不重叠,滑动窗口可以重叠(重叠程度由滑动步长决定)。
- 触发频率公式 :
- 滚动窗口触发频率 = 窗口长度
- 滑动窗口触发频率 = 滑动步长
- 数据重复性 :滑动窗口在重叠部分会重复计算数据,滚动窗口每条数据只计算一次。
- 实际例子 :
- 滚动:每 5 秒统计一次这 5 秒内的数据(干净不重叠)。
- 滑动:每 5 秒统计一次最近 10 秒的数据(窗口之间有 5 秒重叠)。
- 老师最爱问 :
- 何时用滑动窗口? → 需要平滑趋势、避免窗口边界突变时(如股票滑动平均)。
- 何时用滚动窗口? → 简单的周期性统计,对边界不敏感时。
- 滑动步长大于窗口长度会发生什么? → 会出现数据间隙(部分数据不被任何窗口包含),一般不用。
- 记忆口诀 :
"滚动不重叠,滑动常重叠;滚动触发等窗长,滑动触发看步长;重叠数据重复算,趋势平滑滑动强。"
(3)会话窗口(Session Windows)
- 会话窗口,是基于"会话"(session)对数据进行分组的。会话窗口只能基于时间来定义。
- 会话窗口中,最重要的参数就是会话的超时时间 ,也就是两个会话窗口之间的最小距离。如果相邻两个数据到来的时间间隔(Gap)小于指定的大小(size),那说明还在保持会话,它们就属于同一个窗口 ;如果gap大于size,那么新来的数据就应该属于新的会话窗口,而前一个窗口就应该关闭了。

- 会话窗口的长度不固定,起始和结束时间也是不确定的,各个分区之间窗口没有任何关联。会话窗口之间一定是不会重叠的,而且会留有至少为size的间隔(session gap)。
- 在一些类似保持会话 的场景下,可以使用会话窗口来进行数据的处理统计。
(4)全局窗口(Global Windows)
- "全局窗口",这种窗口全局有效 ,会把相同key的所有数据都分配到同一个窗口中。这种窗口没有结束的时候,默认是不会做触发计算的。如果希望它能对数据进行计算处理,还需要自定义"触发器"(Trigger)。
- 全局窗口没有结束的时间点,所以一般在希望做更加灵活的窗口处理时自定义使用。Flink中的计数窗口(Count Window),底层就是用全局窗口实现的。

二、Windows窗口API
(一)按键分区和非按键分区
- 在定义窗口操作之前,首先需要确定,到底是基于按键分区(Keyed)的数据流KeyedStream来开窗,还是直接在没有按键分区的DataStream上开窗。也就是说,在调用窗口算子之前,是否有keyBy操作。
1.按键分区窗口(Keyed Windows)
- 经过按键分区keyBy操作后,数据流会按照key被分为多条逻辑流(logical streams),这就是KeyedStream。基于KeyedStream进行窗口操作时,窗口计算会在多个并行子任务上同时执行。相同key的数据会被发送到同一个并行子任务,而窗口操作会基于每个key进行单独的处理。所以可以认为,每个key上都定义了一组窗口,各自独立地进行统计计算。
- 在代码实现上,我们需要先对DataStream调用.keyBy()进行按键分区,然后再调用.window()定义窗口。
java
stream.keyBy(...)
.window(...)
2.非按键分区(Non-Keyed Windows)
- 如果没有进行keyBy,那么原始的DataStream就不会分成多条逻辑流。这时窗口逻辑只能在一个任务(task)上执行,就相当于并行度变成了1。
- 在代码中,直接基于DataStream调用.windowAll()定义窗口。
stream.windowAll(...) - 注意:对于非按键分区的窗口操作,手动调大窗口算子的并行度也是无效的,windowAll本身就是一个非并行的操作。
c
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
(二)代码中窗口API的调用
- 窗口操作主要有两个部分:窗口分配器(Window Assigners)和窗口函数(Window Functions)。
java
// Keyed Window 键控流
stream
.keyBy(<key selector>) <- 按照一个Key进行分组
.window (<window assigner>) <- 将数据流中的元素分配到相应的窗口中
.reduce/aggregate/process/apply() <- 窗口函数Window Function
// Non-Keyed Window 非键控流
stream
.windowAll(...) <- 不分组,将数据流中的所有元素分配到相应的窗口中
.reduce/aggregate/process() <- 窗口函数Window Function
- 其中.window()方法需要传入一个窗口分配器,它指明了窗口的类型;窗口函数用来定义窗口具体的处理逻辑。
(三)窗口函数
窗口函数定义了要对窗口中收集的数据做的计算操作,根据处理的方式可以分为两类:增量聚合函数和全窗口函数。
1.增量聚合函数(ReduceFunction / AggregateFunction)
- 窗口将数据收集起来,最基本的处理操作当然就是进行聚合。我们可以每来一个数据就在之前结果上聚合一次,这就是"增量聚合"。
- 常见的增量聚合函数如下:
reduce(reduceFunction)/sum()/min()/max()
aggregate(aggregateFunction)
2.全窗口函数(full window functions)
- 有些场景下,我们要做的计算必须基于全部的数据才有效,这时做增量聚合就没什么意义了;另外,输出的结果有可能要包含上下文中的一些信息(比如窗口的起始时间),这是增量聚合函数做不到的。
- 所以,我们还需要有更丰富的窗口计算方式。窗口操作中的另一大类就是全窗口函数。与增量聚合函数不同,全窗口函数需要先收集窗口中的数据,并在内部缓存起来,等到窗口要输出结果的时候再取出数据进行计算。
- 常见的全窗口函数如下:
apply(windowFunction)
process(processWindowFunction)
java
package chapter06.watermarker;
import bean.OrderInfo;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
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.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows;
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 org.apache.flink.util.OutputTag;
import java.time.Duration;
public class WaterMarkerDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
.setBootstrapServers("niit01:9092")
.setTopics("watermark")
.setStartingOffsets(OffsetsInitializer.latest())
.setValueOnlyDeserializer(new SimpleStringSchema()).build();
DataStreamSource<String> dataStreamSource =
env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "Watermark Strategy");
// 定义测流标签
OutputTag<OrderInfo> sideOutput = new OutputTag<>("SideOutput", TypeInformation.of(OrderInfo.class));
SingleOutputStreamOperator<OrderInfo> map = dataStreamSource.map(new MapFunction<String, OrderInfo>() {
@Override
public OrderInfo map(String json) throws Exception {
return JSON.parseObject(json, OrderInfo.class);
}
});
SingleOutputStreamOperator<String> result = map
.assignTimestampsAndWatermarks(WatermarkStrategy // 设置水印
.<OrderInfo>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<OrderInfo>() {
@Override
public long extractTimestamp(OrderInfo orderInfo, long l) {
return orderInfo.getTimestamp();
}
}))
.keyBy(orderInfo -> orderInfo.getUid())
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(10)) // 设置长期延迟
.sideOutputLateData(sideOutput) // 测流输出
.apply(new WindowFunction<OrderInfo, String, Integer, TimeWindow>() {
@Override
public void apply(Integer key, TimeWindow timeWindow, Iterable<OrderInfo> iterable, Collector<String> collector) throws Exception {
long start = timeWindow.getStart();
long end = timeWindow.getEnd();
String startStr = DateFormatUtils.format(start, "yyyy-MM-dd HH:mm:ss");
String endStr = DateFormatUtils.format(end, "yyyy-MM-dd HH:mm:ss");
int sumMoney = 0;
for (OrderInfo orderInfo : iterable) {
sumMoney += orderInfo.getMoney();
}
collector.collect(startStr + "->" + endStr + "->" + key + "->" + sumMoney);
}
});
result.print();
result.getSideOutput(sideOutput).print("严重迟到的数据");
env.execute();
}
}
三、时间语义
(一)有关概念

1.EventTime(事件时间)
事件(数据)时间:是事件/数据真真正正发生时/产生时的时间
2.ProcessingTime(处理时间)
处理时间:是事件/数据被处理/计算时的系统的时间
| 对比维度 | 事件时间(Event Time) | 处理时间(Processing Time) | 考点重要性 |
|---|---|---|---|
| 定义 | 数据实际发生的时间(业务时间) | 数据到达 Flink 的系统时间 | ⭐⭐⭐ |
| 窗口触发依据 | Watermark 推进 | 系统时钟周期性触发 | ⭐⭐⭐⭐⭐ |
| 能否处理乱序 | ✅ 通过 Watermark 容忍乱序 | ❌ 无法处理 | ⭐⭐⭐⭐⭐ |
| 结果确定性 | ✅ 确定性 | ❌ 依赖机器负载,不稳定 | ⭐⭐⭐⭐ |
| 数据丢失风险 | 低(可配合侧输出兜底) | 高 | ⭐⭐⭐⭐ |
| 实现复杂度 | 高 | 低 | ⭐⭐⭐ |
| 典型场景 | 金融、日志、传感器 | 实时监控、简单告警 | ⭐⭐⭐⭐ |
| 面试必问 | Watermark 原理、迟到策略 | 为什么不稳定 | ⭐⭐⭐⭐⭐ |
3.数据处理系统中的时间语义
- 在实际应用中,事件时间语义会更为常见 。一般情况下,业务日志数据中都会记录数据生成的时间戳(timestamp),它就可以作为事件时间的判断基础。
- 在Flink中,由于处理时间比较简单,早期版本默认的时间语义是处理时间;而考虑到事件时间在实际应用中更为广泛,从Flink1.12版本开始,Flink已经将事件时间作为默认的时间语义了。
(二)水印、水位线(Watermark)
1.为什么需要WaterMark?
- 当flink 以 EventTime 模式处理流数据时,它会根据数据里的时间戳来处理基于时间的算子。但是由于网络、分布式等原因,会导致数据乱序的情况。
- 所谓乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的Event Time顺序排列的。
- 只要使用event time,就必须使用watermark。在上游算子指定,比如:source、map算子后。
- Watermark的核心本质可以理解成一个延迟触发机制。

2.WaterMark是什么?
- Watermark就是给数据额外添加的一列时间戳!
- Watermark = 当前最大的事件时间 - 最大允许的延迟时间(或最大允许的乱序时间)
- 比如,事件时间是10分30秒, 最大允许的延迟时间是2秒,那么水印就是10分28秒
3.Watermark能解决什么问题?

4.水位线生成策略
- 在Flink的DataStream API中,有一个单独用于生成水位线的方法:.
assignTimestampsAndWatermarks(),它主要用来为流中的数据分配时间戳,并生成水位线来指示事件时间。 - 具体使用如下:
java
DataStream<Event> withTimestampsAndWatermarks =
stream.assignTimestampsAndWatermarks(<watermark strategy>);
- 说明:WatermarkStrategy作为参数,这就是所谓的"水位线生成策略" 。WatermarkStrategy是一个接口,该接口中包含了一个"时间戳分配器"
TimestampAssigner和一个"水位线生成器"WatermarkGenerator。
四、Flink对于延迟数据的处理
- 水印(水位线、watermark)机制可以帮助我们在短期延迟下,允许乱序数据的到来。这个机制很好的处理了那些因为网络等情况短期延迟的数据。
- 但是水印机制无法长期的等待下去,因为水印机制简单说就是让窗口一直等在那里,等达到水印时间才会触发计算和关闭窗口。
- 根据具体业务情况,一般水印也就是几秒钟最多几分钟而已。
- 主要的解决办法是给定一个允许延迟的时间,在该时间范围内仍可以接受处理延迟数据。
- 设置允许延迟的时间是通过
allowedLateness(lateness: Time)设置。
(一)allowedLateness(lateness: Time)
- 当我们对流设置窗口后得到的
WindowedSteam对象就可以使用allowedLateness方法。该方法传入一个Time值,设置允许的长期延迟(迟到)的时间。给定允许延迟时间,处理延迟数据
1.情况一
- 未设置
allowedLateness(为0),当watermark满足条件,会触发窗口的 执行 + 关闭窗口。
2.情况二
- 当设置了
allowedLateness,当watermark满足条件后,只会触发窗口的执行,不会触发窗口关闭。也就是,watermark满足条件后会正常触发窗口计算,将已有的数据完成计算。但是,不会关闭窗口。
3.情况三
- 如果在
allowedLateness允许的时间内仍有这个窗口的数据进来,那么每进来一条,会和已经计算过的(被watermark触发的)数据一起再计算一次。
4.总结
- 水印:短期延迟,达到条件后触发计算并且关闭窗口(触发+关闭同时进行)。
- 水印+allowedLateness : 短期延迟+ 等待长期延迟效果, 达到水印条件后,会触发窗口计算,但是不关闭窗口。事件时间延迟达到水印+allowedLateness之和后会关闭窗口。
(二)侧输出-SideOutput
- Flink 通过watermark在短时间内允许了乱序到来的数据。通过延迟数据处理机制,可以处理长期迟到的数据。
- Flink的这两个延迟机制尽量确保了数据不会错过了属于他们的窗口,但是如果真的迟到太久,Flink还有一个机制将这些数据收集起来保存成为一个DataStream,然后,交由开发人员自行处理。这个机制就叫做侧输出机制(Side Output)。
- 侧输出机制:可以将错过水印又错过allowedLateness允许的时间的数据,单独的存放到一个DataStream中,然后开发人员可以自定逻辑对这些超级迟到数据进行处理。
五、总结
本文围绕 Flink 流处理中的窗口机制展开,从基本概念到实际应用进行了较为全面的梳理。回顾全文,核心要点可以概括为以下几个方面:
(一)窗口的本质
窗口是切割无界数据流的手段,通过将无限数据划分为有限的数据块,使得实时聚合、统计成为可能。窗口可以理解为"桶",数据按规则落入桶中,桶满或时间到即触发计算。
(二)窗口的分类
1.按驱动类型分
时间窗口(定点发车)和计数窗口(人齐发车)。
2.按分配规则分
- 滚动窗口(无重叠、首尾相接)、滑动窗口(可有重叠、支持高频更新)、会话窗口(基于活动间隔动态划分)和全局窗口(需配合自定义触发器)。
- 其中滑动窗口与滚动窗口的对比是常见考点,核心区别在于数据是否被重复计算以及触发频率的不同。
3.窗口 API 与函数:
-
按键分区(keyBy)后调用 .window(),非按键分区则使用 .windowAll()。
-
窗口函数分为增量聚合函数(ReduceFunction / AggregateFunction,每来一条数据就更新一次状态,效率高)和全窗口函数(WindowFunction / ProcessWindowFunction,缓存全部数据后再计算,能获取窗口元信息)。
4.时间语义与水位线:
-
事件时间(Event Time)是数据实际发生的时间,能正确处理乱序和延迟,是生产环境的默认选择;处理时间(Processing Time)依赖系统时钟,简单但不稳定。
-
水位线(Watermark) 是事件时间进度的衡量标准,公式为:Watermark = 当前最大事件时间 - 允许的最大乱序时间。它解决了乱序数据导致窗口无法正确触发的问题,是实现事件时间窗口的核心机制。
5.延迟数据的处理策略:
-
短期延迟通过水位线本身的容忍度解决。
-
长期延迟则使用 allowedLateness 让窗口在触发计算后继续保持一段时间,接收迟到的数据并触发多次更新。
-
对于严重超时的数据(既错过了水位线,又超过了 allowedLateness),可以通过侧输出(SideOutput) 将其收集到单独的 DataStream 中,由开发者自定义处理(如存入死信队列或人工干预)。
-
总之,窗口是流处理中连接"无限数据"与"有限计算"的桥梁,而事件时间、水位线和延迟处理机制共同保证了窗口计算的正确性与健壮性。在实际项目中,应根据业务对实时性、准确性、容错性的不同要求,合理选择窗口类型、时间语义和延迟容忍策略。希望本文能帮助读者建立起清晰的 Flink 窗口知识体系,为后续深入流计算开发打下坚实基础。