目录

1. 增量聚合函数(incremental aggregation functions)

(1)归约函数(ReduceFunction)

(2)聚合函数(AggregateFunction)

2. 全窗口函数(full window functions)

(1)窗口函数(WindowFunction)

(2)处理窗口函数(ProcessWindowFunction)

3. 增量聚合和全窗口函数的结合使用

4、窗口的生命周期

1. 窗口的创建

2. 窗口计算的触发

3. 窗口的销毁

4. 窗口 API 调用总结



Flink连续两次开窗 flink 开窗函数_数据


        经窗口分配器处理之后,数据可以分配到对应的窗口中,而数据流经过转换得到的数据类


型是 WindowedStream 。这个类型并不是 DataStream ,所以并不能直接进行其他转换,而必须


进一步调用窗口函数,对收集到的数据进行处理计算之后,才能最终再次得到 DataStream ,如


图 6-21 所示。



        窗口函数定义了要对窗口中收集的数据做的计算操作,根据处理的方式可以分为两类:增



量聚合函数和全窗口函数。下面我们来进行分别讲解。




1. 增量聚合函数(incremental aggregation functions)




        为了提高实时性,我们可以再次将流处理的思路发扬光大:就像 DataStream 的简单聚合



一样,每来一条数据就立即进行计算,中间只要保持一个简单的聚合状态就可以了;区别只是



在于不立即输出结果,而是要等到窗口结束时间。等到窗口到了结束时间需要输出计算结果的



时候,我们只需要拿出之前聚合的状态直接输出,这无疑就大大提高了程序运行的效率和实时



性。




典型的增量聚合函数有两个: ReduceFunction 和 AggregateFunction。




(1)归约函数(ReduceFunction)




        最基本的聚合方式就是归约(reduce)。




        窗口函数中也提供了 ReduceFunction :只要基于 WindowedStream 调用 .reduce() 方法,然



后传入 ReduceFunction 作为参数,就可以指定以归约两个元素的方式去对窗口中数据进行聚



合了。这里的 ReduceFunction 其实与简单聚合时用到的 ReduceFunction 是同一个函数类接口,



所以使用方式也是完全一样的。




        ReduceFunction 中需要重写一个 reduce 方法,它的两个参数代表输入的两 个元素,而归约最终输出结果的数据类型,与输入的数据类型必须保持一致。也就是说,中间 聚合的状态和输出的结果,都和输入的数据类型是一样的。




下面是使用 ReduceFunction 进行增量聚合的代码示例



import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import 
org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindo
ws;
import org.apache.flink.streaming.api.windowing.time.Time;
import java.time.Duration;
public class WindowReduceExample {
 public static void main(String[] args) throws Exception {
 StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
 env.setParallelism(1);
 // 从自定义数据源读取数据,并提取时间戳、生成水位线
 SingleOutputStreamOperator<Event> stream = env.addSource(new 
ClickSource())
 .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoun
dedOutOfOrderness(Duration.ZERO)
 .withTimestampAssigner(new SerializableTimestampAssigner<Event>() 
{
 @Override
 public long extractTimestamp(Event element, long recordTimestamp) 
{
 return element.timestamp;
 }
 })); stream.map(new MapFunction<Event, Tuple2<String, 
Long>>() {
 @Override
 public Tuple2<String, Long> map(Event value) throws Exception { 
 // 将数据转换成二元组,方便计算
 return Tuple2.of(value.user, 1L);
 }
 })
 .keyBy(r -> r.f0)
 // 设置滚动事件时间窗口
 .window(TumblingEventTimeWindows.of(Time.seconds(5)))
 .reduce(new ReduceFunction<Tuple2<String, Long>>() {
 @Override
 public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, 
Tuple2<String, Long> value2) throws Exception {
 // 定义累加规则,窗口闭合时,向下游发送累加结果
 return Tuple2.of(value1.f0, value1.f1 + value2.f1);
 }
 })
 .print();
 env.execute();
 }
}



        代码中我们对每个用户的行为数据进行了开窗统计。与 word count 逻辑类似,首先将数



据转换成 (user, count) 的二元组形式(类型为 Tuple2<String, Long> ),每条数据对应的初始 count 值都是 1 ;然后按照用户 id 分组,在处理时间下开滚动窗口,统计每 5 秒内的用户行为数量。 对于窗口的计算,我们用 ReduceFunction 对 count 值做了增量聚合:窗口中会将当前的总 count 值保存成一个归约状态,每来一条数据,就会调用内部的 reduce 方法,将新数据中的 count



值叠加到状态上,并得到新的状态保存起来。等到了 5 秒窗口的结束时间,就把归约好的状态



直接输出。




        这里需要注意,我们经过窗口聚合转换输出的数据,数据类型依然是二元组 Tuple2<String,



Long> 。




(2)聚合函数(AggregateFunction)




        ReduceFunction 可以解决大多数归约聚合的问题,但是这个接口有一个限制,就是聚合状



态的类型、输出结果的类型都必须和输入数据类型一样。这就迫使我们必须在聚合前,先将数



据转换( map )成预期结果类型;而在有些情况下,还需要对状态进行进一步处理才能得到输



出结果,这时它们的类型可能不同,使用 ReduceFunction 就会非常麻烦。




        例如,如果我们希望计算一组数据的平均值,应该怎样做聚合呢?很明显,这时我们需要



计算两个状态量:数据的总和( sum ),以及数据的个数( count ),而最终输出结果是两者的商



( sum/count )。如果用 ReduceFunction ,那么我们应该先把数据转换成二元组 (sum, count) 的形



式,然后进行归约聚合,最后再将元组的两个元素相除转换得到最后的平均值。本来应该只是



一个任务,可我们却需要 map-reduce-map 三步操作,这显然不够高效。




        于是自然可以想到,如果取消类型一致的限制,让输入数据、中间状态、输出结果三者类



型都可以不同,不就可以一步直接搞定了吗?




        Flink 的 Window API 中的 aggregate 就提供了这样的操作。直接基于 WindowedStream 调



用 .aggregate() 方法,就可以定义更加灵活的窗口聚合操作。这个方法需要传入一个



AggregateFunction 的实现类作为参数。 AggregateFunction 在源码中的定义如下:



public interface AggregateFunction<IN, ACC, OUT> extends Function, Serializable 
{
 ACC createAccumulator();
 ACC add(IN value, ACC accumulator);
 OUT getResult(ACC accumulator);
155
 ACC merge(ACC a, ACC b);
}



        AggregateFunction 可以看作是 ReduceFunction 的通用版本,这里有三种类型:输入类型



( IN )、累加器类型( ACC )和输出类型( OUT )。输入类型 IN 就是输入流中元素的数据类型;



累加器类型 ACC 则是我们进行聚合的中间状态类型;而输出类型当然就是最终计算结果的类



型了。




接口中有四个方法:



⚫ createAccumulator() :创建一个累加器,这就是为聚合创建了一个初始状态,每个聚



合任务只会调用一次。



⚫ add() :将输入的元素添加到累加器中。这就是基于聚合状态,对新来的数据进行进



一步聚合的过程。方法传入两个参数:当前新到的数据 value ,和当前的累加器



accumulator ;返回一个新的累加器值,也就是对聚合状态进行更新。每条数据到来之



后都会调用这个方法。



⚫ getResult() :从累加器中提取聚合的输出结果。也就是说,我们可以定义多个状态,



然后再基于这些聚合的状态计算出一个结果进行输出。比如之前我们提到的计算平均



值,就可以把 sum 和 count 作为状态放入累加器,而在调用这个方法时相除得到最终



结果。这个方法只在窗口要输出结果时调用。



⚫ merge() :合并两个累加器,并将合并后的状态作为一个累加器返回。这个方法只在



需要合并窗口的场景下才会被调用;最常见的合并窗口( Merging Window )的场景



就是会话窗口( Session Windows )。




        所以可以看到,AggregateFunction 的工作原理是:首先调用 createAccumulator() 为任务初



始化一个状态 ( 累加器 ) ;而后每来一个数据就调用一次 add() 方法,对数据进行聚合,得到的



结果保存在状态中;等到了窗口需要输出时,再调用 getResult() 方法得到计算结果。很明显,



与 ReduceFunction 相同, AggregateFunction 也是增量式的聚合;而由于输入、中间状态、输



出的类型可以不同,使得应用更加灵活方便。




        下面来看一个具体例子。我们知道,在电商网站中,PV (页面浏览量)和 UV (独立访客



数)是非常重要的两个流量指标。一般来说, PV 统计的是所有的点击量;而对用户 id 进行去



重之后,得到的就是 UV 。所以有时我们会用 PV/UV 这个比值,来表示“人均重复访问量”,



也就是平均每个用户会访问多少次页面,这在一定程度上代表了用户的粘度。




代码实现如下:



import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import 
org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import java.util.HashSet;
public class WindowAggregateFunctionExample {
 public static void main(String[] args) throws Exception {
 StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
 env.setParallelism(1);
 SingleOutputStreamOperator<Event> stream = env.addSource(new 
ClickSource())
 .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMono
tonousTimestamps()
 .withTimestampAssigner(new SerializableTimestampAssigner<Event>() 
{
 @Override
 public long extractTimestamp(Event element, long recordTimestamp) 
{
 return element.timestamp;
 }
 }));
 // 所有数据设置相同的 key,发送到同一个分区统计 PV 和 UV,再相除
 stream.keyBy(data -> true)
 .window(SlidingEventTimeWindows.of(Time.seconds(10), 
Time.seconds(2)))
 .aggregate(new AvgPv())
 .print();
 env.execute();
 }
 public static class AvgPv implements AggregateFunction<Event, 
Tuple2<HashSet<String>, Long>, Double> {
 @Override
 public Tuple2<HashSet<String>, Long> createAccumulator() {

 // 创建累加器
 return Tuple2.of(new HashSet<String>(), 0L);
 }
 @Override
 public Tuple2<HashSet<String>, Long> add(Event value, 
Tuple2<HashSet<String>, Long> accumulator) {
 // 属于本窗口的数据来一条累加一次,并返回累加器
 accumulator.f0.add(value.user);
 return Tuple2.of(accumulator.f0, accumulator.f1 + 1L);
 }
 @Override
 public Double getResult(Tuple2<HashSet<String>, Long> accumulator) {
 // 窗口闭合时,增量聚合结束,将计算结果发送到下游
 return (double) accumulator.f1 / accumulator.f0.size();
 }
 @Override
 public Tuple2<HashSet<String>, Long> merge(Tuple2<HashSet<String>, Long> 
a, Tuple2<HashSet<String>, Long> b) {
 return null;
 }
 }
}



·        代码中我们创建了事件时间滑动窗口,统计 10 秒钟的“人均 PV ”,每 2 秒统计一次。由



于聚合的状态还需要做处理计算,因此窗口聚合时使用了更加灵活的 AggregateFunction 。为了



统计 UV ,我们用一个 HashSet 保存所有出现过的用户 id ,实现自动去重;而 PV 的统计则类



似一个计数器,每来一个数据加一就可以了。所以这里的状态,定义为包含一个 HashSet 和一



个 count 值的二元组( Tuple2<HashSet<String>, Long> ),每来一条数据,就将 user 存入 HashSet , 同时 count 加 1 。这里的 count 就是 PV ,而 HashSet 中元素的个数( size )就是 UV ;所以最终 窗口的输出结果,就是它们的比值。




        这里没有涉及会话窗口,所以 merge() 方法可以不做任何操作。




        另外,Flink 也为窗口的聚合提供了一系列预定义的简单聚合方法,可以直接基于



WindowedStream 调用。主要包括 .sum()/max()/maxBy()/min()/minBy() ,与 KeyedStream 的简单



聚合非常相似。它们的底层,其实都是通过 AggregateFunction 来实现的。




        通过 ReduceFunction 和 AggregateFunction 我们可以发现,增量聚合函数其实就是在用流



处理的思路来处理有界数据集,核心是保持一个聚合状态,当数据到来时不停地更新状态。这



就是 Flink 所谓的“有状态的流处理”,通过这种方式可以极大地提高程序运行的效率,所以



在实际应用中最为常见。




2. 全窗口函数(full window functions)




        窗口操作中的另一大类就是全窗口函数。与增量聚合函数不同,全窗口函数需要先收集窗



口中的数据,并在内部缓存起来,等到窗口要输出结果的时候再取出数据进行计算。




        很明显,这就是典型的批处理思路了——先攒数据,等一批都到齐了再正式启动处理流程。



这样做毫无疑问是低效的:因为窗口全部的计算任务都积压在了要输出结果的那一瞬间,而在



之前收集数据的漫长过程中却无所事事。这就好比平时不用功,到考试之前通宵抱佛脚,肯定



不如把工夫花在日常积累上。




        那为什么还需要有全窗口函数呢?这是因为有些场景下,我们要做的计算必须基于全部的



数据才有效,这时做增量聚合就没什么意义了;另外,输出的结果有可能要包含上下文中的一



些信息(比如窗口的起始时间),这是增量聚合函数做不到的。所以,我们还需要有更丰富的



窗口计算方式,这就可以用全窗口函数来实现。




在 Flink 中,全窗口函数也有两种: WindowFunction 和 ProcessWindowFunction。




(1)窗口函数(WindowFunction)




        WindowFunction 字面上就是“窗口函数”,它其实是老版本的通用窗口函数接口。我们可



以基于 WindowedStream 调用 .apply() 方法,传入一个 WindowFunction 的实现类。



stream
 .keyBy(<key selector>)
 .window(<window assigner>)
 .apply(new MyWindowFunction());



        这个类中可以获取到包含窗口所有数据的可迭代集合(Iterable ),还可以拿到窗口



( Window )本身的信息。 WindowFunction 接口在源码中实现如下:



public interface WindowFunction<IN, OUT, KEY, W extends Window> extends Function, 
Serializable {
void apply(KEY key, W window, Iterable<IN> input, Collector<OUT> out) throws 
Exception;
}



        当窗口到达结束时间需要触发计算时,就会调用这里的 apply 方法。我们可以从 input 集



合中取出窗口收集的数据,结合 key 和 window 信息,通过收集器( Collector )输出结果。这



里 Collector 的用法,与 FlatMapFunction 中相同。




        不过我们也看到了,WindowFunction 能提供的上下文信息较少,也没有更高级的功能。




        事实上,它的作用可以被 ProcessWindowFunction 全覆盖,所以之后可能会逐渐弃用。一般在



实际应用,直接使用 ProcessWindowFunction 就可以了。




(2)处理窗口函数(ProcessWindowFunction)




        ProcessWindowFunction 是 Window API 中最底层的通用窗口函数接口。之所以说它“最底



层”,是因为除了可以拿到窗口中的所有数据之外, ProcessWindowFunction 还可以获取到一个



“上下文对象”( Context )。这个上下文对象非常强大,不仅能够获取窗口信息,还可以访问当



前的时间和状态信息。这里的时间就包括了处理时间( processing time )和事件时间水位线( eventtime watermark)。这就使得 ProcessWindowFunction 更加灵活、功能更加丰富。事实上, ProcessWindowFunction 是 Flink 底层 API ——处理函数( process function )中的一员,关于处 理函数我们会在后续章节展开讲解。




        当 然 , 这 些 好 处 是 以 牺 牲 性 能 和 资 源 为 代 价 的 。 作 为 一 个 全 窗 口 函 数 ,



ProcessWindowFunction 同样需要将所有数据缓存下来、等到窗口触发计算时才使用。它其实



就是一个增强版的 WindowFunction 。




        具体使用跟 WindowFunction 非常类似,我们可以基于 WindowedStream 调用 .process() 方 法,传入一个 ProcessWindowFunction 的实现类。




下面是一个电商网站统计每小时 UV 的例子:



import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import
org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import 
org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.sql.Timestamp;
import java.util.HashSet;
public class UvCountByWindowExample {
 public static void main(String[] args) throws Exception {
 StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
 env.setParallelism(1);
 SingleOutputStreamOperator<Event> stream = env.addSource(new 
ClickSource())
 .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBound
edOutOfOrderness(Duration.ZERO)
 .withTimestampAssigner(new 
SerializableTimestampAssigner<Event>() {
 @Override
 public long extractTimestamp(Event element, long 
recordTimestamp) {
 return element.timestamp;
 }
 }));
 // 将数据全部发往同一分区,按窗口统计 UV
 stream.keyBy(data -> true)
 .window(TumblingEventTimeWindows.of(Time.seconds(10)))
 .process(new UvCountByWindow())
 .print();
 env.execute();
 }
 // 自定义窗口处理函数
 public static class UvCountByWindow extends ProcessWindowFunction<Event, 
String, Boolean, TimeWindow>{
 @Override
 public void process(Boolean aBoolean, Context context, Iterable<Event> 
elements, Collector<String> out) throws Exception {
 HashSet<String> userSet = new HashSet<>();
 // 遍历所有数据,放到 Set 里去重
 for (Event event: elements){
 userSet.add(event.user);
 }
 // 结合窗口信息,包装输出内容
 Long start = context.window().getStart();
 Long end = context.window().getEnd();
 out.collect(" 窗 口 : " + new Timestamp(start) + " ~ " + new 
Timestamp(end)
 + " 的独立访客数量是:" + userSet.size());
 }
 }
}



        这里我们使用的是事件时间语义。定义 10 秒钟的滚动事件窗口后,直接使用



ProcessWindowFunction 来定义处理的逻辑。我们可以创建一个 HashSet ,将窗口所有数据的



userId 写入实现去重,最终得到 HashSet 的元素个数就是 UV 值。




        当 然 , 这 里 我 们 并 没 有 用 到 上 下 文 中 其 他 信 息 , 所 以 其 实 没 有 必 要 使 用



ProcessWindowFunction 。全窗口函数因为运行效率较低,很少直接单独使用,往往会和增量



聚合函数结合在一起,共同实现窗口的处理计算。




3. 增量聚合和全窗口函数的结合使用




        我们已经了解了 Window API 中两类窗口函数的用法,下面我们先来做个简单的总结。



增量聚合函数处理计算会更高效。举一个最简单的例子,对一组数据求和。大量的数据连



续不断到来,全窗口函数只是把它们收集缓存起来,并没有处理;到了窗口要关闭、输出结果



的时候,再遍历所有数据依次叠加,得到最终结果。而如果我们采用增量聚合的方式,那么只



需要保存一个当前和的状态,每个数据到来时就会做一次加法,更新状态;到了要输出结果的



时候,只要将当前状态直接拿出来就可以了。增量聚合相当于把计算量“均摊”到了窗口收集



数据的过程中,自然就会比全窗口聚合更加高效、输出更加实时。




        而全窗口函数的优势在于提供了更多的信息,可以认为是更加“通用”的窗口操作。它只



负责收集数据、提供上下文相关信息,把所有的原材料都准备好,至于拿来做什么我们完全可



以任意发挥。这就使得窗口计算更加灵活,功能更加强大。




        所以在实际应用中,我们往往希望兼具这两者的优点,把它们结合在一起使用。Flink 的



Window API 就给我们实现了这样的用法。




        我们之前在调用 WindowedStream 的 .reduce() 和 .aggregate() 方法时,只是简单地直接传入



了一个 ReduceFunction 或 AggregateFunction 进行增量聚合。除此之外,其实还可以传入第二



个参数:一个全窗口函数,可以是 WindowFunction 或者 ProcessWindowFunction 。




// ReduceFunction 与 WindowFunction 结合
public <R> SingleOutputStreamOperator<R> reduce(
 ReduceFunction<T> reduceFunction, WindowFunction<T, R, K, W> function) 
// ReduceFunction 与 ProcessWindowFunction 结合
public <R> SingleOutputStreamOperator<R> reduce(
 ReduceFunction<T> reduceFunction, ProcessWindowFunction<T, R, K, W> 
function)
// AggregateFunction 与 WindowFunction 结合
public <ACC, V, R> SingleOutputStreamOperator<R> aggregate(
 AggregateFunction<T, ACC, V> aggFunction, WindowFunction<V, R, K, W> 
windowFunction)
// AggregateFunction 与 ProcessWindowFunction 结合
public <ACC, V, R> SingleOutputStreamOperator<R> aggregate(
 AggregateFunction<T, ACC, V> aggFunction,
 ProcessWindowFunction<V, R, K, W> windowFunction)



         这样调用的处理机制是:基于第一个参数(增量聚合函数)来处理窗口数据,每来一个数



据就做一次聚合;等到窗口需要触发计算时,则调用第二个参数(全窗口函数)的处理逻辑输



出结果。需要注意的是,这里的全窗口函数就不再缓存所有数据了,而是直接将增量聚合函数



的结果拿来当作了 Iterable 类型的输入。一般情况下,这时的可迭代集合中就只有一个元素了。




        下面我们举一个具体的实例来说明。在网站的各种统计指标中,一个很重要的统计指标就



是热门的链接;想要得到热门的 url ,前提是得到每个链接的“热门度”。一般情况下,可以用



url 的浏览量(点击量)表示热门度。我们这里统计 10 秒钟的 url 浏览量,每 5 秒钟更新一次;



另外为了更加清晰地展示,还应该把窗口的起始结束时间一起输出。我们可以定义滑动窗口,



并结合增量聚合函数和全窗口函数来得到统计结果。




        具体实现代码如下:



import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import 
org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import 
org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
public class UrlViewCountExample {
 public static void main(String[] args) throws Exception {
 StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
 env.setParallelism(1);
 SingleOutputStreamOperator<Event> stream = env.addSource(new 
ClickSource())
 .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonot
onousTimestamps()
 .withTimestampAssigner(new 
SerializableTimestampAssigner<Event>() {
 @Override
 public long extractTimestamp(Event element, long 
recordTimestamp) {
 return element.timestamp;
 }
 }));
 // 需要按照 url 分组,开滑动窗口统计
 stream.keyBy(data -> data.url)
 .window(SlidingEventTimeWindows.of(Time.seconds(10), 
Time.seconds(5)))
 // 同时传入增量聚合函数和全窗口函数
 .aggregate(new UrlViewCountAgg(), new UrlViewCountResult())
 .print();
 env.execute();
 }
 // 自定义增量聚合函数,来一条数据就加一
 public static class UrlViewCountAgg implements AggregateFunction<Event, Long, 
Long> {
 @Override
 public Long createAccumulator() {
 return 0L;
 }
 @Override
 public Long add(Event value, Long accumulator) {
 return accumulator + 1;
 }
 @Override
 public Long getResult(Long accumulator) {
 return accumulator;
 }
 @Override
 public Long merge(Long a, Long b) {
 return null;
 }
 }
 // 自定义窗口处理函数,只需要包装窗口信息
 public static class UrlViewCountResult extends ProcessWindowFunction<Long, 
UrlViewCount, String, TimeWindow> {
 @Override
 public void process(String url, Context context, Iterable<Long> elements, 
Collector<UrlViewCount> out) throws Exception {
 // 结合窗口信息,包装输出内容
 Long start = context.window().getStart();
 Long end = context.window().getEnd();
 // 迭代器中只有一个元素,就是增量聚合函数的计算结果
 out.collect(new UrlViewCount(url, elements.iterator().next(), start, 
end));
 }
 }
}



        这里我们为了方便处理,单独定义了一个 POJO 类 UrlViewCount 来表示聚合输出结果的



数据类型,包含了 url 、浏览量以及窗口的起始结束时间。



import java.sql.Timestamp;
public class UrlViewCount {
 public String url;
 public Long count;
 public Long windowStart;
 public Long windowEnd;
 public UrlViewCount() {
 }
 public UrlViewCount(String url, Long count, Long windowStart, Long windowEnd) 
{
 this.url = url;
 this.count = count;
 this.windowStart = windowStart;
 this.windowEnd = windowEnd;
 }
 @Override
 public String toString() {
 return "UrlViewCount{" +
 "url='" + url + '\'' +
 ", count=" + count +
 ", windowStart=" + new Timestamp(windowStart) +
 ", windowEnd=" + new Timestamp(windowEnd) +
 '}';
 }
}



        代码中用一个 AggregateFunction 来实现增量聚合,每来一个数据就计数加一;得到的结



果交给 ProcessWindowFunction ,结合窗口信息包装成我们想要的 UrlViewCount ,最终输出统



计结果。



        注:ProcessWindowFunction 是处理函数中的一种,后面我们会详细讲解。这里只用它来



将增量聚合函数的输出结果包裹一层窗口信息。




        窗口处理的主体还是增量聚合,而引入全窗口函数又可以获取到更多的信息包装输出,这



样的结合兼具了两种窗口函数的优势,在保证处理性能和实时性的同时支持了更加丰富的应用



场景。




4、窗口的生命周期

1. 窗口的创建




        窗口的类型和基本信息由窗口分配器(window assigners )指定,但窗口不会预先创建好,



而是由数据驱动创建。当第一个应该属于这个窗口的数据元素到达时,就会创建对应的窗口。




2. 窗口计算的触发




        除了窗口分配器,每个窗口还会有自己的窗口函数(window functions )和触发器( trigger )。 窗口函数可以分为增量聚合函数和全窗口函数,主要定义了窗口中计算的逻辑;而触发器则是指定调用窗口函数的条件。




        对于不同的窗口类型,触发计算的条件也会不同。例如,一个滚动事件时间窗口,应该在



水位线到达窗口结束时间的时候触发计算,属于“定点发车”;而一个计数窗口,会在窗口中



元素数量达到定义大小时触发计算,属于“人满就发车”。所以 Flink 预定义的窗口类型都有



对应内置的触发器。




        对于事件时间窗口而言,除去到达结束时间的“定点发车”,还有另一种情形。当我们设



置了允许延迟,那么如果水位线超过了窗口结束时间、但还没有到达设定的最大延迟时间,这



期间内到达的迟到数据也会触发窗口计算。这类似于没有准时赶上班车的人又追上了车,这时



车要再次停靠、开门,将新的数据整合统计进来。




3. 窗口的销毁




        一般情况下,当时间达到了结束点,就会直接触发计算输出结果、进而清除状态销毁窗口。



这时窗口的销毁可以认为和触发计算是同一时刻。这里需要注意, Flink 中只对时间窗口



( TimeWindow )有销毁机制;由于计数窗口( CountWindow )是基于全局窗口( GlobalWindw )



实现的,而全局窗口不会清除状态,所以就不会被销毁。




        在特殊的场景下,窗口的销毁和触发计算会有所不同。事件时间语义下,如果设置了允许



延迟,那么在水位线到达窗口结束时间时,仍然不会销毁窗口;窗口真正被完全删除的时间点,



是窗口的结束时间加上用户指定的允许延迟时间。




4. 窗口 API 调用总结




        到目前为止,我们已经彻底明白了 Flink 中窗口的概念和 Window API 的调用,我们再用



一张图做一个完整总结,如图 6-22 所示。



Flink连续两次开窗 flink 开窗函数_大数据_02



 



        Window API 首先按照时候按键分区分成两类。 keyBy 之后的 KeyedStream ,可以调



用 .window() 方法声明按键分区窗口( Keyed Windows );而如果不做 keyBy , DataStream 也可



以直接调用 .windowAll() 声明非按键分区窗口。之后的方法调用就完全一样了。




        接下来首先是通过.window()/.windowAll() 方法定义窗口分配器,得到 WindowedStream ;



然 后 通 过 各 种 转 换 方 法 ( reduce/aggregate/apply/process ) 给 出 窗 口 函 数



(ReduceFunction/AggregateFunction/ProcessWindowFunction) ,定义窗口的具体计算处理逻辑,



转换之后重新得到 DataStream 。这两者必不可少,是窗口算子( WindowOperator )最重要的组



成部分。




        此外,在这两者之间,还可以基于 WindowedStream 调用 .trigger() 自定义触发器、调



用 .evictor() 定义移除器、调用 .allowedLateness() 指定允许延迟时间、调用 .sideOutputLateData()



将迟到数据写入侧输出流,这些都是可选的 API ,一般不需要实现。而如果定义了侧输出流,



可以基于窗口聚合之后的 DataStream 调用 .getSideOutput() 获取侧输出流。