目录
一、基本处理函数(ProcessFunction)
1、处理函数的概念
2、ProcessFunction 解析
(1)抽象方法.processElement()
(2)非抽象方法.onTimer()
3、处理函数的分类
(1)ProcessFunction
(2)KeyedProcessFunction
(3)ProcessWindowFunction
(4)ProcessAllWindowFunction
(5)CoProcessFunction
(6)ProcessJoinFunction
(7)BroadcastProcessFunction
(8)KeyedBroadcastProcessFunction
二、应用案例——Top N
1、使用 ProcessAllWindowFunction
2、使用 KeyedProcessFunction
三、侧输出流(Side Output)
在更底层,我们可以不定义任何具体的算子(比如 map, filter ,或者 window ),而只是提 炼出一个统一的“处理”(process )操作——它是所有转换算子的一个概括性的表达,可以自 定义处理逻辑,所以这一层接口就被叫作“处理函数”(process function )。在处理函数中,我们直面的就是数据流中最基本的元素:数据事件(event )、状态( state )以及时间(time )。这就相当于对流有了完全的控制权。
但是无论那种算子,如果我们想要访问事件的时间戳,或者当前的水位线信息,都是完全做不到的。跟时间相关的操作,目前我们 只会用窗口来处理。而在很多应用需求中,要求我们对时间有更精细的控制,需要能够获取水位线,甚至要“把控时间”、定义什么时候做什么事,这就不是基本的时间窗口能够实现的了。处理函数提供了一个“ 定时服务” (TimerService),我们可以通过它访问流中的事件、时间戳、水位线 ,甚至可以注册“定时事件”。而且处理函数 继承了 AbstractRichFunction 抽象类, 所以拥有富函数类的所有特性,同样可以访问状态(state)和其他运行时信息。此外,处理函数还可以 直接将数据输出到侧输出流(side output )中。所以, 处理函数是最为灵活的处理方法,可以实现各种自定义的业务逻辑;同时也是整个 DataStream API 的底层础。
用于“处理元素”,定义了处理的核心逻辑。这个方法对于流中的每个元素都会调用一次, 参数包括三个:输入数据值 value,上下文 ctx ,以及“收集器”( Collector)out 。方法没有返 回值,处理之后的输出数据是通过收集器 out 来定义的。 ProcessFunction 可以轻松实现 flatMap 这样的基本转换功能(当然 map 、 filter 更不在话下);而通过富函数提供的获取上下文方法 .getRuntimeContext() ,也可以自定义状态(state )进行处理。
env.addSource()
.process(new ProcessFunction() {
@Override
public void processElement(Event event, ProcessFunction.Context context, Collector collector) throws Exception {
if(event.user.equals("Mary")){
collector.collect(event.user);
}else if(event.user.equals("Bob")){
collector.collect(event.user);
collector.collect(event.user);
}
// 获 取 当 前 的 水 位 线 打 印 输 出
System.out.println(context.timerService().currentWatermark());
}
}).print();
用于定义定时触发的操作,这个方法只有在注册好的定时器触发的时候才会调用,而定时器是通过“定时服务”TimerService 来注册的。打个比方,注册定时器(timer)就是设了一个闹钟,到了设定时间就会响;而.onTimer()中定义的,就是闹钟响的时候要做的事。所以它本质上是一个基于时间的“回调”(callback)方法,通过时间的进展来触发;在事件时间语义下就是由水位线(watermark)来触发了。与.processElement()类似,定时方法.onTimer()也有三个参数:时间戳(timestamp),上下文(ctx),以及收集器(out)。
定时器真正的设置需要用到上下文 ctx 中的定时服务。在 Flink 中,只有“按键分区流”KeyedStream 才支持设置定时器的操作。
对于不同类型的流,其实都可以直接调用.process()方法进行自定义处理,这时传入的参数就都叫作处理函数。Flink 提供了 8 个不同的处理函数:
// 要用定时器,必须基于KeyedStream
stream.keyBy(data -> true)
.process(new KeyedProcessFunction() {
@Override
public void processElement(Event value, Context ctx, Collector out) throws Exception {
Long currTs = ctx.timerService().currentProcessingTime();
out.collect("数据到达,到达时间:" + new Timestamp(currTs));
// 注册一个10秒后的定时器
ctx.timerService().registerProcessingTimeTimer(currTs + 10 * 1000L);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector out) throws Exception {
out.collect("定时器触发,触发时间:" + new Timestamp(timestamp));
}
})
.print();
stream.keyBy( t -> t.f0 )
.window( TumblingEventTimeWindows.of(Time.seconds(10)) )
.process(new MyProcessWindowFunction())
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 由于使用事件时间,需要先定义时间戳和水位线
SingleOutputStreamOperator eventStream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(
WatermarkStrategy.forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.timestamp;
}
})
);
// 只需要url就可以统计数量,所以转换成String直接开窗统计
SingleOutputStreamOperator result = eventStream.map(new MapFunction() {
@Override
public String map(Event event) throws Exception {
return event.url;
}
}).windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5))) // 开滑动窗口
.process(new ProcessAllWindowFunction() {
@Override
public void process(Context context, Iterable iterable, Collector collector) throws Exception {
HashMap urlCountMap = new HashMap<>();
// 遍历窗口中的数据,将浏览量保存到HashMap中
for (String url : iterable) {
urlCountMap.put(url, urlCountMap.getOrDefault(url, 0L) + 1L);
}
ArrayList> mapList = new ArrayList<>();
for (String key : urlCountMap.keySet()) {
mapList.add(Tuple2.of(key, urlCountMap.get(key)));
}
mapList.sort(new Comparator>() {
@Override
public int compare(Tuple2 o1, Tuple2 o2) {
return o2.f1.intValue() - o1.f1.intValue();
}
});
// 取排序后的前两名,构建输出结果
StringBuilder result = new StringBuilder();
result.append("========================================\n");
for (int i = 0; i < 2; i++) {
Tuple2 temp = mapList.get(i);
String info = "浏览量No." + (i + 1) +
" url:" + temp.f0 +
" 浏览量:" + temp.f1 +
" 窗口结束时间:" + new Timestamp(context.window().getEnd()) + "\n";
result.append(info);
}
result.append("========================================\n");
collector.collect(result.toString());
}
});
result.print();
env.execute();
}
OutputTag outputTag = new OutputTag("side-output") {};
DataStream stringStream = longStream.getSideOutput(outputTag);