Flink 窗口(Window) 是处理 无界流数据 时最核心的概念之一,它能将无限制的数据流按 时间或数量 切分成一个个有限的"数据桶",然后在这些"桶"里执行聚合计算。
一、什么是窗口处理函数
Flink 窗口处理函数定义了 窗口内数据如何被计算与输出。根据处理时机不同,可以分为:
| 类型 | 是否缓存窗口内所有数据 | 优点 | 典型函数 |
|---|---|---|---|
| 增量处理 | ❌ | 低延迟、节省空间 | reduce、aggregate |
| 全量处理 | ✔ | 可访问全窗口数据 | apply、process |
二、增量处理 --- 每条数据来就处理
🔹 reduce
- 每条数据到达都会更新聚合结果
- 输入 / 累加器 / 输出 类型一致
- 不保存整个窗口数据,只累加状态
java
.reduce(new ReduceFunction<SensorReading>() {
@Override
public SensorReading reduce(SensorReading a, SensorReading b) {
// 计算最大温度
return a.getTemperature() > b.getTemperature() ? a : b;
}
})
⚠ 如果窗口只有一条数据,reduce() 不会被调用。
🔹 aggregate
更灵活的累加处理:
✔ 输入类型、累加器类型、输出类型可以不一致
✔ 可在累加器中做更复杂逻辑
核心方法:
createAccumulator():初始化累加器add():每条记录到达调用getResult():窗口触发时返回结果merge():会话窗口需要合并状态
三、全量处理 --- 等窗口触发再处理
不同于增量处理,全量处理保留整个窗口数据,并在窗口结束时一次性计算:
🔹 apply
适合窗口中数据量不是特别大但需要全量访问的场景。
java
.apply(new WindowFunction<...>() {
@Override
public void apply(...) {
// 访问完整窗口数据操作
}
});
🔹 process
最底层的全量处理函数,可以获取更多上下文信息,如窗口时间、watermark、状态等:
java
.process(new ProcessWindowFunction<SensorReading, String, String, TimeWindow>() {
@Override
public void process(String key, Context context,
Iterable<SensorReading> elements,
Collector<String> out) {
// 访问窗口全部元素
}
});
这个函数比 apply 更强大。
四、为什么这些很重要?
- 在 大部分真实业务中,我们既希望结果准确(全量处理),又希望响应快(增量处理)。
- 通过增量处理先减少延迟,再用全量处理做更精细计算,可以在性能和准确性间取得平衡。
五、传感器温度实时窗口统计
事件模型 --- SensorReading
java
public class SensorReading {
private String sensorId;
private Long timestamp;
private Double temperature;
public SensorReading() {}
public SensorReading(String sensorId, Long timestamp, Double temperature) {
this.sensorId = sensorId;
this.timestamp = timestamp;
this.temperature = temperature;
}
// getter / setter
@Override
public String toString() {
return "SensorReading{" +
"sensorId='" + sensorId + '\'' +
", timestamp=" + timestamp +
", temperature=" + temperature +
'}';
}
}
🔹 5.2 自定义模拟数据源 --- SensorSource
java
public class SensorSource implements SourceFunction<SensorReading> {
private volatile boolean running = true;
@Override
public void run(SourceContext<SensorReading> ctx) throws Exception {
Random rand = new Random();
while (running) {
long timestamp = System.currentTimeMillis();
// 5 个 sensor 并行发送
for (int i = 0; i < 5; i++) {
String sensorId = "sensor_" + i;
double temp = 20 + rand.nextGaussian() * 10;
ctx.collect(new SensorReading(sensorId, timestamp, temp));
}
Thread.sleep(200);
}
}
@Override
public void cancel() {
running = false;
}
}
主程序 --- 引入 Watermark + 多种窗口处理
java
public class FlinkWindowDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<SensorReading> stream = env
.addSource(new SensorSource())
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<SensorReading>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, ts) -> event.getTimestamp())
);
// ------ 增量处理:reduce
stream
.keyBy(SensorReading::getSensorId)
.window(TumblingEventTimeWindows.of(Duration.ofSeconds(10)))
.reduce((a, b) -> a.getTemperature() > b.getTemperature() ? a : b)
.print("Reduce Max Temp");
// ------ 全量处理:process
stream
.keyBy(SensorReading::getSensorId)
.window(TumblingEventTimeWindows.of(Duration.ofSeconds(10)))
.process(new ProcessWindowFunction<SensorReading, String, String, TimeWindow>() {
@Override
public void process(String key, Context ctx,
Iterable<SensorReading> elements,
Collector<String> out) {
int count = 0;
double sum = 0;
for (SensorReading r : elements) {
count++;
sum += r.getTemperature();
}
out.collect(key + " avg=" + (sum / count) + ", count=" + count);
}
})
.print("Process Avg Temp");
env.execute("Flink Window Demo");
}
}
六、何时用哪种处理方式?
| 场景 | 推荐 |
|---|---|
| 实时性要求高 & 只需简单汇总 | 增量处理 (reduce, aggregate) |
| 需要完整窗口统计 | 全量处理 (process, apply) |
| 又要快响应又要丰富输出 | 混合模式 |
七、总结
✔ 增量处理 ------ 快、少空间,但无法访问全部数据
✔ 全量处理 ------ 能访问所有数据,结果丰富但占空间
✔ 混合模式 ------ 最灵活、兼顾性能和业务需求