Flink实践场景-通过DataStream Api统计每小时的出租车司机的收入-CSDN博客
在 Flink 中,KeyedProcessFunction
是一种特殊类型的处理函数,它允许你对数据流中的每个键(key)分别进行处理。这种处理函数特别适用于需要对每个键维护独立状态或定时器的场景。
按照上述说法,本人之前的错误理解为应当不需要配置watermark和事件时间,直接再keyedProcessFunction配置定时器即可,但实际运行时出现无法触发计算的场景,后查询原理后才明白之前理解错误:
eyedProcessFunction
通常与事件时间(event time)和 watermarks 一起使用,因为它们允许你基于事件时间来处理数据,而不是仅仅基于数据到达的时间(即处理时间)。Watermarks 是 Flink 中用于处理乱序事件和迟到数据的机制,它们是事件时间处理的核心。
如果你不配置 watermark,KeyedProcessFunction
可能无法输出结果,原因如下:
事件时间处理依赖于 Watermarks:在事件时间语义下,Flink 依赖 watermarks 来确定何时可以处理窗口或触发某些基于时间的操作。如果没有 watermarks,Flink 就无法知道何时足够的数据已经到达,可以安全地执行这些操作。
迟到数据的处理:在实际应用中,数据可能会迟到。Watermarks 允许 Flink 等待一定程度的迟到数据,然后再触发计算。没有 watermarks,系统就无法有效地处理这些迟到的数据。
状态管理:KeyedProcessFunction
允许你为每个键维护状态。在事件时间语义下,这些状态可能会根据 watermarks 的进展来更新或清理。如果没有 watermarks,状态管理可能无法正确进行,导致数据积压或不正确的结果。
定时器:KeyedProcessFunction
允许你注册定时器,这些定时器是基于事件时间的。如果没有 watermarks,定时器就无法正确触发,因为你无法确定何时应该触发这些定时器。
因此,为了充分利用 KeyedProcessFunction
的能力,通常需要配置 watermarks。这样,你可以根据事件时间来处理数据,包括正确地处理迟到数据和维护基于时间的状态。
三、KeyedProcessFunction统计每小时的收入代码
package com.example.trublingwindow;
import com.example.trublingwindow.source.TaxiFare;
import com.example.trublingwindow.source.TaxiFareSource;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.TimerService;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SideOutputDataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import java.time.Duration;
public class TumblingEventTimeWindowTest2 {
private static final OutputTag lateFares = new OutputTag("lateFares") {};
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
DataStreamSource source = env.addSource(new TaxiFareSource());
SingleOutputStreamOperator> process = source
.assignTimestampsAndWatermarks(
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofMinutes(10)).
withTimestampAssigner(((taxiFare, recordTimestamp) -> taxiFare.getTimestamp()))
)
.keyBy(taxi -> taxi.getDriverId())
.process(new KeyedProcessFunction>() {
private MapState windowEndTipsMap;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
MapStateDescriptor windowEndTipsMapStateDescriptor = new MapStateDescriptor("windowEndTipsMap", Long.class, Float.class);
windowEndTipsMap = getRuntimeContext().getMapState(windowEndTipsMapStateDescriptor);
}
@Override
public void onTimer(long timestamp, KeyedProcessFunction>.OnTimerContext ctx, Collector> out) throws Exception {
out.collect(Tuple3.of(ctx.getCurrentKey(), timestamp, windowEndTipsMap.get(timestamp)));
windowEndTipsMap.remove(timestamp);
}
@Override
public void processElement(TaxiFare value, KeyedProcessFunction>.Context ctx, Collector> out) throws Exception {
long timestamp = value.getTimestamp();
TimerService timerService = ctx.timerService();
if (timestamp < timerService.currentWatermark()) {
//skip 事件延迟;其对应的窗口已经触发。
ctx.output(lateFares, value);
} else {
long windowEnd = (timestamp - timestamp % (1000 * 60 * 60 * 1L) + 1000 * 60 * 60 * 1L - 1);
timerService.registerEventTimeTimer(windowEnd);
Float sum = windowEndTipsMap.get(windowEnd);
if (null == sum) {
sum = 0F;
}
sum = sum + value.getTips();
windowEndTipsMap.put(windowEnd, sum);
}
}
});
//迟到的数据打印
SideOutputDataStream sideOutput = process.getSideOutput(lateFares);
sideOutput.print();
//每小时的tips总和
process.print();
env.execute();
}
}
有几个很好的理由希望从 Flink 算子获得多个输出流,如下报告条目:
旁路输出(Side outputs)是一种方便的方法。除了错误报告之外,旁路输出也是实现流的 n 路分割的好方法。
现在你可以对上一节中忽略的延迟事件执行某些操作。
Side output channel 与 OutputTag
相关联。这些标记拥有自己的名称,并与对应 DataStream 类型一致。
private static final OutputTag lateFares = new OutputTag("lateFares") {};
上面显示的是一个静态 OutputTag
,当在 PseudoWindow
的 processElement
方法中发出延迟事件时,可以引用它:
if (eventTime <= timerService.currentWatermark()) {
// 事件延迟,其对应的窗口已经触发。
ctx.output(lateFares, fare);
} else {
. . .
}
以及当在作业的 main
中从该旁路输出访问流时:
// 计算每个司机每小时的小费总和
SingleOutputStreamOperator hourlyTips = fares
.keyBy((TaxiFare fare) -> fare.driverId)
.process(new PseudoWindow(Time.hours(1)));
hourlyTips.getSideOutput(lateFares).print();
或者,可以使用两个同名的 OutputTag 来引用同一个旁路输出,但如果这样做,它们必须具有相同的类型。