01 TimeWindow:Flink 中最常用的窗口类型
前面几篇讲了 Flink Window 的分类、Global Window 和 Keyed Window。实际生产中,用得最多的还是 TimeWindow------基于时间的窗口,包括滚动时间窗口和滑动时间窗口。
为什么 TimeWindow 用得最多?因为大部分实时统计场景都是按时间维度的:每分钟的 PV、每小时的销售额、每天的订单量。这些场景天然适合用时间窗口。
但 TimeWindow 也不是简单的"按时间切分"------它涉及三种时间语义的选择、窗口对齐 offset 的配置、事件时间与 Watermark 的配合、滑动窗口的状态倍数等一系列细节。搞不懂这些细节,上线后就会遇到"窗口不触发""结果不准""内存爆炸"等各种问题。
这篇文章把 TimeWindow 从三种时间语义、滚动与滑动窗口、窗口对齐 offset、内部结构与触发机制,到完整代码实现、时间语义选择、生产最佳实践,一次讲透。
02 三种时间语义:事件时间、处理时间、摄入时间

TimeWindow 的第一个关键决策是:用哪种时间语义?Flink 提供三种时间语义,各有特点:
事件时间(EventTime)
事件时间是事件实际发生的时间,由事件本身携带(如日志中的时间戳字段)。
- 特点:结果可重现,不受系统负载和数据延迟影响。同一份数据,不管什么时候跑、在什么机器上跑,结果都一样。
- 需要 :分配时间戳(
assignTimestampsAndWatermarks)+ Watermark 推进。 - 适用:生产环境推荐,对结果准确性要求高的场景(统计报表、财务对账、数据分析)。
- 窗口类 :
TumblingEventTimeWindows、SlidingEventTimeWindows。
处理时间(ProcessingTime)
处理时间是算子处理该事件时的系统时间,由机器时钟决定。
- 特点:延迟最低,不需要 Watermark,到点就触发。但结果不可重现,系统负载高时窗口内数据可能不完整。
- 需要:不需要分配时间戳,不需要 Watermark。
- 适用:对实时性要求极高、可以接受结果不可重现的场景(实时监控告警、简单的实时大屏)。
- 窗口类 :
TumblingProcessingTimeWindows、SlidingProcessingTimeWindows。
摄入时间(IngestionTime)
摄入时间是事件进入 Flink Source 算子的时间,由 Source 机器时钟决定。
- 特点:介于事件时间和处理时间之间,结果相对可重现(比处理时间稳定),但比事件时间差。
- 需要:Source 自动分配时间戳 + 自动生成 Watermark,用户不需要手动配置。
- 适用:事件没有时间戳,但需要比处理时间更稳定的结果(很少用,大部分场景要么用事件时间要么用处理时间)。
- 窗口类 :
TumblingIngestionTimeWindows、SlidingIngestionTimeWindows。
三种时间语义对比
| 对比项 | 事件时间 EventTime | 处理时间 ProcessingTime | 摄入时间 IngestionTime |
|---|---|---|---|
| 时间来源 | 事件携带的时间戳 | 算子系统时间 | Source 系统时间 |
| 结果可重现 | ✅ 是 | ❌ 否 | ⚠️ 相对 |
| 延迟 | 较高(等 Watermark) | 最低 | 中等 |
| 需要 Watermark | ✅ 是 | ❌ 否 | ✅ 自动生成 |
| 生产推荐 | ⭐⭐⭐ 推荐 | ⭐⭐ 特定场景 | ⭐ 很少用 |
生产建议:90% 以上的场景用事件时间。只有对延迟要求极高(如监控告警,差几秒都不行)且可以接受结果不精确的场景才用处理时间。摄入时间基本不用,知道有这个东西就行。
03 滚动时间窗口详解及代码实现
滚动时间窗口是最基础的 TimeWindow,窗口大小固定,窗口之间无缝衔接,每个元素只属于一个窗口。
完整代码:每分钟订单量统计(事件时间)
java
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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;
import java.time.Duration;
public class TumblingEventTimeExample {
public static class OrderEvent {
private String orderId;
private String productId;
private double amount;
private long timestamp;
public OrderEvent(String orderId, String productId, double amount, long timestamp) {
this.orderId = orderId; this.productId = productId;
this.amount = amount; this.timestamp = timestamp;
}
public String getProductId() { return productId; }
public double getAmount() { return amount; }
public long getTimestamp() { return timestamp; }
}
public static class OrderResult {
private String productId;
private long windowStart;
private long windowEnd;
private long orderCount;
private double totalAmount;
public OrderResult(String productId, long ws, long we, long cnt, double amt) {
this.productId = productId; this.windowStart = ws; this.windowEnd = we;
this.orderCount = cnt; this.totalAmount = amt;
}
@Override
public String toString() {
return String.format("商品=%s, 窗口[%d~%d), 订单数=%d, 销售额=%.2f",
productId, windowStart, windowEnd, orderCount, totalAmount);
}
}
// 累加器
public static class OrderAcc {
long count = 0;
double amount = 0;
}
// 增量聚合
public static class OrderAggregator implements AggregateFunction<OrderEvent, OrderAcc, OrderResult> {
@Override
public OrderAcc createAccumulator() { return new OrderAcc(); }
@Override
public OrderAcc add(OrderEvent event, OrderAcc acc) {
acc.count++;
acc.amount += event.getAmount();
return acc;
}
@Override
public OrderResult getResult(OrderAcc acc) {
return new OrderResult(null, 0, 0, acc.count, acc.amount);
}
@Override
public OrderAcc merge(OrderAcc a, OrderAcc b) {
a.count += b.count; a.amount += b.amount; return a;
}
}
// 全量窗口函数:补全 productId 和窗口信息
public static class OrderWindowFunction extends ProcessWindowFunction<OrderResult, OrderResult, String, TimeWindow> {
@Override
public void process(String productId, Context context, Iterable<OrderResult> elements,
Collector<OrderResult> out) {
OrderResult result = elements.iterator().next();
out.collect(new OrderResult(productId, context.window().getStart(),
context.window().getEnd(), result.orderCount, result.totalAmount));
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<OrderEvent> source = env.fromElements(
new OrderEvent("O001", "P001", 99.0, 1000L),
new OrderEvent("O002", "P001", 199.0, 2000L),
new OrderEvent("O003", "P002", 299.0, 3000L),
new OrderEvent("O004", "P001", 99.0, 61000L),
new OrderEvent("O005", "P002", 299.0, 62000L)
);
// 分配时间戳和 Watermark(允许 5 秒乱序)
DataStream<OrderEvent> withTimestamps = source.assignTimestampsAndWatermarks(
WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, rt) -> event.getTimestamp())
);
// 滚动事件时间窗口:1 分钟
DataStream<OrderResult> result = withTimestamps
.keyBy(OrderEvent::getProductId)
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.aggregate(new OrderAggregator(), new OrderWindowFunction());
result.print();
env.execute("Tumbling EventTime Example");
}
}
关键点说明
TumblingEventTimeWindows.of(Time.minutes(1)):创建 1 分钟的滚动事件时间窗口- 事件时间窗口必须分配时间戳和 Watermark,否则窗口永远不触发
forBoundedOutOfOrderness(Duration.ofSeconds(5)):允许 5 秒乱序,Watermark = 当前最大事件时间 - 5 秒- 窗口是左闭右开区间
[start, end),时间戳等于 end 的元素属于下一个窗口 - 用
aggregate(aggFunc, windowFunc)组合增量聚合和全量窗口函数,既省内存又能拿到窗口信息
处理时间版本(对比)
如果用处理时间,代码更简单,不需要 Watermark:
java
// 滚动处理时间窗口:1 分钟
DataStream<OrderResult> result = source
.keyBy(OrderEvent::getProductId)
.window(TumblingProcessingTimeWindows.of(Time.minutes(1)))
.aggregate(new OrderAggregator(), new OrderWindowFunction());
处理时间窗口不需要 assignTimestampsAndWatermarks,到点(系统时间)就触发。但结果不可重现,系统负载高时窗口内数据可能不完整。
04 滑动时间窗口详解及代码实现
滑动时间窗口的窗口大小固定,但滑动步长可以配置。如果步长小于窗口大小,窗口之间会重叠,一个元素可能属于多个窗口。
完整代码:每 30 秒输出最近 5 分钟平均响应时间
java
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
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.time.Duration;
public class SlidingEventTimeExample {
public static class MetricEvent {
private String api;
private long latency;
private long timestamp;
public MetricEvent(String api, long latency, long timestamp) {
this.api = api; this.latency = latency; this.timestamp = timestamp;
}
public String getApi() { return api; }
public long getLatency() { return latency; }
public long getTimestamp() { return timestamp; }
}
public static class AvgResult {
private String api;
private long windowStart;
private long windowEnd;
private double avgLatency;
private long requestCount;
public AvgResult(String api, long ws, long we, double avg, long cnt) {
this.api = api; this.windowStart = ws; this.windowEnd = we;
this.avgLatency = avg; this.requestCount = cnt;
}
@Override
public String toString() {
return String.format("API=%s, 窗口[%d~%d), 平均延迟=%.2fms, 请求数=%d",
api, windowStart, windowEnd, avgLatency, requestCount);
}
}
// 累加器
public static class LatencyAcc {
long sum = 0;
long count = 0;
}
// 增量聚合:计算平均延迟
public static class AvgLatencyAggregator implements AggregateFunction<MetricEvent, LatencyAcc, AvgResult> {
@Override
public LatencyAcc createAccumulator() { return new LatencyAcc(); }
@Override
public LatencyAcc add(MetricEvent event, LatencyAcc acc) {
acc.sum += event.getLatency();
acc.count++;
return acc;
}
@Override
public AvgResult getResult(LatencyAcc acc) {
double avg = acc.count > 0 ? (double) acc.sum / acc.count : 0;
return new AvgResult(null, 0, 0, avg, acc.count);
}
@Override
public LatencyAcc merge(LatencyAcc a, LatencyAcc b) {
a.sum += b.sum; a.count += b.count; return a;
}
}
public static class AvgWindowFunction extends ProcessWindowFunction<AvgResult, AvgResult, String, TimeWindow> {
@Override
public void process(String api, Context context, Iterable<AvgResult> elements,
Collector<AvgResult> out) {
AvgResult result = elements.iterator().next();
out.collect(new AvgResult(api, context.window().getStart(),
context.window().getEnd(), result.avgLatency, result.requestCount));
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<MetricEvent> source = env.fromElements(
new MetricEvent("/api/login", 100, 1000L),
new MetricEvent("/api/login", 200, 2000L),
new MetricEvent("/api/pay", 300, 3000L),
new MetricEvent("/api/login", 150, 31000L),
new MetricEvent("/api/pay", 250, 32000L)
);
DataStream<MetricEvent> withTimestamps = source.assignTimestampsAndWatermarks(
WatermarkStrategy.<MetricEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, rt) -> event.getTimestamp())
);
// 滑动事件时间窗口:窗口大小 5 分钟,滑动步长 30 秒
DataStream<AvgResult> result = withTimestamps
.keyBy(MetricEvent::getApi)
.window(SlidingEventTimeWindows.of(Time.minutes(5), Time.seconds(30)))
.aggregate(new AvgLatencyAggregator(), new AvgWindowFunction());
result.print();
env.execute("Sliding EventTime Example");
}
}
关键点说明
SlidingEventTimeWindows.of(Time.minutes(5), Time.seconds(30)):窗口大小 5 分钟,滑动步长 30 秒- 状态倍数 = 窗口大小 / 滑动步长 = 5min / 30s = 10 倍。每个元素会被加入 10 个窗口,状态是滚动窗口的 10 倍
- 滑动步长等于窗口大小时,就是滚动窗口;滑动步长大于窗口大小时,会有数据不被任何窗口包含(丢数据),一般不这么用
- 滑动窗口结果平滑,不会突然跳变,适合趋势展示和监控曲线
- 必须用增量聚合:滑动窗口状态倍数大,用全量 ProcessWindowFunction 会 OOM,必须用 ReduceFunction 或 AggregateFunction 增量聚合
05 窗口对齐 offset:让窗口从指定时间点开始
默认情况下,滚动窗口的起始时间是 epoch 时间(1970-01-01 00:00:00 UTC)对齐的。比如 1 小时窗口的起始时间是 00:00、01:00、02:00...(UTC 时间)。
但有时候我们希望窗口从其他时间点开始:
- 时区调整:北京时间 UTC+8,希望窗口按北京时间 00:00 开始,需要 offset -8 小时
- 业务时间对齐:每天从 8 点开始统计(如营业日),需要 offset 8 小时
- 错开触发尖峰:所有窗口同时触发会导致计算尖峰,错开窗口触发时间可以平滑负载
offset 的计算公式
java
start = timestamp - (timestamp - offset + size) % size
offset 是毫秒级,正数向右偏移(窗口开始时间变晚),负数向左偏移(窗口开始时间变早)。
代码示例
java
// 1 小时窗口,offset -8 小时,让窗口按北京时间对齐(00:00 北京时间 = 16:00 UTC 前一天)
TumblingEventTimeWindows.of(Time.hours(1), Time.hours(-8))
// 1 天窗口,offset 8 小时,让窗口从北京时间 08:00 开始(营业日)
TumblingEventTimeWindows.of(Time.days(1), Time.hours(8))
// 5 分钟窗口,offset 2 分钟,让窗口从每小时的第 2 分钟开始(02, 07, 12...)
TumblingEventTimeWindows.of(Time.minutes(5), Time.minutes(2))
注意事项
- offset 只影响窗口的起始时间,不影响窗口大小
- 滑动窗口也支持 offset,两个参数版本
of(size, slide, offset) - 时区转换要算清楚:北京时间 = UTC + 8 小时,要让窗口按北京时间对齐,offset 应该是 -8 小时(因为 Flink 内部用 UTC 时间戳)
- offset 不要设置得太大或太小,一般在一个窗口大小范围内
06 TimeWindow 内部结构与触发机制

TimeWindow 内部结构
TimeWindow 是 Flink 中表示时间窗口的类,内部结构很简单:
java
public class TimeWindow extends Window {
private final long start; // 窗口起始时间(含)
private final long end; // 窗口结束时间(不含)
public TimeWindow(long start, long end) {
this.start = start;
this.end = end;
}
public long getStart() { return start; }
public long getEnd() { return end; }
// 最大时间戳 = end - 1,用于注册触发定时器
public long maxTimestamp() {
return end - 1;
}
}
start:窗口起始时间,包含(左闭)end:窗口结束时间,不包含(右开)maxTimestamp():返回end - 1,用于注册触发定时器- 窗口区间是
[start, end),时间戳等于 end 的元素属于下一个窗口
触发完整流程
TimeWindow 的触发分 5 步:
- 元素到达:WindowAssigner 决定元素属于哪些窗口
- 注册定时器 :onElement 中注册触发定时器,时间 =
window.maxTimestamp() = end - 1 - 定时器触发:事件时间窗口是 Watermark ≥ end-1,处理时间窗口是系统时间 ≥ end
- 返回 FIRE:触发 WindowFunction 计算并输出
- 清理窗口:默认触发后清理窗口状态(如果配置了 allowedLateness,会延迟清理)
EventTimeTrigger vs ProcessingTimeTrigger
| 对比项 | EventTimeTrigger | ProcessingTimeTrigger |
|---|---|---|
| 定时器类型 | 事件时间定时器 | 处理时间定时器 |
| 注册时间 | window.maxTimestamp() = end - 1 | window.end |
| 触发条件 | Watermark ≥ end - 1 | 系统时间 ≥ end |
| 需要 Watermark | ✅ 是 | ❌ 否 |
| 结果可重现 | ✅ 是 | ❌ 否 |
为什么触发定时器注册在 end - 1 而不是 end
因为窗口是左闭右开 [start, end),时间戳等于 end 的元素属于下一个窗口。如果定时器注册在 end,那么时间戳为 end-1 的元素可能还没处理完就触发了。注册在 end-1 保证窗口内所有元素(最大时间戳 end-1)都能被处理。
实际效果是 Watermark ≥ 窗口结束时间时窗口触发(因为 Watermark 是整数毫秒,end-1 和 end 在实际效果上等价)。
07 事件时间窗口与 Watermark 的关系
事件时间窗口的触发完全依赖 Watermark。理解了 Watermark,就理解了事件时间窗口为什么不触发、什么时候触发。
Watermark 是什么
Watermark 是一个时间戳,表示"这个时间之前的数据都已经到齐了"。比如 Watermark = 10:05 表示 10:05 之前的数据都已经到了,10:05 之后可能还有迟到数据。
Watermark 如何触发窗口
事件时间窗口的触发定时器注册在 end - 1。当 Watermark 推进到 ≥ end - 1 时,定时器触发,窗口计算输出。
举个例子:窗口是 [10:00, 10:05),定时器注册在 10:04:59.999。当 Watermark 推进到 ≥ 10:04:59.999 时(实际就是 Watermark ≥ 10:05),窗口触发。
Watermark 卡住的影响
如果 Watermark 不推进,所有事件时间窗口都不会触发。常见原因:
- 数据源空闲:某个 Source 分区没有数据,Watermark 不推进
- 忘了分配时间戳 :没调用
assignTimestampsAndWatermarks,Watermark 永远是 Long.MIN_VALUE - 数据延迟太大:Watermark 策略的乱序时间设得太大,Watermark 推进慢
- 多源对齐问题:多个 Source 的 Watermark 取最小值,某个 Source 慢就拖慢整体
解决方案
java
// 1. 配置空闲源超时:如果某个 Source 超过 1 分钟没有数据,标记为空闲,不参与 Watermark 计算
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withIdleness(Duration.ofMinutes(1)) // 空闲源超时
.withTimestampAssigner((event, rt) -> event.getTimestamp())
// 2. 定期发送 Watermark(自定义 WatermarkGenerator)
// 3. 监控 Watermark:Flink Web UI 可以看每个算子的当前 Watermark,排查卡住问题
08 TimeWindow 实战案例与时间语义选择

四大实战案例
| 案例 | 时间语义 | 窗口类型 | 特点 |
|---|---|---|---|
| 实时大屏统计 | 事件时间 | 滚动 1 分钟 | 结果准确,大屏数字稳定不跳变 |
| 实时监控告警 | 处理时间 | 滚动 10 秒 | 延迟最低,到点就触发 |
| 平滑趋势统计 | 事件时间 | 滑动 5 分钟/30 秒 | 结果平滑,状态 10 倍 |
| 跨时区业务统计 | 事件时间 | 滚动 1 天 offset -8h | 按北京时间对齐 |
时间语义选择决策树
- 事件是否携带时间戳?
- 否 → 用处理时间(事件没有时间戳,只能用处理时间)
- 是 → 继续
- 结果是否需要准确可重现?
- 是 → 用事件时间(推荐)
- 否 → 继续
- 是否要求最低延迟?
- 是 → 用处理时间
- 否 → 用事件时间(更稳定,推荐默认选择)
简单记忆:有时间戳 + 要准确 = 事件时间;要最低延迟 = 处理时间。
滚动 vs 滑动对比
| 对比项 | 滚动窗口 Tumbling | 滑动窗口 Sliding |
|---|---|---|
| 是否重叠 | 不重叠 | 可能重叠 |
| 元素归属 | 只属一个窗口 | 可能属多个窗口 |
| 状态大小 | 1 倍 | 大小/步长 倍 |
| 结果特点 | 离散,可能跳变 | 平滑,连续变化 |
| 适用场景 | 定时统计、报表 | 趋势监控、平滑曲线 |
选择建议:
- 统计报表、定时聚合 → 滚动窗口(状态小,结果清晰)
- 趋势监控、平滑曲线、移动平均 → 滑动窗口(结果平滑,但状态倍数大)
- 滑动窗口的状态倍数 = 窗口大小 / 滑动步长,超过 10 倍就要评估内存了
09 生产环境最佳实践与常见坑
最佳实践
- 优先用事件时间:90% 以上的场景用事件时间,结果可重现,准确稳定。只有对延迟要求极高且可以接受不精确的场景才用处理时间。
- 事件时间窗口必须分配时间戳 :用
assignTimestampsAndWatermarks分配时间戳和 Watermark,否则窗口永远不触发。 - 合理设置 Watermark 乱序时间:太小会丢数据(迟到数据被丢弃),太大会延迟输出(Watermark 推进慢)。根据实际数据乱序情况设置,一般 1-10 秒。
- 滑动窗口用增量聚合:滑动窗口状态倍数大,必须用 ReduceFunction 或 AggregateFunction 增量聚合,不要用全量 ProcessWindowFunction,否则会 OOM。
- 配置空闲源超时 :多 Source 场景下,某个 Source 空闲会导致 Watermark 卡住,用
withIdleness(Duration)配置空闲源超时。 - 跨时区用 offset 对齐:北京时间统计需要 offset -8 小时,让窗口按北京时间 00:00 开始,而不是 UTC 00:00(北京时间 08:00)。
常见坑
-
坑一:忘了分配时间戳
- 现象:用了事件时间窗口,但作业跑起来窗口永远不触发,没有输出
- 原因:没调用
assignTimestampsAndWatermarks,Watermark 永远是 Long.MIN_VALUE - 解决:必须分配时间戳和 Watermark,事件时间窗口依赖 Watermark 触发
-
坑二:Watermark 卡住
- 现象:作业运行一段时间后,所有事件时间窗口都不触发了,状态持续增长
- 原因:某个 Source 分区空闲或数据延迟太大,Watermark 不推进
- 解决:配置
withIdleness(Duration)空闲源超时,监控 Web UI 的 Watermark 指标
-
坑三:滑动窗口状态爆炸
- 现象:滑动窗口作业的状态大小是滚动窗口的好几倍,内存不够用,频繁 GC 或 OOM
- 原因:滑动窗口状态倍数 = 窗口大小 / 滑动步长,如 1 小时窗口 1 分钟滑动 = 60 倍状态
- 解决:用增量聚合省内存,增大滑动步长减小倍数,大状态用 RocksDB 后端
-
坑四:时区没对齐
- 现象:按天统计的窗口,结果是从北京时间 08:00 开始的,而不是 00:00
- 原因:默认窗口按 UTC 时间对齐,UTC 00:00 = 北京时间 08:00
- 解决:用 offset 参数对齐,
TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8))
-
坑五:处理时间结果不可重现
- 现象:用处理时间窗口做统计报表,每次跑结果都不一样,系统负载高时数据不完整
- 原因:处理时间依赖系统时钟,结果不可重现,负载高时窗口内数据可能不完整
- 解决:报表场景必须用事件时间,处理时间只用于监控告警等对延迟要求高的场景
10 总结
TimeWindow 是 Flink 中最常用的窗口类型,核心要点:
- 三种时间语义:事件时间(推荐,结果可重现)、处理时间(延迟最低,结果不可重现)、摄入时间(折中,很少用)。选择原则:有时间戳 + 要准确 = 事件时间;要最低延迟 = 处理时间。
- 滚动时间窗口:大小固定,不重叠,每个元素只属一个窗口,状态小,适合定时统计和报表。
- 滑动时间窗口:大小固定,滑动步长可配,可能重叠,状态倍数 = 窗口大小/滑动步长,结果平滑,适合趋势监控。必须用增量聚合。
- 窗口对齐 offset:让窗口从指定时间点开始,用于时区调整、业务时间对齐、错开触发尖峰。北京时间统计需要 offset -8 小时。
- 内部结构与触发:TimeWindow(start, end),maxTimestamp = end - 1。触发流程:注册定时器 → 时间到 → FIRE → 计算输出 → 清理。事件时间依赖 Watermark,处理时间依赖系统时钟。
- 生产最佳实践:优先事件时间、必须分配时间戳、合理设置 Watermark、滑动窗口用增量聚合、配置空闲源超时、跨时区用 offset 对齐。
TimeWindow 的本质是"按时间切分无界流"------时间是最自然的切分维度,大部分实时统计都是按时间的。理解了三种时间语义的区别、滚动与滑动的选择、事件时间与 Watermark 的配合,TimeWindow 就掌握了。