Flink 全量窗口聚合函数
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)和事件时间水位线(event time watermark)。这就使得ProcessWindowFunction更加灵活、功能更加丰富。事实上,ProcessWindowFunction是Flink底层API——处理函数(process function)中的一员,当然,这些好处是以牺牲性能和资源为代价的。作为一个全窗口函数,ProcessWindowFunction同样需要将所有数据缓存下来、等到窗口触发计算时才使用。它其实就是一个增强版的WindowFunction。具体使用跟WindowFunction非常类似,可以基于WindowedStream调用.process()方法,传入一个ProcessWindowFunction的实现类。下面是一个电商网站统计每小时UV的例子:
/** * 统计每10s 的 UV */ public class WindowProcessFunction0627 { public static void main(String[] args) throws Exception { //获取执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); SingleOutputStreamOperator<Event> eventStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks( WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3)) .withTimestampAssigner( new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event element, long recordTimestamp) { return element.timestamp; } } ) ); //方案1 使用全量窗口函数实现:10s 输出一次Uv eventStream.keyBy(data -> true) .window(TumblingEventTimeWindows.of(Time.seconds(10))) .process(new MyProcessWindowFun0627()).print(); //execut env.execute(); } public static class MyProcessWindowFun0627 extends ProcessWindowFunction<Event, String, Boolean, TimeWindow> { @Override public void process(Boolean aBoolean, Context context, Iterable<Event> elements, Collector<String> out) throws Exception { //声明用户数据的结合 HashSet<String> uSet = new HashSet<>(); //循环保存 for (Event element : elements) { uSet.add(element.user); } //获取 Uv Long aLong = Long.valueOf(uSet.size()); //获取窗口开始结束时间 long end = context.window().getEnd(); long start = context.window().getStart(); out.collect("窗口 " + new Timestamp(start)+ " ~ " + new Timestamp(end) + " --> " +aLong); } } }
输出结果
窗口 2022-06-27 22:46:20.0 ~ 2022-06-27 22:46:30.0 --> 3 窗口 2022-06-27 22:46:30.0 ~ 2022-06-27 22:46:40.0 --> 5 窗口 2022-06-27 22:46:40.0 ~ 2022-06-27 22:46:50.0 --> 4 窗口 2022-06-27 22:46:50.0 ~ 2022-06-27 22:47:00.0 --> 4
这里使用的是事件时间语义。定义10秒钟的滚动事件窗口后,直接使用ProcessWindowFunction来定义处理的逻辑。我们可以创建一个HashSet,将窗口所有数据的userId写入实现去重,最终得到HashSet的元素个数就是UV值。当然,这里我们并没有用到上下文中其他信息,所以其实没有必要使用ProcessWindowFunction。全窗口函数因为运行效率较低,很少直接单独使用,往往会和增量聚合函数结合在一起,共同实现窗口的处理计算。