窗口操作中的另一大类就是全窗口函数。与增量聚合函数不同,全窗口函数需要先收集窗口中的数据,并在内部缓存起来,等到窗口要输出结果的时候再取出数据进行计算。
很明显,这就是典型的批处理思路了— —先攒数据,等一批都到齐了再正式启动处理流程。这样做毫无疑问是低效的:因为窗口全部的计算任务都积压在了要输出结果的那一瞬间,而在之前收集数据的漫长过程中却无所事事。这就好比平时不用功,到考试之前通宵抱佛脚,肯定不如把工夫花在日常积累上。
那为什么还需要有全窗口函数呢?这是因为有些场景下,我们要做的计算必须基于全部的数据才有效,这时做增量聚合就没什么意义了;另外,输出的结果有可能要包含上下文中的一些信息(比如窗口的起始时间),这是增量聚合函数做不到的。所以,我们还需要有更丰富的窗口计算方式,这就可以用全窗口函数来实现。
在 Flink 中,全窗口函数也有两种:WindowFunction 和ProcessWindowFunction。
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 就可以了。
ProcessWindowFunction 是 Window API 中最底层的通用窗口函数接口。之所以说它“最底层”,是因为除了可以拿到窗口中的所有数据之外,ProcessWindowFunction 还可以获取到一个“上下文对象”(Context)。这个上下文对象非常强大,不仅能够获取窗口信息,还可以访问当前的时间和状态信息。这里的时间就包括了处理时间(processing time)和事件时间水位线(event time watermark)。这就使得 ProcessWindowFunction 更加灵活、功能更加丰富。事实上,ProcessWindowFunction 是 Flink 底层 API——处理函数(process function)中的一员,当 然 , 这 些 好 处 是 以 牺 牲 性 能 和 资 源 为 代 价 的 。 作 为 一 个 全 窗 口 函 数 ,ProcessWindowFunction 同样需要将所有数据缓存下来、等到窗口触发计算时才使用。它其实就是一个增强版的WindowFunction。
需求:统计每5s中UV的次数
代码:需求实现
public class WindowProcessTest {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 读取数据,并提取时间戳、生成水位线
DataStream<Event> stream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.timestamp;
}
}));
stream.print("data");
// 统计每5秒的UV次数
stream.keyBy(data -> true)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(new UvCountByWindow())
.print();
env.execute();
}
//实现自定义ProcessWindowFunction,输出一条统计信息
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保存user
HashSet<String> userSet = new HashSet<>();
//遍历数据,去重
for (Event event:elements) {
userSet.add(event.user);
}
//获取UV信息
Integer uv = userSet.size();
//获取开始时间和结束时间
Long start = context.window().getStart();
Long end = context.window().getEnd();
//打印
out.collect("窗口 "+new Timestamp(start) + " ~ " + new Timestamp(end) + " UV值为:" + uv);
}
}
}
这里我们使用的是事件时间语义。定义 5 秒钟的滚动事件窗口后,直接使用ProcessWindowFunction 来定义处理的逻辑。我们可以创建一个 HashSet,将窗口所有数据的user 写入实现去重,最终得到 HashSet 的元素个数就是 UV 值。
当 然 , 这 里 我 们 并 没 有 用 到 上 下 文 中 其 他 信 息 , 所 以 其 实 没 有 必 要 使 用ProcessWindowFunction。全窗口函数因为运行效率较低,很少直接单独使用,往往会和增量聚合函数结合在一起,共同实现窗口的处理计算。
全窗口函数和增量聚合函数各有优缺点,增量聚合函数速度更快,更高效,延迟小,但是它包装不了想要的窗口信息,全窗口函数能拿到对应的窗口信息,但是,它是把所有数据都攒起来做了一个批处理,这个效率太低,延迟太高。
一般增量聚合函数调用getResult 方法之后,不是直接输出,而是传递给后面的全窗口函数中的process方法中的 element 传递,当前的 element 其实就是增量聚合的结果,这样就给两者的优势放在一起,变成了一个通用强大的用法
举例:结合增量聚合函数与全窗口函数一起统计每5秒UV出现的次数
代码如下:需求实现
public class UvCountExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 读取数据,并提取时间戳、生成水位线
DataStream<Event> stream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.timestamp;
}
}));
//使用AggregateFunction 和 ProcessWindowFunction结合计算UV
stream.keyBy(data -> true)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.aggregate(new UvAgg(), new UvCountResult())
.print();
stream.print("data");
env.execute();
}
//自定义实现 AggregateFunction 增量聚合计算UV值
public static class UvAgg implements AggregateFunction<Event, HashSet<String>, Long> {
@Override
public HashSet<String> createAccumulator() {
return new HashSet<>();
}
@Override
public HashSet<String> add(Event value, HashSet<String> accumulator) {
accumulator.add(value.user);
return accumulator;
}
@Override
public Long getResult(HashSet<String> accumulator) {
return (long) accumulator.size();
}
@Override
public HashSet<String> merge(HashSet<String> a, HashSet<String> b) {
return null;
}
}
//自定义实现 ProcessWindowFunction 包装窗口信息输出
public static class UvCountResult extends ProcessWindowFunction<Long, String, Boolean, TimeWindow> {
@Override
public void process(Boolean aBoolean, ProcessWindowFunction<Long, String, Boolean, TimeWindow>.Context context, Iterable<Long> elements, Collector<String> out) throws Exception {
//获取UV信息
Long uv = elements.iterator().next();
//获取开始时间和结束时间
Long start = context.window().getStart();
Long end = context.window().getEnd();
//打印
out.collect("窗口 " + new Timestamp(start) + " ~ " + new Timestamp(end) + " UV值为:" + uv);
}
}
}