1.2 窗口与时间语义

流处理窗口类型深度解析:滚动窗口(Tumbling)、滑动窗口(Sliding)、会话窗口(Session)、全局窗口(Global)的分配机制、触发器 Trigger 与驱逐器 Evictor、fired-but-not-closed 语义,以及窗口在 Event Time 与 Processing Time 下的行为差异。

为什么需要窗口

无界流的数据永无止境,无法直接对「全部数据」做聚合(和不会收敛)。窗口把无限流切分为有限块,在每个块上单独计算。

无界流:──A──B──C──D──E──F──G──H──I──J──K──L──...(永不结束)

加 1 分钟滚动窗口后:
窗口 1: [A, B, C]      → 聚合结果 1
窗口 2: [D, E, F, G]   → 聚合结果 2
窗口 3: [H, I, J]      → 聚合结果 3
...

四种窗口类型

1. 滚动窗口(Tumbling Window)

窗口之间不重叠,每个事件只属于一个窗口。

时间线:|----1min----|----1min----|----1min----|----1min----|
事件:       A  B        C  D  E       F  G        H  I  J
窗口:      [A,B]        [C,D,E]      [F,G]        [H,I,J]

适用:分钟级/小时级固定统计(每分钟订单量、每小时 UV)。

2. 滑动窗口(Sliding Window)

窗口之间有重叠,一个事件可能属于多个窗口。

窗口大小 5 分钟,滑动步长 1 分钟:

时间线:|1m|1m|1m|1m|1m|1m|1m|1m|...    
窗口: [-------5min------]
         [-------5min------]
           [-------5min------]

适用:移动平均(近 5 分钟平均延迟)、趋势平滑。

3. 会话窗口(Session Window)

由活动间隙动态划分,没有固定大小。

事件流:A──B────C──D──E───────F──G
              ↑ 间隙 > 30min    ↑ 间隙 > 30min
窗口:[A,B]    [C,D,E]          [F,G]

适用:用户行为分析(一次会话内的点击序列)。

4. 全局窗口(Global Window)

只有一个窗口,包含所有数据。

窗口:[A, B, C, D, E, F, G, H, I, J, ...]

适用:需要自定义触发条件的场景(如「收集 100 条后触发")。

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// KafkaSource 是 Source 接口实现,走 fromSource;addSource 已在 1.20 废弃
DataStream<Event> stream = env.fromSource(
    kafkaSource,
    WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
        .withTimestampAssigner((event, ts) -> event.getEventTime()),
    "kafka");

// 滚动窗口(Event Time)
stream.keyBy(Event::getUserId)
    .window(TumblingEventTimeWindows.of(Duration.ofMinutes(1)))
    .aggregate(new CountAggregate());

// 滑动窗口
stream.keyBy(Event::getUserId)
    .window(SlidingEventTimeWindows.of(Duration.ofMinutes(5), Duration.ofMinutes(1)))
    .aggregate(new AverageAggregate());

// 会话窗口
stream.keyBy(Event::getUserId)
    .window(EventTimeSessionWindows.withGap(Duration.ofMinutes(30)))
    .aggregate(new SessionAggregate());

// 全局窗口 + 自定义触发器
stream.keyBy(Event::getUserId)
    .window(GlobalWindows.create())
    .trigger(CountTrigger.of(100))
    .aggregate(new BatchAggregate());

触发器(Trigger)

窗口的触发器决定何时输出结果:

触发器行为
EventTimeTrigger当 Watermark 越过窗口的 maxTimestamp(结束时间减 1ms)时触发(默认)
ProcessingTimeTrigger处理时间到达窗口结束时间时触发
CountTrigger窗口内元素数量达到阈值时触发
ContinuousEventTimeTrigger每隔一段时间触发一次(窗口内的增量更新)
// 每分钟触发一次(窗口未关闭时也输出)
stream.keyBy(Event::getUserId)
    .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))
    .trigger(ContinuousEventTimeTrigger.of(Duration.ofMinutes(1)))
    .aggregate(new CountAggregate());

这个例子:5 分钟窗口,但每分钟输出一次当前计数(增量更新)。窗口关闭时再输出最终值。

驱逐器(Evictor)

驱逐器在窗口触发前移除部分元素:

stream.keyBy(Event::getUserId)
    .window(TumblingEventTimeWindows.of(Duration.ofMinutes(1)))
    .evictor(TimeEvictor.of(Duration.ofSeconds(30)))  // 只保留最近 30 秒的数据
    .aggregate(new CountAggregate());

适用:只关心窗口内的「最新」数据,丢弃过期元素。

fired-but-not-closed

Flink 窗口的微妙语义:触发(fire)≠ 关闭(close)。

  • fire:输出当前窗口的计算结果
  • close:窗口状态被清理,不再接受新数据

默认行为:Watermark 超过窗口结束时间 → fire + close 同时发生。

如果配置了 allowedLateness:

stream.keyBy(Event::getUserId)
    .window(TumblingEventTimeWindows.of(Duration.ofMinutes(1)))
    .allowedLateness(Duration.ofSeconds(30))  // 允许迟到 30 秒
    .aggregate(new CountAggregate());

行为:

Watermark 到达 06:01:00 → 窗口 [06:00, 06:01) fire,输出结果
                            → 窗口保持 open(因为 allowedLateness = 30s)
Watermark 到达 06:01:30 → 窗口 close,状态清理

在 fire 和 close 之间的 30 秒内,迟到的数据还会被计入该窗口并再次输出。机制在 EventTimeTrigger 里:Watermark 已经越过 maxTimestamp 时,它收到元素会直接返回 FIRE。所以这是 allowedLateness 自带的重新触发,不需要配 ContinuousEventTimeTrigger;后者管的是「窗口还没结束,也要按固定间隔先输出一次」,两者不要混为一谈。

窗口在 Event Time vs Processing Time 下的差异

场景Event TimeProcessing Time
窗口分配按事件时间戳按处理时的系统时间
乱序容忍Watermark + allowedLateness无乱序(按到达顺序)
结果一致性可复现(重跑相同数据得相同结果)不可复现(取决于处理速度)
延迟可能因等待 Watermark 而延迟立即输出

eventd v3 使用 Event Time + 1 分钟滚动窗口 + 10 秒 allowedLateness:

stream.assignTimestampsAndWatermarks(
    WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(10))
        .withIdleness(Duration.ofMinutes(5))
        .withTimestampAssigner((event, timestamp) -> event.getEventTime())
)
.keyBy(Event::getSymbol)
.window(TumblingEventTimeWindows.of(Duration.ofMinutes(1)))
.allowedLateness(Duration.ofSeconds(10))
.aggregate(new OrderCountAggregate());

小结

窗口类型特点适用
滚动不重叠,固定大小固定周期统计
滑动有重叠,固定大小移动平均、趋势分析
会话动态大小,按间隙划分用户行为、会话分析
全局只有一个窗口自定义触发条件
概念作用
Trigger决定何时输出结果
Evictor触发前过滤窗口内元素
allowedLateness容忍迟到数据的时间

下一节 1.3 Flink 环境搭建 :MiniCluster、Maven 配置、第一个可运行 Flink 作业。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南