FLink处理函数ProcessFunction、KeyedProcessFunction、ProcessWindowFunction、 ProcessAllWindowFunction
一、处理函数简介
在底层,我们可以不定义任何具体的算子(比如 map,filter,或者 window),而只是提炼出一个统一的“处理”(process)操作——它是所有转换算子的一个概括性的表达,可以自定义处理逻辑,所以这一层接口就被叫作“处理函数”(process function)。在处理函数中,我们直面的就是数据流中最基本的元素:数据事件(event)、状态(state)以及时间(time)。这就相当于对流有了完全的控制权。处理函数比较抽象,没有具体的操作,所以对于一些常见的简单应用(比如求和、开窗口)会显得有些麻烦;不过正是因为它不限定具体做什么,所以理论上我们可以做任何事情,实现所有需求。
二、几种处理函数简介
1、ProcessFunction是用于处理数据流的通用函数。它是一个抽象类,定义了处理数据流的常用方法,如processElement,onTimer等。您可以扩展ProcessFunction类并重写这些方法,以便在Flink程序中执行复杂的数据流处理逻辑。
2、KeyedProcessFunction是ProcessFunction的特殊类型,用于处理带有键的数据流。它定义了额外的方法,如getKey,context.timerService()等,用于访问数据流中每个元素的键以及在处理函数中安排定时器。
3、ProcessWindowFunction和ProcessAllWindowFunction是用于处理时间窗口的特殊函数。它们提供了一个process方法,用于在每个窗口中对数据进行处理。ProcessWindowFunction接受带有键的数据流,并且每个窗口都对应于一个键,而ProcessAllWindowFunction接受不带键的数据流,并且每个窗口都包含整个数据流。
三、处理函数详细介绍
1、基本处理函数ProcessFunction,继承AbstractRichFunction
基本处理函数提供了一个“定时服务”(TimerService),我们可以通过它访问流中的事件(event)、时间戳(timestamp)、水位线(watermark),甚至可以注册“定时事件”。而且处理函数继承了 AbstractRichFunction 抽象类,所以拥有富函数类的所有特性,同样可以访问状态(state)和其他运行时信息。此外,处理函数还可以直接将数据输出到侧输出流(side output)中。所以,处理函数是最为灵活的处理方法,可以实现各种自定义的业务逻辑;同时也是整个 DataStream API 的底层基础。
(1)使用方法
主程序中调用自定义的处理类
stream.process(new MyProcessFunction())
新建MyProcessFunction类
自定义处理函数主要继承了ProcessFunction。有两个泛型类型参数:I 表示 Input,也就是输入的数据类型;O 表示 Output,也就是处理完成之后输出的数据类型
public class MyProcessFunction extends ProcessFunction<I, O>{
//打开时处理
@Override
public void open(Configuration parameters) throws Exception {}
/**
* 用于处理元素
* value:当前流中的输入元素,也就是正在处理的数据,类型与流中数据类
* ctx:类型是 ProcessFunction 中定义的内部抽象类 Context,表示当前运行的上下文,可以获取到当前的时间戳,
* 并提供了用于查询时间和注册定时器的“定时服务”(TimerService),以及可以将数据发送到“侧输出流”(side output)的方法.output()。
* out:"收集器"(类型为 Collector),用于返回输出数据。使用方式与 flatMap算子中的收集器完全一样,直接调用 out.collect()方法就可以向下游发出一个数据。
*/
@Override
public void processElement(I value, Context ctx, Collector<O> out) throws Exception {}
//关闭时处理
@Override
public void close() throws Exception {}
}
(2)引用技术简介
上文提到的TimerService,是 Flink 关于时间和定时器的基础服务接口,包含以下六个方法:
处理函数中都可以直接访问TimerService,但是只有基于 KeyedStream 的处理函数,才能去调用注册和删除定时器的方法;
// 获取当前的处理时间
long currentProcessingTime();
// 获取当前的水位线(事件时间)
long currentWatermark();
// 注册处理时间定时器,当处理时间超过 time 时触发
void registerProcessingTimeTimer(long time);
// 注册事件时间定时器,当水位线超过 time 时触发
void registerEventTimeTimer(long time);
// 删除触发时间为 time 的处理时间定时器
void deleteProcessingTimeTimer(long time);
// 删除触发时间为 time 的处理时间定时器
void deleteEventTimeTimer(long time);
2、按键分区处理函数KeyedProcessFunction,继承AbstractRichFunction
在 Flink 程序中,为了实现数据的聚合统计,或者开窗计算之类的功能,我们一般都要先用 keyBy 算子对数据流进行“按键分区”,得到一个 KeyedStream。也就是指定一个键(key),按照它的哈希值(hash code)将数据分成不同的“组”,然后分配到不同的并行子任务上执行计算;这相当于做了一个逻辑分流的操作,从而可以充分利用并行计算的优势实时处理海量数据。
只有在 KeyedStream 中才支持使用 TimerService 设置定时器的操作。所以一般情况下,我们都是先做了 keyBy 分区之后,再去定义处理操作;代码中更加常见的处理函数是 KeyedProcessFunction,最基本的 ProcessFunction 反而出镜率没那么高。
KeyedProcessFunction 的一个特色,就是可以灵活地使用定时器。定时器(timers)是处理函数中进行时间相关操作的主要机制。在.onTimer()方法中可以实现定时处理的逻辑,而它能触发的前提,就是之前曾经注册过定时器、并且现在已经到了触发时间。注册定时器的功能,是通过上下文中提供的“定时服务”(TimerService)来实现的。
(1)使用方法
使用方法
DataStream<Tuple2<String, Long>> clickCountStream = stream
.keyBy("替换为需要分组字段")
.process(new MyProcessFunction());
自定义处理函数,需要替换输入输出类型
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
/**
* K:当前按键分区的 key 的类型
* I:输入类型
* O:输出类型
*/
public class MyProcessFunction extends KeyedProcessFunction<K, I, O> {
/**
*用来处理流中的每一个数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processElement(I value, Context ctx, Collector<O> out) throws Exception {
}
/**
* 非必须方法onTimer()用来定义定时器触发时的回调操作
* 由于定时器只能在 KeyedStream 上使用,所以到了 KeyedProcessFunction 这里,我们才真正对时间有了精细的控制,定时方法.onTimer()才真正派上了用场
* 使用场景:假设你有一个流数据流,其中包含每个用户的点击数据,并且你想要对每个用户的点击数进行计数。你可以使用 KeyedProcessFunction 来实现这个功能,如下所示:
*/
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector out) throws Exception {
super.onTimer(timestamp, ctx, out);
}
}
3、窗口处理函数ProcessWindowFunction,继承AbstractRichFunction
ProcessWindowFunction 既是处理函数又是全窗口函数。从名字上也可以推测出,它的本质似乎更倾向于“窗口函数”一些。
使用方式
DataStream<OUT> clickCountStream = clickEventStream
.keyBy(ClickEvent::getUserId)
//定义一小时窗口
.timeWindow(Time.hours(1))
.process(new MyProcessFunction());
自定义窗口函数
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.util.Collector;
/**
* IN:输入类型
* OUT:输出类型
* KEY:数据中键 key 的类型
* W:窗口的类型,一般为TimeWindow
*/
public class MyProcessFunction extends ProcessWindowFunction<IN, OUT, KEY, W extends Window> {
/**
* KEY、IN、OUT替换成对应的类型
* context:当前窗口进行计算的上下文
*/
@Override
public void process(KEY o, Context context, Iterable<IN> elements, Collector<OUT> out) throws Exception {
}
}
4、全窗口函数
它与 ProcessWindowFunction 类似,但是它会对窗口中的所有数据进行处理,而不是仅处理触发窗口计算的数据。
使用方式
DataStream<OUT> clickCountStream = clickEventStream
.keyBy(ClickEvent::getUserId)
//定义一小时窗口
.timeWindow(Time.hours(1))
.process(new MyProcessFunction());
自定义窗口函数
import org.apache.flink.streaming.api.functions.windowing.ProcessAllWindowFunction;
import org.apache.flink.streaming.api.windowing.windows.Window;
import org.apache.flink.util.Collector;
/**
* IN:输入类型
* OUT:输出类型
* KEY:数据中键 key 的类型
* W:窗口的类型
*/
public class MyProcessFunction extends ProcessAllWindowFunction<IN, OUT, W extends Window> {
/**
* IN、OUT替换成对应的类型
* context:当前窗口进行计算的上下文
*/
@Override
public void process(Context context, Iterable<IN> elements, Collector<OUT> out) throws Exception {
}
}