无论是无界的数据流还是有界的,Flink都可以做到接收一个数据就立即处理一个数据,最终我们可以得到整个数据流的所有数据的统计结果。
但是,一般来说更多的,我们希望得到的是统计某个区间、或者某个时间段内的数据结果,比如每天的商品销量、每天的网站点击量,这种情况下,我们就需要Flink中的窗口机制Window API来实现。
Window,Flink中的窗口机制,我的简单理解就是:该机制可以将一个数据流中的数据按照一定规则进行切割,分割为一段一段的数据,然后将这些一段一段的数据分发在各自的桶(bucket)中进行分析计算,进而得到一个区间内的统计数据结果。
window机制并不是让Flink变成了批处理,每个桶内的还是每来一条数据就计算一次。
1. 滚动时间窗口(Tumbling Windows)
将数据依据固定的窗口长度对数据进行切分,而且时间对齐,窗口长度固定,窗口之间没有重叠。针对不同的时间语义(即ProcessingTime和EventTime),Flink提供了TumblingProcessingTimeWindows和TumblingEventTimeWindows等常用窗口定义类。
2. 滑动时间窗口(Sliding Windows)
滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成,窗口长度固定,可以有重叠。
简单理解就是:当我们定义了第一个窗口的左边界位置的时候,将第一个窗口的左边界向右滑动我们指定的滑动间隔(距离),就是第二个滑动窗口的左边界,同理再向右滑动一次,就是第三个窗口的左边界。
如果创建滑动窗口时,指定的滑动间隔与窗口长度相同,那此时其实就相当于滚动窗口。针对不同的时间语义(即ProcessingTime和EventTime),Flink提供了SlidingProcessingTimeWindows和SlidingEventTimeWindows等常用窗口定义类。
3. 会话窗口(Session Windows)
会话窗口比较特殊,其没有固定的窗口长度,而是是有固定的窗口间隙,或者说timeout,简单来讲就是如果一个窗口在timeout时间之内没有收到数据(这里的时间取决于你使用的是EventTime还是ProcessTime),那么就会关闭当前窗口,开启下一个窗口。针对不同的时间语义(即ProcessingTime和EventTime),Flink提供了ProcessingTimeSessionWindows和EventTimeSessionWindows等常用窗口定义类。
1. 滚动计数窗口
滚动计数,按照数据个数进行划分,滚动的定义与上面相同。
2. 滑动计数窗口
按照数据个数进行划分,滑动的定义与上面相同。
flink中对数据流开窗处理有以下几个方法
(1)window():基本的开窗方法,此方法的调用必须在keyBy操作执行之后,时间窗和计数窗口都可以通过此方法开启。
(2)windowAll():此方法的调用不需要keyBy操作,windowAll方法直接开窗,不进行数据分区,相当于对当前整个数据流进行开窗,屏蔽了flink的并行运行,所以该方法而非必要情况下不要使用。
(3)timeWindow():开启时间窗口,是window方法的简写。
(4)countWindow():开启计数窗口,是window方法的简写。
public class WindowApiTest1 {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource streamSource = env.socketTextStream("192.168.0.1", 9999);
streamSource.flatMap(new MyFlatMap())
//windowAll方法直接开窗,不进行数据分区,相当于对当前数据流进行开窗,屏蔽了flink的并行运行
.windowAll(TumblingProcessingTimeWindows.of(Time.minutes(1)))//开窗方法,参数传递窗口定义
//TumblingProcessingTimeWindows即滚动时间窗口,指定窗口宽度为1分钟
.sum(2);
streamSource.flatMap(new MyFlatMap())
.keyBy(1)//window方法开窗之前必须先进行keyBy,否则无法执行
.window(TumblingProcessingTimeWindows.of(Time.minutes(1)))//开窗方法,参数传递窗口定义
//TumblingProcessingTimeWindows即滚动时间窗口,指定窗口宽度为1分钟
.sum(2);
//上面直接调用window的方法会比较复杂,还有简化的方法
streamSource.flatMap(new MyFlatMap())
.keyBy(1)//开窗之前必须先进行keyBy,否则无法执行
.timeWindow(Time.minutes(1))//调用该方法与上面的写法作用相同
.sum(2);
//开启滑动窗口
streamSource.flatMap(new MyFlatMap())
.keyBy(0)
.window(SlidingProcessingTimeWindows.of(Time.minutes(2), Time.minutes(1)))//开窗方法,参数传递窗口定义
//SlidingProcessingTimeWindows即滑动时间窗口,指定窗口宽度为2分钟,滑动宽度为1分钟
.sum(1);
//简化方法
streamSource.flatMap(new MyFlatMap())
.keyBy(0)//开窗之前必须先进行keyBy,否则无法执行
.timeWindow(Time.minutes(2), Time.minutes(1))//调用该方法与上面的写法作用相同
.sum(1);
//开启会话窗口
streamSource.flatMap(new MyFlatMap())
.keyBy(1)//开窗之前必须先进行keyBy,否则无法执行
//区分EventTime和ProcessingTime
// .window(ProcessingTimeSessionWindows.withGap(Time.minutes(1)))
.window(EventTimeSessionWindows.withGap(Time.minutes(1)))
//指定会话间隔时间为1分钟
.sum(2);
//滚动计数窗口
streamSource.flatMap(new MyFlatMap())
.keyBy(0)//开窗之前必须先进行keyBy,否则无法执行
.countWindow(10)
.sum(1);
//滑动计数窗口
streamSource.flatMap(new MyFlatMap())
.keyBy(0)//开窗之前必须先进行keyBy,否则无法执行
.countWindow(10, 5)
.sum(1);
}
public static class MyFlatMap implements FlatMapFunction> {
public void flatMap(String s, Collector> collector) throws Exception {
String[] s1 = s.split(" ");
for (String s2:s1) {
collector.collect(new Tuple2(s2, 1));
}
}
}
}
如何确定时间窗口的起始边界:当第一条数据进入Flink后,就可以确定第一个窗口的起始边界,而确定了第一个窗口的起始边界,自然也就确定了后续所有窗口的起始边界。在TimeWindow类中有个方法叫getWindowStartWithOffset(),该方法就是时间窗口用来生成第一个窗口边界的,第一个参数就是数据的时间戳,第二个参数是偏移量,第三个参数就代表着窗口大小。
public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
return timestamp - (timestamp - offset + windowSize) % windowSize;
}
//滑动时间窗口
public static SlidingProcessingTimeWindows of(Time size, Time slide, Time offset) {
return new SlidingProcessingTimeWindows(size.toMilliseconds(), slide.toMilliseconds(), offset.toMilliseconds());
}
//滚动时间窗口
public static TumblingProcessingTimeWindows of(Time size, Time offset) {
return new TumblingProcessingTimeWindows(size.toMilliseconds(), offset.toMilliseconds());
}
上面的API仅仅是做了数据开窗,接下来就是要对窗口内的数据进行计算处理。可以简单的分为两类:
(1)增量聚合函数:每条数据到来就进行计算,保持一个简单的状态,比如上面代码中调用的sum方法、还有min方法等,复杂一点的我们需要实现ReduceFunction, AggregateFunction接口进行自定义。但是注意,虽然数据是每来一次就计算一次,但是每次的处理结果并不是立即输出的,而是会在指定的时间点输出最后的计算结果。
(2)全窗口函数:先把窗口所有数据收集起来,等到计算的时候会遍历所有数据,然后计算再输出计算结果,这种其实就是批处理模式了,比如ProcessWindowFunction,WindowFunction。
/**
* @ClassName WindowApiTest2
* @Description: 窗口函数,开窗之后进行的计算操作
* 主要分为两类,分别是增量聚合函数和全量聚合函数
*
* 1. 增量聚合函数表示来一个数据就增量计算一次,比如最大值最小值,数据求和等运算
*
* 2. 全量聚合函数表示必须等到一个窗口内的数据到齐之后才能进行计算,比如排序等
* @Author
* @Date
* @Modified By:
* @Version V1.0
*/
public class WindowApiTest2 {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource streamSource = env.socketTextStream("192.168.0.1", 9999);
//reduce增量聚合函数
streamSource.flatMap(new WindowApiTest1.MyFlatMap())
.keyBy(0)
.timeWindow(Time.minutes(1))
.reduce(new MyReduceFunction());
//AggregateFunction增量聚合函数
streamSource.flatMap(new MyFlatMap())
.keyBy(0)
.timeWindow(Time.minutes(1))
.aggregate(new MyAggregationFunction());
//全量聚合函数(全窗口计算)
streamSource.flatMap(new MyFlatMap())
.keyBy(0)
.timeWindow(Time.minutes(1))
.apply(new MyWindowFunction());
streamSource.flatMap(new MyFlatMap())
.keyBy(0)
.timeWindow(Time.minutes(1))
.process(new MyProcessWindowFunction());
}
static class MyFlatMap implements FlatMapFunction> {
public void flatMap(String s, Collector> collector) throws Exception {
String[] s1 = s.split(" ");
for (String s2:s1) {
collector.collect(new Tuple2(s2, 1));
}
}
}
static class MyReduceFunction implements ReduceFunction> {
//第一个参数表示已经增量聚合计算后的数据结果,第二个参数表示最新的未进行计算的数据
//reduce就是将两个数据进行归约为一条数据
public Tuple2 reduce(Tuple2 stringIntegerTuple2, Tuple2 t1) throws Exception {
return null;
}
}
//第一个泛型表示输入的数据类型,第二个泛型表示本次计算后的得到的结果,第三个泛型表示最终输出数据
/*
AggregateFunction与ReduceFunction的区别在于:
ReduceFunction:传进来什么类型的数据,计算结果和输出数据必须是相同类型的数据
而AggregateFunction则要更加灵活,AggregateFunction的输出数据类型、中间处理数据类型和输入数据类型都可以不同
*/
static class MyAggregationFunction implements AggregateFunction, Tuple2, Tuple2> {
//创建一个初始的数据对象
public Tuple2 createAccumulator() {
return new Tuple2(null, 0);
}
//将上一次的中间处理结果数据与本次输入数据相加,计算得到新的中间计算数据结果
public Tuple2 add(Tuple2 stringIntegerTuple2, Tuple2 stringIntegerTuple22) {
return new Tuple2(stringIntegerTuple22.f0, stringIntegerTuple2.f1+stringIntegerTuple22.f1);
}
//对中间计算数据结果进行处理,获取最终结果,也是输出的数据
public Tuple2 getResult(Tuple2 stringIntegerTuple2) {
return stringIntegerTuple2;
}
//合并分区操作
public Tuple2 merge(Tuple2 stringIntegerTuple2, Tuple2 acc1) {
return new Tuple2(stringIntegerTuple2.f0, stringIntegerTuple2.f1+acc1.f1);
}
}
//第一个泛型表示输入的数据类型,第二个泛型表示最后输出的数据结构,第三个泛型表示当前的key数据,是一个一元组,window就表示当前的窗口类型
static class MyWindowFunction implements WindowFunction, Tuple2, Tuple, TimeWindow> {
/**
* @Author
* @Description //TODO
* @Date 2021/6/10 21:40
* @param tuple 第一个参数就是当前数据的key
* @param timeWindow 第二个参数就是当前的窗口,通过这个参数我们可以获取当前窗口的一些信息
* @param input 第三个参数是当前窗口中的所有数据,通过这个参数我们可以遍历窗口中的数据进行计算处理
* @param output 第四个参数就是输出结果,将计算处理的数据结果写入到这里
* @Return void
* @Modified By:
*/
public void apply(Tuple tuple, TimeWindow timeWindow, Iterable> input, Collector> output) throws Exception {
//获取key
String key = tuple.getField(0);
//遍历窗口中的数据集合计算
for (Tuple2 tuple2:input) {
}
//输出结果
output.collect(new Tuple2());
}
}
//ProcessWindowFunction和WindowFunction其实是很类似的
static class MyProcessWindowFunction extends ProcessWindowFunction, Tuple2, Tuple, TimeWindow> {
/**
* @Author
* @Description //TODO
* @Date 2021/6/10 21:54
* @param tuple 第一个参数就是当前数据的key
* @param context 第二个参数就是当前的运行上下文,和WindowFunction相比能拿到更多的信息
* @param iterable 第三个参数是当前窗口中的所有数据,通过这个参数我们可以遍历窗口中的数据进行计算处理
* @param collector 第四个参数就是输出结果,将计算处理的数据结果写入到这里
* @Return void
* @Modified By:
*/
@Override
public void process(Tuple tuple, Context context, Iterable> iterable, Collector> collector) throws Exception {
}
}
}
(3)其他窗口api包括
trigger():定义什么时候窗口关闭,触发计算并输出结果
evictor():定义移除某些数据的逻辑
allowedLateness(Time lateness):允许处理迟到数据的时间,指在当前窗口本该关闭的时刻,不进行窗口关闭而是等待这个方法指定的时间之后在进行关闭窗口,但是同时也会立即计算输出一次当前窗口内所有数据的计算结果,在这段时间内如果继续接收到有属于本窗口的数据,这些数据也会参与计算,也就是说会有多次的计算结果
sideOutputLateData(OutputTag
getSideOutput(OutputTag