4. 滚动窗口(Tumbling Window)
滚动窗口,英文叫 Tumbling Window,是窗口模型里最基础、最直观的一种。说白了,就是把数据流按固定的时间长度切成一段一段,每段之间没有重叠,也没有间隙。你想想看,就像切香肠,一刀下去,一段完整的,下一刀再切下一段。
我个人习惯把滚动窗口叫做“固定快照窗口”。因为每个窗口里的数据,都是那个时间段内到达的所有事件的完整集合。窗口与窗口之间,数据是严格隔离的,不会出现同一条数据出现在两个窗口里的情况。
核心定义:滚动窗口将数据流划分为固定大小、不重叠、连续的时间段。每个事件恰好属于一个窗口。
4.1 滚动窗口定义与特性
滚动窗口有三个关键特性,我一个个说。
- 固定长度:窗口大小是固定的,比如 5 秒、1 分钟、1 小时。这个长度在窗口创建时就确定,不会动态变化。
- 无重叠:相邻窗口之间没有共享数据。窗口 A 的数据不会出现在窗口 B 里。这一点和滑动窗口完全不同。
- 连续覆盖:整个数据流被窗口完整覆盖,没有空隙。上一个窗口结束的时刻,就是下一个窗口开始的时刻。
我记得有一次在项目中,业务方要求统计每 5 分钟的订单金额。他们一开始想用滑动窗口,但发现数据重复计算了,导致金额虚高。后来换成滚动窗口,问题就解决了。嗯,这里要注意:如果你的业务场景要求每个事件只被统计一次,滚动窗口就是最自然的选择。
| 特性 | 滚动窗口 | 滑动窗口 | 会话窗口 |
|---|---|---|---|
| 窗口长度 | 固定 | 固定 | 动态 |
| 窗口重叠 | 无 | 有 | 无 |
| 触发频率 | 每段结束时 | 每滑动步长 | 会话间隙超时 |
| 数据归属 | 唯一窗口 | 多个窗口 | 唯一窗口 |
4.2 滚动窗口的触发条件
触发条件其实很简单:当时间到达窗口的结束边界时,窗口被触发计算并输出结果。
举个例子,窗口大小设为 10 秒,从 00:00:00 开始。那么:
- 窗口 [00:00:00, 00:00:10) 在 00:00:10 触发
- 窗口 [00:00:10, 00:00:20) 在 00:00:20 触发
- 以此类推
这里有个细节:窗口的结束边界是左闭右开的。也就是说,时间戳正好等于窗口结束时间的事件,会进入下一个窗口。我曾经踩过这个坑,在调试时发现一条数据“消失”了,后来才意识到是边界处理的问题。
避坑指南:我曾经在处理乱序数据时,发现滚动窗口的触发时间依赖于 watermark。如果 watermark 推进太慢,窗口可能迟迟不触发,导致结果延迟。建议根据业务容忍的延迟时间,合理设置 allowed lateness 和 watermark 生成策略。
4.3 滚动窗口的代码实现
代码实现这块,我分别给出 Flink 和 Spark Streaming 的示例。两种框架的 API 风格不同,但核心逻辑是一样的。
Flink 实现
Flink 的滚动窗口 API 非常简洁。我个人习惯用 .window(TumblingEventTimeWindows.of(Time.seconds(10))) 这种写法,语义清晰。
// Flink 滚动窗口示例 - 统计每10秒的订单金额
DataStream<Order> orderStream = ...;
DataStream<OrderStats> statsStream = orderStream
.keyBy(order -> order.getRegion())
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.aggregate(new OrderAggregateFunction());
// 自定义聚合函数
public static class OrderAggregateFunction
implements AggregateFunction<Order, OrderStats, OrderStats> {
@Override
public OrderStats createAccumulator() {
return new OrderStats();
}
@Override
public OrderStats add(Order order, OrderStats acc) {
acc.setCount(acc.getCount() + 1);
acc.setTotalAmount(acc.getTotalAmount() + order.getAmount());
return acc;
}
@Override
public OrderStats getResult(OrderStats acc) {
return acc;
}
@Override
public OrderStats merge(OrderStats a, OrderStats b) {
a.setCount(a.getCount() + b.getCount());
a.setTotalAmount(a.getTotalAmount() + b.getTotalAmount());
return a;
}
}
小技巧:在 Flink 中,如果使用事件时间,记得设置 watermark。我一般用 WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(5)) 来处理乱序数据,这样既能容忍一定延迟,又不会让窗口触发太晚。
Spark Streaming 实现
Spark Streaming 的滚动窗口通过 .window(windowLength, slideInterval) 实现。当 slideInterval 等于 windowLength 时,就是滚动窗口。
// Spark Streaming 滚动窗口示例 - 统计每10秒的订单金额
val orderStream: DStream[Order] = ...
val statsStream = orderStream
.map(order => (order.region, (1L, order.amount)))
.window(Seconds(10), Seconds(10)) // 窗口长度=滑动步长,即滚动窗口
.reduceByKey { (a, b) =>
(a._1 + b._1, a._2 + b._2)
}
.map { case (region, (count, totalAmount)) =>
OrderStats(region, count, totalAmount)
}
你看,代码其实不复杂。关键是要理解:.window(Seconds(10), Seconds(10)) 这两个参数,第一个是窗口长度,第二个是滑动步长。两者相等,就是滚动窗口。
4.4 滚动窗口的应用场景
滚动窗口的应用场景非常广泛。我挑几个典型的说说。
- 周期性报表:比如每 5 分钟生成一次系统健康报告,统计 CPU 使用率、内存占用等指标。滚动窗口天然适合这种固定周期的统计需求。
- 计费与计量:按小时或按天统计用户的 API 调用次数、流量消耗。每个计费周期独立计算,不重复。
- 监控告警:每 1 分钟统计错误日志数量,超过阈值就触发告警。滚动窗口能保证告警不会因为数据重叠而重复触发。
- 实时大屏:每 10 秒刷新一次交易金额、订单数量等指标。滚动窗口的输出频率固定,适合前端展示。
我记得在做一个物联网项目时,需要统计每 30 秒内传感器的平均温度。当时用了滚动窗口,代码写起来很快,上线后效果也很好。为什么?因为传感器数据是连续上报的,不需要考虑会话间隙,也不需要滑动重叠,滚动窗口就是最直接的选择。
总结一下:滚动窗口适合那些“按固定周期、独立统计、不重复计算”的场景。如果你的业务逻辑符合这个特征,滚动窗口就是你的首选。