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 秒内传感器的平均温度。当时用了滚动窗口,代码写起来很快,上线后效果也很好。为什么?因为传感器数据是连续上报的,不需要考虑会话间隙,也不需要滑动重叠,滚动窗口就是最直接的选择。

总结一下:滚动窗口适合那些“按固定周期、独立统计、不重复计算”的场景。如果你的业务逻辑符合这个特征,滚动窗口就是你的首选。

滚动窗口核心逻辑示意图 数据流 窗口 1 [0, 10s) 窗口 2 [10s, 20s) 窗口 3 [20s, 30s) 触发条件:每个窗口结束时(10s、20s、30s)自动触发计算并输出结果 固定长度 无重叠 连续覆盖