FlinkAPI开发之处理函数

案例用到的测试数据请参考文章:
Flink自定义Source模拟数据流
原文链接:https://blog.csdn.net/m0_52606060/article/details/135436048

概述

之前所介绍的流处理API,无论是基本的转换、聚合,还是更为复杂的窗口操作,其实都是基于DataStream进行转换的,所以可以统称为DataStream API。
在Flink更底层,我们可以不定义任何具体的算子(比如map,filter,或者window),而只是提炼出一个统一的“处理”(process)操作——它是所有转换算子的一个概括性的表达,可以自定义处理逻辑,所以这一层接口就被叫作“处理函数”(process function)。

在这里插入图片描述

基本处理函数(ProcessFunction)

处理函数的功能和使用

我们之前学习的转换算子,一般只是针对某种具体操作来定义的,能够拿到的信息比较有限。如果我们想要访问事件的时间戳,或者当前的水位线信息,都是完全做不到的。跟时间相关的操作,目前我们只会用窗口来处理。而在很多应用需求中,要求我们对时间有更精细的控制,需要能够获取水位线,甚至要“把控时间”、定义什么时候做什么事,这就不是基本的时间窗口能够实现的了。
这时就需要使用底层的处理函数。处理函数提供了一个“定时服务”(TimerService),我们可以通过它访问流中的事件(event)、时间戳(timestamp)、水位线(watermark),甚至可以注册“定时事件”。而且处理函数继承了AbstractRichFunction抽象类,所以拥有富函数类的所有特性,同样可以访问状态(state)和其他运行时信息。此外,处理函数还可以直接将数据输出到侧输出流(side output)中。所以,处理函数是最为灵活的处理方法,可以实现各种自定义的业务逻辑。
处理函数的使用与基本的转换操作类似,只需要直接基于DataStream调用.process()方法就可以了。方法需要传入一个ProcessFunction作为参数,用来定义处理逻辑。

stream.process(new MyProcessFunction())

这里ProcessFunction不是接口,而是一个抽象类,继承了AbstractRichFunction;MyProcessFunction是它的一个具体实现。所以所有的处理函数,都是富函数(RichFunction),富函数可以调用的东西这里同样都可以调用。

ProcessFunction解析

在源码中我们可以看到,抽象类ProcessFunction继承了AbstractRichFunction,有两个泛型类型参数:I表示Input,也就是输入的数据类型;O表示Output,也就是处理完成之后输出的数据类型。
内部单独定义了两个方法:一个是必须要实现的抽象方法.processElement();另一个是非抽象方法.onTimer()。

public abstract class ProcessFunction<I, O> extends AbstractRichFunction {...public abstract void processElement(I value, Context ctx, Collector<O> out) throws Exception;public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out) throws Exception {}...}

抽象方法.processElement()

用于“处理元素”,定义了处理的核心逻辑。这个方法对于流中的每个元素都会调用一次,参数包括三个:输入数据值value,上下文ctx,以及“收集器”(Collector)out。方法没有返回值,处理之后的输出数据是通过收集器out来定义的。
value:当前流中的输入元素,也就是正在处理的数据,类型与流中数据类型一致。
ctx:类型是ProcessFunction中定义的内部抽象类Context,表示当前运行的上下文,可以获取到当前的时间戳,并提供了用于查询时间和注册定时器的“定时服务”(TimerService),以及可以将数据发送到“侧输出流”(side output)的方法.output()。
out:“收集器”(类型为Collector),用于返回输出数据。使用方式与flatMap算子中的收集器完全一样,直接调用out.collect()方法就可以向下游发出一个数据。这个方法可以多次调用,也可以不调用。
通过几个参数的分析不难发现,ProcessFunction可以轻松实现flatMap、map、filter这样的基本转换功能;而通过富函数提供的获取上下文方法.getRuntimeContext(),也可以自定义状态(state)进行处理,这也就能实现聚合操作的功能了。

非抽象方法.onTimer()

这个方法只有在注册好的定时器触发的时候才会调用,而定时器是通过“定时服务”TimerService来注册的。打个比方,注册定时器(timer)就是设了一个闹钟,到了设定时间就会响;而.onTimer()中定义的,就是闹钟响的时候要做的事。所以它本质上是一个基于时间的“回调”(callback)方法,通过时间的进展来触发;在事件时间语义下就是由水位线(watermark)来触发了。
定时方法.onTimer()也有三个参数:时间戳(timestamp),上下文(ctx),以及收集器(out)。这里的timestamp是指设定好的触发时间,事件时间语义下当然就是水位线了。另外这里同样有上下文和收集器,所以也可以调用定时服务(TimerService),以及任意输出处理之后的数据。
既然有.onTimer()方法做定时触发,我们用ProcessFunction也可以自定义数据按照时间分组、定时触发计算输出结果;这其实就实现了窗口(window)的功能。所以说ProcessFunction其实可以实现一切功能。
注意:在Flink中,只有“按键分区流”KeyedStream才支持设置定时器的操作。

处理函数的分类

我们知道,DataStream在调用一些转换方法之后,有可能生成新的流类型;例如调用.keyBy()之后得到KeyedStream,进而再调用.window()之后得到WindowedStream。对于不同类型的流,其实都可以直接调用.process()方法进行自定义处理,这时传入的参数就都叫作处理函数。当然,它们尽管本质相同,都是可以访问状态和时间信息的底层API,可彼此之间也会有所差异。

Flink提供了8个不同的处理函数:

(1)ProcessFunction
最基本的处理函数,基于DataStream直接调用.process()时作为参数传入。
(2)KeyedProcessFunction
对流按键分区后的处理函数,基于KeyedStream调用.process()时作为参数传入。要想使用定时器,比如基于KeyedStream。
(3)ProcessWindowFunction
开窗之后的处理函数,也是全窗口函数的代表。基于WindowedStream调用.process()时作为参数传入。
(4)ProcessAllWindowFunction
同样是开窗之后的处理函数,基于AllWindowedStream调用.process()时作为参数传入。
(5)CoProcessFunction
合并(connect)两条流之后的处理函数,基于ConnectedStreams调用.process()时作为参数传入。
(6)ProcessJoinFunction
间隔连接(interval join)两条流之后的处理函数,基于IntervalJoined调用.process()时作为参数传入。
(7)BroadcastProcessFunction
广播连接流处理函数,基于BroadcastConnectedStream调用.process()时作为参数传入。这里的“广播连接流”BroadcastConnectedStream,是一个未keyBy的普通DataStream与一个广播流(BroadcastStream)做连接(conncet)之后的产物。
(8)KeyedBroadcastProcessFunction
按键分区的广播连接流处理函数,同样是基于BroadcastConnectedStream调用.process()时作为参数传入。与BroadcastProcessFunction不同的是,这时的广播连接流,是一个KeyedStream与广播流(BroadcastStream)做连接之后的产物。

按键分区处理函数(KeyedProcessFunction)

在上节中提到,只有在KeyedStream中才支持使用TimerService设置定时器的操作。所以一般情况下,我们都是先做了keyBy分区之后,再去定义处理操作;代码中更加常见的处理函数是KeyedProcessFunction。

定时器(Timer)和定时服务(TimerService)

在.onTimer()方法中可以实现定时处理的逻辑,而它能触发的前提,就是之前曾经注册过定时器、并且现在已经到了触发时间。注册定时器的功能,是通过上下文中提供的“定时服务”来实现的。
定时服务与当前运行的环境有关。前面已经介绍过,ProcessFunction的上下文(Context)中提供了.timerService()方法,可以直接返回一个TimerService对象。TimerService是Flink关于时间和定时器的基础服务接口,包含以下六个方法:

// 获取当前的处理时间
long currentProcessingTime();// 获取当前的水位线(事件时间)
long currentWatermark();// 注册处理时间定时器,当处理时间超过time时触发
void registerProcessingTimeTimer(long time);// 注册事件时间定时器,当水位线超过time时触发
void registerEventTimeTimer(long time);// 删除触发时间为time的处理时间定时器
void deleteProcessingTimeTimer(long time);// 删除触发时间为time的处理时间定时器
void deleteEventTimeTimer(long time);

六个方法可以分成两大类:基于处理时间和基于事件时间。而对应的操作主要有三个:获取当前时间,注册定时器,以及删除定时器。需要注意,尽管处理函数中都可以直接访问TimerService,不过只有基于KeyedStream的处理函数,才能去调用注册和删除定时器的方法;未作按键分区的DataStream不支持定时器操作,只能获取当前时间。
TimerService会以键(key)和时间戳为标准,对定时器进行去重;也就是说对于每个key和时间戳,最多只有一个定时器,如果注册了多次,onTimer()方法也将只被调用一次。

KeyedProcessFunction案例

基于keyBy之后的KeyedStream,直接调用.process()方法,这时需要传入的参数就是KeyedProcessFunction的实现类。

stream.keyBy( t -> t.f0 ).process(new MyKeyedProcessFunction())

类似地,KeyedProcessFunction也是继承自AbstractRichFunction的一个抽象类,与ProcessFunction的定义几乎完全一样,区别只是在于类型参数多了一个K,这是当前按键分区的key的类型。同样地,我们必须实现一个.processElement()抽象方法,用来处理流中的每一个数据;另外还有一个非抽象方法.onTimer(),用来定义定时器触发时的回调操作。

import com.zxl.bean.Orders;
import org.apache.flink.streaming.api.TimerService;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;// TODO: 2024/1/15 参数1.key的数据类型,输入格式,输出格式
public class MyKeyedProcessFunction extends KeyedProcessFunction<Integer, Orders, String> {private Integer sum=0;@Overridepublic void processElement(Orders orders, KeyedProcessFunction<Integer, Orders, String>.Context ctx, Collector<String> collector) throws Exception {sum+=orders.getOrder_amount();//获取当前数据的keyInteger currentKey = ctx.getCurrentKey();// TODO 1.定时器注册TimerService timerService = ctx.timerService();// TODO: 2024/1/15  事件时间的案例// TODO: 2024/1/15  数据中提取出来的事件时间Long currentEventTime = ctx.timestamp();// TODO: 2024/1/15 注册事件时间定时器,当水位线超过time时触发 timerService.registerEventTimeTimer(currentEventTime + 5000L);// TODO: 2024/1/15 删除以事件事件触发时间的定时器timerService.deleteEventTimeTimer(currentEventTime);//System.out.println("当前key=" + currentKey + ",当前时间=" + currentEventTime + ",注册了一个5s的定时器");/*// TODO: 2024/1/15 2.处理时间的案例// TODO: 2024/1/15 获取当前处理时间 long currentTs = timerService.currentProcessingTime();// TODO: 2024/1/15 注册处理时间定时器,当处理时间超过time时触发 timerService.registerProcessingTimeTimer(currentTs + 5000);// TODO: 2024/1/15 删除以处理时间为触发时间的定时器//deleteProcessingTimeTimer(currentWatermark);System.out.println("当前key=" + currentKey + ",当前时间=" + currentTs + ",注册了一个5s后的定时器");*//*// TODO: 2024/1/15   3、获取 process的 当前watermarklong currentWatermark = timerService.currentWatermark();System.out.println("当前数据=" + orders + ",当前watermark=" + currentWatermark);*/}// TODO: 2024/1/15 触发执行器,编写写触发后的逻辑运行@Overridepublic void onTimer(long timestamp, KeyedProcessFunction<Integer, Orders, String>.OnTimerContext ctx, Collector<String> out) throws Exception {super.onTimer(timestamp, ctx, out);// TODO: 2024/1/15 触发是执行Integer currentKey = ctx.getCurrentKey();out.collect(sum.toString());System.out.println("key=" + currentKey + "现在时间是" + timestamp + "定时器触发");}
}
import com.zxl.Functions.MyKeyedProcessFunction;
import com.zxl.bean.Orders;
import com.zxl.datas.OrdersData;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import java.time.Duration;public class KeyedProcessTimerDemo {public static void main(String[] args) throws Exception {// TODO: 2024/1/15  创建Flink流处理执行环境StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();// TODO: 2024/1/15  设置并行度为1environment.setParallelism(1);// TODO: 2024/1/7 定义时间语义environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);// TODO: 2024/1/6 订单数据DataStreamSource<Orders> ordersDataStreamSource = environment.addSource(new OrdersData());// TODO: 2024/1/15 设置水位线WatermarkStrategy<Orders> watermarkStrategy = WatermarkStrategy// TODO: 2024/1/15 设置最大乱序程度,数据流中乱序数据时间戳的最大差值.<Orders>forBoundedOutOfOrderness(Duration.ofSeconds(3))// TODO: 2024/1/15 指定水位线中对应的时间字段.withTimestampAssigner((orders, l) -> orders.getOrder_date())// TODO: 2024/1/15 设置空闲等待时间,如果某个分区中有长时间无数据产生将放弃此分区的水位线选举权力.withIdleness(Duration.ofSeconds(3));// TODO: 2024/1/15 添加水位线SingleOutputStreamOperator<Orders> operator = ordersDataStreamSource.assignTimestampsAndWatermarks(watermarkStrategy);// TODO: 2024/1/15 设置分区keyKeyedStream<Orders, Integer> keyedStream = operator.keyBy(Orders::getProduct_id);// TODO: 2024/1/15  注意只有keyedstream流能重写KeyedProcessFunction方法SingleOutputStreamOperator<String> process = keyedStream.process(new MyKeyedProcessFunction());// TODO: 2024/1/15 打印数据ordersDataStreamSource.print();process.print();// TODO: 2024/1/15 执行程序environment.execute();}
}

在这里插入图片描述

窗口处理函数

除了KeyedProcessFunction,另外一大类常用的处理函数,就是基于窗口的ProcessWindowFunction和ProcessAllWindowFunction了。在第六章窗口函数的介绍中,我们之前已经简单地使用过窗口处理函数了。

窗口处理函数的使用

进行窗口计算,我们可以直接调用现成的简单聚合方法(sum/max/min),也可以通过调用.reduce()或.aggregate()来自定义一般的增量聚合函数(ReduceFunction/AggregateFucntion);而对于更加复杂、需要窗口信息和额外状态的一些场景,我们还可以直接使用全窗口函数、把数据全部收集保存在窗口内,等到触发窗口计算时再统一处理。窗口处理函数就是一种典型的全窗口函数。
窗口处理函数ProcessWindowFunction的使用与其他窗口函数类似,也是基于WindowedStream直接调用方法就可以,只不过这时调用的是.process()。

stream.keyBy( t -> t.f0 ).window( TumblingEventTimeWindows.of(Time.seconds(10)) ).process(new MyProcessWindowFunction())

ProcessWindowFunction解析

ProcessWindowFunction既是处理函数又是全窗口函数。从名字上也可以推测出,它的本质似乎更倾向于“窗口函数”一些。事实上它的用法也确实跟其他处理函数有很大不同。我们可以从源码中的定义看到这一点:

public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window> extends AbstractRichFunction {...public abstract void process(KEY key, Context context, Iterable<IN> elements, Collector<OUT> out) throws Exception;public void clear(Context context) throws Exception {}public abstract class Context implements java.io.Serializable {...}
}

ProcessWindowFunction依然是一个继承了AbstractRichFunction的抽象类,它有四个类型参数:

IN:input,数据流中窗口任务的输入数据类型。
OUT:output,窗口任务进行计算之后的输出数据类型。
KEY:数据中键key的类型。
W:窗口的类型,是Window的子类型。一般情况下我们定义时间窗口,W就是TimeWindow。
ProcessWindowFunction里面处理数据的核心方法.process()。方法包含四个参数。
key:窗口做统计计算基于的键,也就是之前keyBy用来分区的字段。
context:当前窗口进行计算的上下文,它的类型就是ProcessWindowFunction内部定义的抽象类Context。
elements:窗口收集到用来计算的所有数据,这是一个可迭代的集合类型。
out:用来发送数据输出计算结果的收集器,类型为Collector。

可以明显看出,这里的参数不再是一个输入数据,而是窗口中所有数据的集合。而上下文context所包含的内容也跟其他处理函数有所差别:

public abstract class Context implements java.io.Serializable {public abstract W window();public abstract long currentProcessingTime();public abstract long currentWatermark();public abstract KeyedStateStore windowState();public abstract KeyedStateStore globalState();public abstract <X> void output(OutputTag<X> outputTag, X value);
}

除了可以通过.output()方法定义侧输出流不变外,其他部分都有所变化。这里不再持有TimerService对象,只能通过currentProcessingTime()和currentWatermark()来获取当前时间,所以失去了设置定时器的功能;另外由于当前不是只处理一个数据,所以也不再提供.timestamp()方法。与此同时,也增加了一些获取其他信息的方法:比如可以通过.window()直接获取到当前的窗口对象,也可以通过.windowState()和.globalState()获取到当前自定义的窗口状态和全局状态。注意这里的“窗口状态”是自定义的,不包括窗口本身已经有的状态,针对当前key、当前窗口有效;而“全局状态”同样是自定义的状态,针对当前key的所有窗口有效。
所以我们会发现,ProcessWindowFunction中除了.process()方法外,并没有.onTimer()方法,而是多出了一个.clear()方法。从名字就可以看出,这主要是方便我们进行窗口的清理工作。如果我们自定义了窗口状态,那么必须在.clear()方法中进行显式地清除,避免内存溢出。
至于另一种窗口处理函数ProcessAllWindowFunction,它的用法非常类似。区别在于它基于的是AllWindowedStream,相当于对没有keyBy的数据流直接开窗并调用.process()方法:

stream.windowAll( TumblingEventTimeWindows.of(Time.seconds(10)) ).process(new MyProcessAllWindowFunction())

应用案例——Top N

案例需求:实时统计一段时间内的出现次数最多的水位。例如,统计最近10秒钟内出现次数最多的两个水位,并且每5秒钟更新一次。我们知道,这可以用一个滑动窗口来实现。于是就需要开滑动窗口收集传感器的数据,按照不同的水位进行统计,而后汇总排序并最终输出前两名。这其实就是著名的“Top N”问题。

案例需求:实时统计一段时间内的出现次数最多的水位。例如,统计最近10秒钟内出现次数最多的两个水位,并且每5秒钟更新一次。我们知道,这可以用一个滑动窗口来实现。于是就需要开滑动窗口收集传感器的数据,按照不同的水位进行统计,而后汇总排序并最终输出前两名。这其实就是著名的“Top N”问题。

使用ProcessAllWindowFunction

思路一:一种最简单的想法是,我们干脆不区分不同水位,而是将所有访问数据都收集起来,统一进行统计计算。所以可以不做keyBy,直接基于DataStream开窗,然后使用全窗口函数ProcessAllWindowFunction来进行处理。
在窗口中可以用一个HashMap来保存每个水位的出现次数,只要遍历窗口中的所有数据,自然就能得到所有水位的出现次数。最后把HashMap转成一个列表ArrayList,然后进行排序、取出前两名输出就可以了。
代码具体实现如下:

import com.zxl.bean.Orders;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.functions.windowing.ProcessAllWindowFunction;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;import java.util.*;public class MyTopNPAWF extends ProcessAllWindowFunction<Orders, String, TimeWindow> {@Overridepublic void process(ProcessAllWindowFunction<Orders, String, TimeWindow>.Context context, Iterable<Orders> elements, Collector<String> out) throws Exception {// 定义一个hashmap用来存,key=商品ID,value=count值Map<Integer, Integer> vcCountMap = new HashMap<>();// 1.遍历数据, 统计交易额大于50出现的次数for (Orders element : elements) {Integer vc = element.getOrder_amount();Integer productId = element.getProduct_id();if (vc > 50) {// 1.1 key存在,不是这个key的第一条数据,直接累加if (vcCountMap.containsKey(productId)) {vcCountMap.put(productId, vcCountMap.get(productId) + 1);} else {vcCountMap.put(productId, 1);}} else {// 1.2 key不存在,初始化vcCountMap.put(productId, 1);}}// 2.对 count值进行排序: 利用List来实现排序List<Tuple2<Integer, Integer>> datas = new ArrayList<>();for (Integer vc : vcCountMap.keySet()) {datas.add(Tuple2.of(vc, vcCountMap.get(vc)));}// 对List进行排序,根据count值 降序datas.sort(new Comparator<Tuple2<Integer, Integer>>() {@Overridepublic int compare(Tuple2<Integer, Integer> o1, Tuple2<Integer, Integer> o2) {// 降序, 后 减 前return o2.f1 - o1.f1;}});// 3.取出 count最大的2个 vcStringBuilder outStr = new StringBuilder();outStr.append("================================\n");// 遍历 排序后的 List,取出前2个, 考虑可能List不够2个的情况  ==》 List中元素的个数 和 2 取最小值for (int i = 0; i < Math.min(2, datas.size()); i++) {Tuple2<Integer, Integer> vcCount = datas.get(i);outStr.append("Top" + (i + 1) + "\n");outStr.append("vc=" + vcCount.f0 + "\n");outStr.append("count=" + vcCount.f1 + "\n");outStr.append("窗口开始时间=" + DateFormatUtils.format(context.window().getStart(), "yyyy-MM-dd HH:mm:ss.SSS") + "\n" + "窗口结束时间=" + DateFormatUtils.format(context.window().getEnd(), "yyyy-MM-dd HH:mm:ss.SSS") + "\n");outStr.append("================================\n");}out.collect(outStr.toString());}
}

import com.zxl.Functions.MyTopNPAWF;
import com.zxl.bean.Orders;
import com.zxl.datas.OrdersData;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;import java.time.Duration;public class ProcessAllWindowTopNDemo {public static void main(String[] args) throws Exception {// TODO: 2024/1/15  创建Flink流处理执行环境StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();// TODO: 2024/1/15  设置并行度为1environment.setParallelism(1);// TODO: 2024/1/7 定义时间语义environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);// TODO: 2024/1/6 订单数据DataStreamSource<Orders> ordersDataStreamSource = environment.addSource(new OrdersData());// TODO: 2024/1/15 设置水位线WatermarkStrategy<Orders> watermarkStrategy = WatermarkStrategy// TODO: 2024/1/15 设置最大乱序程度,数据流中乱序数据时间戳的最大差值.<Orders>forBoundedOutOfOrderness(Duration.ofSeconds(3))// TODO: 2024/1/15 指定水位线中对应的时间字段.withTimestampAssigner((orders, l) -> orders.getOrder_date())// TODO: 2024/1/15 设置空闲等待时间,如果某个分区中有长时间无数据产生将放弃此分区的水位线选举权力.withIdleness(Duration.ofSeconds(3));// TODO: 2024/1/15 添加水位线SingleOutputStreamOperator<Orders> operator = ordersDataStreamSource.assignTimestampsAndWatermarks(watermarkStrategy);// TODO: 2024/1/15 设置窗口SingleOutputStreamOperator<String> process = operator.windowAll(SlidingEventTimeWindows.of(Time.seconds(10),Time.seconds(5))).process(new MyTopNPAWF());// TODO: 2024/1/15 打印数据ordersDataStreamSource.print();process.print();// TODO: 2024/1/15 执行程序environment.execute();}
}

在这里插入图片描述

使用KeyedProcessFunction

思路二:在上一小节的实现过程中,我们没有进行按键分区,直接将所有数据放在一个分区上进行了开窗操作。这相当于将并行度强行设置为1,在实际应用中是要尽量避免的,所以Flink官方也并不推荐使用AllWindowedStream进行处理。另外,我们在全窗口函数中定义了HashMap来统计vc的出现次数,计算过程是要先收集齐所有数据、然后再逐一遍历更新HashMap,这显然不够高效。
基于这样的想法,我们可以从两个方面去做优化:一是对数据进行按键分区,分别统计vc的出现次数;二是进行增量聚合,得到结果最后再做排序输出。所以,我们可以使用增量聚合函数AggregateFunction进行浏览量的统计,然后结合ProcessWindowFunction排序输出来实现Top N的需求。
具体实现可以分成两步:先对每个vc统计出现次数,然后再将统计结果收集起来,排序输出最终结果。由于最后的排序还是基于每个时间窗口的,输出的统计结果中要包含窗口信息,我们可以输出包含了vc、出现次数(count)以及窗口结束时间的Tuple3。之后先按窗口结束时间分区,然后用KeyedProcessFunction来实现。
用KeyedProcessFunction来收集数据做排序,这时面对的是窗口聚合之后的数据流,而窗口已经不存在了;我们需要确保能够收集齐所有数据,所以应该在窗口结束时间基础上再“多等一会儿”。具体实现上,可以采用一个延迟触发的事件时间定时器。基于窗口的结束时间来设定延迟,其实并不需要等太久——因为我们是靠水位线的推进来触发定时器,而水位线的含义就是“之前的数据都到齐了”。所以我们只需要设置1毫秒的延迟,就一定可以保证这一点。
而在等待过程中,之前已经到达的数据应该缓存起来,我们这里用一个自定义的HashMap来进行存储,key为窗口的标记,value为List。之后每来一条数据,就把它添加到当前的HashMap中,并注册一个触发时间为窗口结束时间加1毫秒(windowEnd + 1)的定时器。待到水位线到达这个时间,定时器触发,我们可以保证当前窗口所有vc的统计结果Tuple3都到齐了;于是从HashMap中取出进行排序输出。
具体代码实现如下:

package com.zxl.Functions;import com.zxl.bean.Orders;
import org.apache.flink.api.common.functions.AggregateFunction;// TODO: 2024/1/16 三个参数:输入类型(IN)、累加器类型(ACC)和输出类型(OUT)。
public class VcCountAgg implements AggregateFunction<Orders, Integer, Integer> {// TODO: 2024/1/16 创建一个新的累加器,开始一个新的聚合。累加器是正在运行的聚合的状态。@Overridepublic Integer createAccumulator() {return 0;}// TODO: 2024/1/16 将给定的输入添加到给定的累加器,并返回新的累加器值。@Overridepublic Integer add(Orders orders, Integer o) {return orders.getOrder_amount() + o;}// TODO: 2024/1/16 从累加器获取聚合结果。@Overridepublic Integer getResult(Integer o) {return o;}// TODO: 2024/1/16 合并两个累加器,返回合并后的累加器的状态。@Overridepublic Integer merge(Integer o, Integer acc1) {return o + acc1;}
}
package com.zxl.Functions;import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;// TODO: 2024/1/16  四个参数:输入类型(IN)、输出类型(OUT)、key的类型、窗口
public class WindowResult extends ProcessWindowFunction<Integer, Tuple3<Integer, Integer, Long>, Integer, TimeWindow> {@Overridepublic void process(Integer key, ProcessWindowFunction<Integer, Tuple3<Integer, Integer, Long>, Integer, TimeWindow>.Context context, Iterable<Integer> elements, Collector<Tuple3<Integer, Integer, Long>> collector) throws Exception {// 迭代器里面只有一条数据,next一次即可Integer count = elements.iterator().next();long windowEnd = context.window().getEnd();collector.collect(Tuple3.of(key, count, windowEnd));}
}
package com.zxl.Functions;import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;import java.util.*;public class TopNDemo extends KeyedProcessFunction<Long, Tuple3<Integer, Integer, Long>, String> {// 存不同窗口的 统计结果,key=windowEnd,value=list数据Map<Long, List<Tuple3<Integer, Integer, Long>>> dataListMap = new HashMap<>();@Overridepublic void processElement(Tuple3<Integer, Integer, Long> value, Context ctx, Collector<String> out) throws Exception {// 进入这个方法,只是一条数据,要排序,得到齐才行 ===》 存起来,不同窗口分开存System.out.println(value);// 1. 存到HashMap中Long windowEnd = value.f2;// System.out.println(dataListMap.containsKey(windowEnd));if (dataListMap.containsKey(windowEnd)) {// 1.1 包含vc,不是该vc的第一条,直接添加到List中List<Tuple3<Integer, Integer, Long>> dataList = dataListMap.get(windowEnd);dataList.add(value);dataListMap.put(windowEnd,dataList);} else {// 1.1 不包含vc,是该vc的第一条,需要初始化listList<Tuple3<Integer, Integer, Long>> dataList = new ArrayList<>();dataList.add(value);// System.out.println(dataList);dataListMap.put(windowEnd, dataList);}// 2. 注册一个定时器, windowEnd+1ms即可(// 同一个窗口范围,应该同时输出,只不过是一条一条调用processElement方法,只需要延迟10ms即可ctx.timerService().registerEventTimeTimer(windowEnd + 1);System.out.println(dataListMap);}@Overridepublic void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {super.onTimer(timestamp, ctx, out);// 定时器触发,同一个窗口范围的计算结果攒齐了,开始 排序、取TopNLong windowEnd = ctx.getCurrentKey();// 1. 排序List<Tuple3<Integer, Integer, Long>> dataList = dataListMap.get(windowEnd);dataList.sort(new Comparator<Tuple3<Integer, Integer, Long>>() {@Overridepublic int compare(Tuple3<Integer, Integer, Long> o1, Tuple3<Integer, Integer, Long> o2) {// 降序, 后 减 前return o2.f1 - o1.f1;}});// 2. 取TopNStringBuilder outStr = new StringBuilder();outStr.append("================================\n");for (int i = 0; i < dataList.size(); i++) {Tuple3<Integer, Integer, Long> vcCount = dataList.get(i);System.out.println("vcCount" + vcCount);outStr.append("Top" + (i + 1) + "\n");outStr.append("vc=" + vcCount.f0 + "\n");outStr.append("count=" + vcCount.f1 + "\n");outStr.append("窗口结束时间=" + vcCount.f2 + "\n");outStr.append("================================\n");}out.collect(outStr.toString());}
}
package com.zxl.ProcessFunction;import com.zxl.Functions.TopNDemo;
import com.zxl.Functions.VcCountAgg;
import com.zxl.Functions.WindowResult;
import com.zxl.bean.Orders;
import com.zxl.datas.OrdersData;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import java.time.Duration;public class KeyedProcessFunctionTopNDemo {public static void main(String[] args) throws Exception {// TODO: 2024/1/15  创建Flink流处理执行环境StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();// TODO: 2024/1/15  设置并行度为1environment.setParallelism(1);// TODO: 2024/1/7 定义时间语义environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);// TODO: 2024/1/6 订单数据DataStreamSource<Orders> ordersDataStreamSource = environment.addSource(new OrdersData());// TODO: 2024/1/15 设置水位线WatermarkStrategy<Orders> watermarkStrategy = WatermarkStrategy// TODO: 2024/1/15 设置最大乱序程度,数据流中乱序数据时间戳的最大差值.<Orders>forBoundedOutOfOrderness(Duration.ofSeconds(3))// TODO: 2024/1/15 指定水位线中对应的时间字段.withTimestampAssigner((orders, l) -> orders.getOrder_date())// TODO: 2024/1/15 设置空闲等待时间,如果某个分区中有长时间无数据产生将放弃此分区的水位线选举权力.withIdleness(Duration.ofSeconds(3));// TODO: 2024/1/15 添加水位线SingleOutputStreamOperator<Orders> operator = ordersDataStreamSource.assignTimestampsAndWatermarks(watermarkStrategy);WindowedStream<Orders, Integer, TimeWindow> windowedStream = operator.keyBy(orders -> orders.getProduct_id()).window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(2)));SingleOutputStreamOperator<Tuple3<Integer, Integer, Long>> streamOperator = windowedStream.aggregate(new VcCountAgg(),new WindowResult());SingleOutputStreamOperator<String> process = streamOperator.keyBy(m -> m.f2).process(new TopNDemo());process.print();environment.execute();}
}

侧输出流(Side Output)

处理函数还有另外一个特有功能,就是将自定义的数据放入“侧输出流”(side output)输出。这个概念我们并不陌生,之前在讲到窗口处理迟到数据时,最后一招就是输出到侧输出流。而这种处理方式的本质,其实就是处理函数的侧输出流功能。
我们之前讲到的绝大多数转换算子,输出的都是单一流,流里的数据类型只能有一种。而侧输出流可以认为是“主流”上分叉出的“支流”,所以可以由一条流产生出多条流,而且这些流中的数据类型还可以不一样。利用这个功能可以很容易地实现“分流”操作。
具体应用时,只要在处理函数的.processElement()或者.onTimer()方法中,调用上下文的.output()方法就可以了。

DataStream<Integer> stream = env.fromSource(...);OutputTag<String> outputTag = new OutputTag<String>("side-output") {};SingleOutputStreamOperator<Long> longStream = stream.process(new ProcessFunction<Integer, Long>() {@Overridepublic void processElement( Integer value, Context ctx, Collector<Integer> out) throws Exception {// 转换成Long,输出到主流中out.collect(Long.valueOf(value));// 转换成String,输出到侧输出流中ctx.output(outputTag, "side-output: " + String.valueOf(value));}
});

这里output()方法需要传入两个参数,第一个是一个“输出标签”OutputTag,用来标识侧输出流,一般会在外部统一声明;第二个就是要输出的数据。
我们可以在外部先将OutputTag声明出来:

OutputTag<String> outputTag = new OutputTag<String>("side-output") {};

如果想要获取这个侧输出流,可以基于处理之后的DataStream直接调用.getSideOutput()方法,传入对应的OutputTag,这个方式与窗口API中获取侧输出流是完全一样的。

DataStream<String> stringStream = longStream.getSideOutput(outputTag);

案例需求:对每个订单,订单金额超过60提醒
代码如下:

package com.zxl.ProcessFunction;import com.zxl.bean.Orders;
import com.zxl.datas.OrdersData;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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 SideOutputDemo {public static void main(String[] args) throws Exception {// TODO: 2024/1/15  创建Flink流处理执行环境StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();// TODO: 2024/1/15  设置并行度为1environment.setParallelism(1);// TODO: 2024/1/7 定义时间语义environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);// TODO: 2024/1/6 订单数据DataStreamSource<Orders> ordersDataStreamSource = environment.addSource(new OrdersData());// TODO: 2024/1/15 设置水位线WatermarkStrategy<Orders> watermarkStrategy = WatermarkStrategy// TODO: 2024/1/15 设置最大乱序程度,数据流中乱序数据时间戳的最大差值.<Orders>forBoundedOutOfOrderness(Duration.ofSeconds(3))// TODO: 2024/1/15 指定水位线中对应的时间字段.withTimestampAssigner((orders, l) -> orders.getOrder_date())// TODO: 2024/1/15 设置空闲等待时间,如果某个分区中有长时间无数据产生将放弃此分区的水位线选举权力.withIdleness(Duration.ofSeconds(3));// TODO: 2024/1/15 添加水位线SingleOutputStreamOperator<Orders> operator = ordersDataStreamSource.assignTimestampsAndWatermarks(watermarkStrategy);// TODO: 2024/1/16 侧输出流标签OutputTag<String> warnTag = new OutputTag<>("warn", Types.STRING);SingleOutputStreamOperator<String> process = operator.keyBy(orders -> orders.getProduct_id()).process(new KeyedProcessFunction<Integer, Orders, String>() {@Overridepublic void processElement(Orders orders, KeyedProcessFunction<Integer, Orders, String>.Context context, Collector<String> collector) throws Exception {if (orders.getOrder_amount() > 60) {context.output(warnTag, "当前销售额=" + orders.getOrder_amount() + ",大于阈值60!!!");} else {collector.collect(orders.toString());}}});process.getSideOutput(warnTag).print("侧输出流");process.print("主流");environment.execute();}
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/628811.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

cron表达式大全

简介 Cron表达式是一种用于指定定时任务的时间表达式&#xff0c;常用来指定任务的执行时间、执行频率和执行间隔。它由6&#xff5e;7个字段组成&#xff0c;分别表示秒、分、时、日期、月份、星期、年份&#xff08;可省略&#xff09;。 Cron表达式基本语法如下&#xff1a;…

人工智能 | ChatGPT 和文心一言哪个更好用?

github&#xff1a;https://github.com/MichaelBeechan CSDN&#xff1a;https://blog.csdn.net/u011344545 ChatGPT 和文心一言哪个更好用&#xff1f; ChatGPT 和文心一言哪个更好用&#xff1f;方向一&#xff1a;ChatGPT主要优势局限性和挑战如何克服chatGPT的局限性和挑战…

sql- sum if() 用法举例

题目&#xff1a; 从订单明细表(order_detail)中查询出所有购买过商品1和商品2&#xff0c;但是没有购买过商品3的用户 订单表 order_info order_id (订单id)user_id (用户id)create_date (下单日期)total_amount (订单金额)11012021-09-3029000.00101032020-10-0228000.00 订…

全罐喂养一个月多少钱?适合全罐喂养的猫罐头推荐

不少铲屎官一直没有办法get到猫罐头对猫咪的好处&#xff0c;或者get到了又觉得给猫咪买猫罐头好贵&#xff0c;看到其他铲屎官都开始全罐喂养了&#xff0c;但是自己却迟迟下不了手&#xff0c;犹犹豫豫的不知道全罐喂养一个月多少钱&#xff1f; 今天&#xff0c;铲龄15年的…

FreeBSD安装postgresql

安装postgresql15 $ sudo pkg install postgresql15-server postgresql15-client # 安装完之后会自动注册到服务列表 rootfreebsd14-11-199:~ # service -l|grep post postgresql # 将postgresl添加到开机自启动 rootfreebsd14-11-199:~ # service -e|grep post rootfreebsd1…

Python实现Excel切片删除功能(附源码)

使用Python实现Excel中的“切片删除”功能&#xff08;附源码&#xff09; 先上效果图&#xff01;&#xff01;&#xff01; 如下是需要处理的Excel文件&#xff0c;可以看到在27行和117行处的E列存在数据不一致情况&#xff0c;需进行“切片删除” 运行结果图如下&#xff…

【MySQL】union (all) 后 order by 子查询排序不生效问题解决方案

力扣题 1、题目地址 2308. 按性别排列表格 2、模拟表 表&#xff1a;Genders Column NameTypeuser_idintgendervarchar user_id 是该表的主键(具有唯一值的列)。gender 的值是 ‘female’&#xff0c;‘male’&#xff0c;‘other’ 之一。该表中的每一行都包含用户的 ID…

查看服务器资源使用情况

查看服务器资源使用情况 一、top命令二、理解IOPS三、腾讯云机器cvm四、iotop五、atop六、查看内存使用情况一、top命令 "top"命令是一个Linux系统的实用工具,用于动态监视系统的运行状态。它会实时显示系统中正在运行的进程列表,并按照CPU使用率、内存使用率等指…

【前端架构】前端通用架构

一个强大的前端通用架构应该具备多种能力&#xff0c;以支持现代化的应用程序开发和提高开发效率。以下是一些前端通用架构应该具备的关键能力&#xff1a; 模块化和组件化&#xff1a;支持模块化开发和组件化架构&#xff0c;能够将应用拆分为独立的模块和组件&#xff0c;以便…

leetcode 2418. 按身高排序

题目 给你一个字符串数组 names &#xff0c;和一个由 互不相同 的正整数组成的数组 heights 。两个数组的长度均为 n 。 对于每个下标 i&#xff0c;names[i] 和 heights[i] 表示第 i 个人的名字和身高。 请按身高 降序 顺序返回对应的名字数组 names 。 解题方法&#xff…

Go新项目-为何选Gin框架?(0)

先说结论&#xff1a;我们选型Gin框架 早在大概在2019年下旬&#xff0c;由于内部一个多线程上传的需求&#xff0c;考虑到Go协程的优势&#xff1b; 内部采用Gin框架编写了内部的数据上传平台BAP&#xff0c;采用GinVue开发&#xff0c;但前期没考虑到工程化思维&#xff0c;导…

开源世界许可证Copyleft GPL LGPL MIT BSD Apache

Copyleft Copyleft利用版权法来提供分发作品的副本和修改版本的权利,并要求在作品的修改版本中保留同样的权利。换句话说,copyleft是让创造性的作品可以自由修改,并要求作品的所有修改和扩展版本也是自由的一般方法。 GPL是首个通用的copyleft许可证。 GNU通用公共许可证(…

VUE的插槽介绍

什么是插槽 插槽&#xff08;Slot&#xff09;是 vue 为组件的封装者提供的功能&#xff0c;允许开发者在封装组件时&#xff0c;把不确定的、希望由用户指定的部分定义为插槽。 可以把插槽认为是组件封装期间&#xff0c;为用户预留的内容的占位符&#xff0c;在组件的使用者…

Vue3入门

文章目录 一、Vue3介绍二、Vue3项目创建1&#xff09;使用vue-cli创建vue3项目2&#xff09;使用vite创建vue3项目 三、Setup函数vue2的创建vue实例和vue3创建vue实例的区别 四、ref和reactive1&#xff09;ref函数2&#xff09;reactive函数3&#xff09;reactive对比ref4&…

苹果最新系统iOS 17的调试和适配方法 - Xcode 14.3.1 真机调试指南

最近苹果发布了iOS 17作为其最新操作系统版本&#xff0c;作为开发者&#xff0c;你可能需要了解如何在Xcode 14.3.1中进行真机调试和适配。本文将为你详细介绍步骤和注意事项。 I. 检查Xcode版本 在开始之前&#xff0c;确保你已经安装了Xcode 14.3.1或更高版本。你可以在Xco…

利用先进的条形码识别和 OCR 技术改善机场行李处理

机场每年处理数百万件行李&#xff0c;主要航空公司每家运输超过 1 亿件行李。每年有 2500 万件行李被错误处理&#xff0c;正确处理至关重要。使用最好的技术是关键&#xff0c;首先是从机场到飞机的正确转乘。 行李分拣 Dynamsoft 的客户是一家机场行李分拣解决方案提供商。…

排序算法-快速排序(含C语言代码示例)

一、算法介绍 快速排序&#xff08;QuickSort&#xff09;是一种常用的高效排序算法&#xff0c;由Tony Hoare在1960年提出。它采用分治法&#xff08;Divide and Conquer&#xff09;策略&#xff0c;通过将原始数组分成较小的子数组来解决排序问题。下面是对快速排序的详细介…

c语言将csv文件中的XY轴数据转换为html波形图

目标&#xff1a; c语言实现一个最简化的csv转html波形图显示方案。 csv文件格式&#xff1a; 共两行数据&#xff0c;第一行是x轴数据&#xff0c;第二行是y轴数据。 csv文件名分为3段: 波形图名称&#xff0c;x轴名称&#xff0c;y轴名称。 c代码&#xff1a; int csv2html…

网络命令ping和telnet

1. 请解释ping和telnet的工作原理。 ping和telnet是两种常用的网络工具&#xff0c;其工作原理分别如下&#xff1a; ping&#xff1a; 目的&#xff1a;ping主要用于检查网络是否通畅以及测量网络连接速度。工作原理&#xff1a;ping是基于ICMP&#xff08;Internet Control …

HTML5:dialog

JavaScript 练手小技巧&#xff1a;HTML5 的 dialog 标签制作对话框_dialog html-CSDN博客 <dialog id"dialog"> <h2 align"center">修改</h2> <input type"text" id"title1" placeholder"标题" value…