我们知道flink是一个流计算框架,流计算就意味着程序是不知道自己什么时候该结束,所以一般的流程序都是:来了一个元素,得到一个相应的结果。这个特性在做WordCount的时候感受非常清晰,比方说,使用spark或者flink batch模型进行WordCount时,对某个文档的统计结果输出:
(hello, 2)
(flink, 3)
那么如果我们使用flink streaming模型进行处理时,输出的结果很有可能长下面这个样子:
1> (hello, 1)
2> (flink, 1)
1> (hello, 2)
2> (flink, 2)
2> (flink, 3)
为啥会这样呢,因为flink流程需并不直到这个输入的文本流什么时候结束,因此来一个word,就得到一个count结果,将结果写入结果流一次,所以来了多少个word,结果流里面就有多少个count结果。
给出一下flink流进行WordCount的代码:
public class WindowWordCount {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<Tuple2<String, Long>> source = env.fromElements(
Tuple2.of("hello", 1L),
Tuple2.of("flink", 1L),
Tuple2.of("hello", 2L),
Tuple2.of("spark", 3L),
Tuple2.of("hello", 4L),
Tuple2.of("flink", 5L),
Tuple2.of("spark", 7L));
DataStream<Tuple2<String, Long>> words = source
.assignTimestampsAndWatermarks(new TimestampsAndWatermarks())
.map(new MapFunction<Tuple2<String, Long>, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(Tuple2<String, Long> value) throws Exception {
return Tuple2.of(value.f0, 1L);
}
});
words.keyBy(x->x.f0)
.reduce((ReduceFunction<Tuple2<String, Long>>) (value1,value2)-> Tuple2.of(value1.f0,
value1.f1 + value2.f1))
.print();
env.execute();
}
static class TimestampsAndWatermarks implements WatermarkStrategy<Tuple2<String, Long>> {
@Override
public WatermarkGenerator<Tuple2<String, Long>> createWatermarkGenerator(
WatermarkGeneratorSupplier.Context context) {
return new WatermarkGenerator<Tuple2<String, Long>>() {
@Override
public void onEvent(
Tuple2<String, Long> event,
long eventTimestamp,
WatermarkOutput output) {
output.emitWatermark(new Watermark(event.f1-1));
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
return;
}
};
}
@Override
public TimestampAssigner<Tuple2<String, Long>> createTimestampAssigner(
TimestampAssignerSupplier.Context context) {
return (element, recordTimestamp) -> element.f1;
}
}
}
上面的代码产生的结果是:
1> (spark,1)
1> (spark,2)
10> (flink,1)
10> (flink,2)
4> (hello,1)
4> (hello,2)
4> (hello,3)
所以实际上,我决定更贴切的说法是:因为流是没有边界的,流中来个一个事件event
,就触发一次相应的计算,得到一个结果。那么我理解的窗口就是:其改变了flink得到结果的方式,为流明确地规定了界限,从来一个元素得到一次相应的结果,变为一个窗口关闭得到一次结果。
我们依然从WordCount举例子,不同的是,我们在这里使用窗口:
words.keyBy(x->x.f0)
.window(TumblingEventTimeWindows.of(Time.milliseconds(5))) // 窗口跟在keyby后面
.reduce((ReduceFunction<Tuple2<String, Long>>) (value1,value2)-> Tuple2.of(value1.f0,
value1.f1 + value2.f1))
.print();
因为我设置了5ms的滚动窗口,产生了[0,5), [5,10)
这两个窗口(窗口的产生规则),那么此时产生的结果就是:
10> (flink,1) // 0~5 ms
10> (flink,1) // 5~10 ms
1> (spark,1) // 0~5 ms
1> (spark,1) // 5~10 ms
4> (hello,3) // 0~5ms
可以看到,flink 变为一个窗口关闭得到一个结果(窗口的关闭由watermark触发,一旦窗口的结束时间小于watermark,窗口就关闭)。
但是需要注意的是,即便我们使用了窗口,reduce方法的计算也是来一个事件计算一次,只是在窗口关闭的时候输出一个结果罢了。也就是说reduce在窗口内也是增量聚合,这很合理,因为这样处理利用流处理的优点,将压力分给了各个时间段,而避免了窗口结束时刻统一计算的热点问题。所以说,窗口仅仅改变了读出结果的方式,而没有改变产生结果的方式。