【Flink】Flink的处理函数、TopN案例和侧输出流

目录

一、基本处理函数(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)


一、基本处理函数(ProcessFunction

1、处理函数的概念

        在更底层,我们可以不定义任何具体的算子(比如 map, filter ,或者 window ),而只是提 炼出一个统一的“处理”(process )操作——它是所有转换算子的一个概括性的表达,可以自 定义处理逻辑,所以这一层接口就被叫作“处理函数”(process function )。
        在处理函数中,我们直面的就是数据流中最基本的元素:数据事件(event )、状态( state )以及时间(time )。这就相当于对流有了完全的控制权。 

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

2、ProcessFunction 解析

(1)抽象方法.processElement()

        用于“处理元素”,定义了处理的核心逻辑。这个方法对于流中的每个元素都会调用一次, 参数包括三个:输入数据值 value上下文 ctx ,以及“收集器”( Collectorout 。方法没有返 回值,处理之后的输出数据是通过收集器 out 来定义的。 ​​​​​​​​​​​​​​
        
        ProcessFunction 可以轻松实现 flatMap 这样的基本转换功能(当然 map filter 更不在话下);而通过富函数提供的获取上下文方法 .getRuntimeContext() ,也可以自定义状态(state )进行处理。
env.addSource()
   .process(new ProcessFunction<Event, String>() {
            @Override
            public void processElement(Event event, ProcessFunction<Event, String>.Context context, Collector<String> 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();

​​​​​​​(2)非抽象方法.onTimer()

        用于定义定时触发的操作,这个方法只有在注册好的定时器触发的时候才会调用,而定时器是通过“定时服务”TimerService 来注册的。打个比方,注册定时器(timer)就是设了一个闹钟,到了设定时间就会响;而.onTimer()中定义的,就是闹钟响的时候要做的事。所以它本质上是一个基于时间的“回调”(callback)方法,通过时间的进展来触发;在事件时间语义下就是由水位线(watermark)来触发了。与.processElement()类似,定时方法.onTimer()也有三个参数:时间戳(timestamp),上下文(ctx),以及收集器(out)。

        定时器真正的设置需要用到上下文 ctx 中的定时服务。在 Flink 中,只有“按键分区流”KeyedStream 才支持设置定时器的操作。

3、处理函数的分类

      对于不同类型的流,其实都可以直接调用.process()方法进行自定义处理,这时传入的参数就都叫作处理函数。Flink 提供了 8 个不同的处理函数: ​​​​​​​​​​​​​​

1ProcessFunction

最基本的处理函数,基于 DataStream 直接调用 .process() 时作为参数传入。

(2)KeyedProcessFunction

对流按键分区后的处理函数,基于 KeyedStream 调用 .process() 时作为参数传入。要想使用定时器,比如基于 KeyedStream。KeyedProcessFunction 的一个特色,就是可以灵活地使用定时器。
Flink 的定时器同样具有容错性,它和状态一起都会被保存到一致性检查点( checkpoint )中。当发生故障时,Flink 会重启并读取检查点中的状态,恢复定时器。如果是处理时间的定时器,有可能会出现已经“过期”的情况,这时它们会在重启时被立刻触发。
        // 要用定时器,必须基于KeyedStream
        stream.keyBy(data -> true)
                .process(new KeyedProcessFunction<Boolean, Event, String>() {
                    @Override
                    public void processElement(Event value, Context ctx, Collector<String> 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<String> out) throws Exception {
                        out.collect("定时器触发,触发时间:" + new Timestamp(timestamp));
                    }
                })
                .print();

(3)ProcessWindowFunction

开窗之后的处理函数,也是全窗口函数的代表。基于 WindowedStream 调用 .process()时作为参数传入。ProcessWindowFunction 既是处理函数又是全窗口函数。从名字上也可以推测出,它的本
质似乎更倾向于“窗口函数”一些。
ProcessWindowFunction 中除了 .process()方法外,并没有.onTimer()方法, 而是多出1个.clear() 方法。从名字就可以看出,这主要是方便我们进行窗口的清理工作。如果我们自定义了窗口状态,那么必须在.clear() 方法中进行显式地清除,避免内存溢出。
stream.keyBy( t -> t.f0 )
     .window( TumblingEventTimeWindows.of(Time.seconds(10)) )
     .process(new MyProcessWindowFunction())

(4)ProcessAllWindowFunction

同样是开窗之后的处理函数,基于 AllWindowedStream 调用 .process() 时作为参数传入。

5)CoProcessFunction

合并( connect )两条流之后的处理函数,基于 ConnectedStreams 调用 .process() 时作为参数传入。关于流的连接合并操作,我们会在后续章节详细介绍。

(6)ProcessJoinFunction

间隔连接( interval join )两条流之后的处理函数,基于 IntervalJoined 调用 .process() 时作为参数传入。

(7)BroadcastProcessFunction

广播连接流处理函数,基于 BroadcastConnectedStream 调用 .process()时作为参数传入。

(8)KeyedBroadcastProcessFunction

按键分区的广播连接流处理函数,同样是基于 BroadcastConnectedStream 调用 .process() 时作为参数传入。

二、应用案例——Top N ​​​​​​​

        网站中一个非常经典的例子,就是实时统计一段时间内的热门 url 。例如,需要统计最近10 秒钟内最热门的两个 url 链接,并且每 5 秒钟更新一次。我们知道,这可以用一个滑动窗口来实现,而“热门度”一般可以直接用访问量来表示。于是就需要开滑动窗口收集 url 的访问数据,按照不同的 url 进行统计,而后汇总排序并最终输出前两名。这其实就是著名的“ Top N ”问题。

1、使用 ProcessAllWindowFunction

        将所有访问数据都收集起来,统一进行统计计算。所以可以不做 keyBy,基于 DataStream 开窗,然后使用全窗口函数ProcessAllWindowFunction 来进行处理。在窗口中可以用一个 HashMap 来保存每个 url 的访问次数,只要遍历窗口中的所有数据,自然就能得到所有 url 的热门度。最后把 HashMap 转成一个列表 ArrayList ,然后进行排序、取出前两名输出就可以了。
public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        // 由于使用事件时间,需要先定义时间戳和水位线
        SingleOutputStreamOperator<Event> eventStream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<Event>forMonotonousTimestamps()
                                .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                                    @Override
                                    public long extractTimestamp(Event element, long recordTimestamp) {
                                        return element.timestamp;
                                    }
                                })
                );

        // 只需要url就可以统计数量,所以转换成String直接开窗统计
        SingleOutputStreamOperator<String> result = eventStream.map(new MapFunction<Event, String>() {
                    @Override
                    public String map(Event event) throws Exception {
                        return event.url;
                    }
                }).windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))  // 开滑动窗口
                .process(new ProcessAllWindowFunction<String, String, TimeWindow>() {
                    @Override
                    public void process(Context context, Iterable<String> iterable, Collector<String> collector) throws Exception {
                        HashMap<String, Long> urlCountMap = new HashMap<>();
                        // 遍历窗口中的数据,将浏览量保存到HashMap中
                        for (String url : iterable) {
                            urlCountMap.put(url, urlCountMap.getOrDefault(url, 0L) + 1L);
                        }
                        ArrayList<Tuple2<String, Long>> mapList = new ArrayList<>();
                        for (String key : urlCountMap.keySet()) {
                            mapList.add(Tuple2.of(key, urlCountMap.get(key)));
                        }
                        mapList.sort(new Comparator<Tuple2<String, Long>>() {
                            @Override
                            public int compare(Tuple2<String, Long> o1, Tuple2<String, Long> o2) {
                                return o2.f1.intValue() - o1.f1.intValue();
                            }
                        });
                        // 取排序后的前两名,构建输出结果
                        StringBuilder result = new StringBuilder();
                        result.append("========================================\n");
                        for (int i = 0; i < 2; i++) {
                            Tuple2<String, Long> 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();
    }

2、使用 KeyedProcessFunction

我们可以从两个方面去做优化:一是对数据进行按键分区,分别统计浏览量;二是进行增量聚合,得到结果最后再做排序输出。所以,我们可以使用增量聚合函数AggregateFunction 进行浏览量的统计,然后结合 ProcessWindowFunction 排序输出来实现 Top N 的需求。
总结处理流程如下:
1 )读取数据源;
(2)筛选浏览行为( pv );
(3)提取时间戳并生成水位线;
(4)按照 url 进行 keyBy 分区操作;
(5)开长度为 1 小时、步长为 5 分钟的事件时间滑动窗口;
(6)使用增量聚合函数 AggregateFunction ,并结合全窗口函数 WindowFunction 进行窗口
聚合,得到每个 url 、在每个统计窗口内的浏览量,包装成 UrlViewCount
(7)按照窗口进行 keyBy 分区操作;
(8)对同一窗口的统计结果数据,使用 KeyedProcessFunction 进行收集并排序输出。

三、侧输出流(Side Output

侧输出流可以认为是“主流”上分叉出的“支流”,所以可以由一条流产生出多条流,而且这些流中的数据类型还可以不一样。利用这个功能可以很容易地实现“分流”操作。
​​​​​​​
如果想要获取这个侧输出流,可以基于处理之后的 DataStream 直接调用 .getSideOutput()
方法,传入对应的 OutputTag ,这个方式与窗口 API 中获取侧输出流是完全一样的。
OutputTag<String> outputTag = new OutputTag<String>("side-output") {};
DataStream<String> stringStream = longStream.getSideOutput(outputTag);

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值