Flink学习笔记(七)处理函数

7. 处理函数

Flink的多层API:

在更底层,我们可以不定义任何具体的算子(比如 map,filter,或者 window),而只是提 炼出一个统一的“处理”(process)操作——它是所有转换算子的一个概括性的表达,可以自 定义处理逻辑,所以这一层接口就被叫作“处理函数”(process function)。因为它不限定 具体做什么,所以理论上我们可以做任何事情,实现所有需求。

7.1 基本处理函数(ProcessFunction)

处理函数主要是定义数据流的转换操作,所以也可以把它归到转换算子中。它所对应的函数 类,就叫作 ProcessFunction。

7.1.1 处理函数的功能和使用

在很多应 用需求中,要求我们对时间有更精细的控制,需要能够获取水位线,甚至要“把控时间”、定义什么时候做什么事,这就不是基本的时间窗口能够实现的了。就必须使用处理函数进行实现。

处理函数提供了一个“定时服务” (TimerService),我们可以通过它访问流中的事件(event)、时间戳(timestamp)、水位线 (watermark),甚至可以注册“定时事件”。处理函数继承了 AbstractRichFunction 抽象类, 所以拥有富函数类的所有特性,同样可以访问状态(state)和其他运行时信息。此外,处理函 数还可以直接将数据输出到侧输出流(side output)中。所以,处理函数是最为灵活的处理方 法,可以实现各种自定义的业务逻辑;同时也是整个 DataStream API 的底层基础。

处理函数的使用与基本的转换操作类似,只需要直接基于 DataStream 调用.process()方法 就可以了。ProcessFunction 不是接口,而是一个抽象类,继承了 AbstractRichFunction; 所以所有的处理函数,都是富函数(RichFunction), 富函数可以调用的东西这里同样都可以调用。

stream.process(new MyProcessFunction())
7.1.2 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 {}
    ...
}
  1. 抽象方法.processElement()

用于“处理元素”,定义了处理的核心逻辑。这个方法对于流中的每个元素都会调用一次, 参数包括三个:输入数据值 value,上下文 ctx,以及“收集器”(Collector)out。方法没有返 回值,处理之后的输出数据是通过收集器 out 来定义的。

  • value:当前流中的输入元素,也就是正在处理的数据,类型与流中数据类 型一致。

  • ctx:类型是 ProcessFunction 中定义的内部抽象类 Context,表示当前运行的 上下文,可以获取到当前的时间戳,并提供了用于查询时间和注册定时器的“定时服 务”(TimerService),以及可以将数据发送到“侧输出流”(side output)的方法.output()。

    Context 抽象类定义如下:

    public abstract class Context {
        public abstract Long timestamp();
        public abstract TimerService timerService();
        public abstract <X> void output(OutputTag<X> outputTag, X value);
    }
    
  • out:“收集器”(类型为 Collector),用于返回输出数据。使用方式与 flatMap 算子中的收集器完全一样,直接调用 out.collect()方法就可以向下游发出一个数据。 这个方法可以多次调用,也可以不调用。

  1. 非抽象方法.onTimer()

这个方法只有在注册 好的定时器触发的时候才会调用,而定时器是通过“定时服务”TimerService 来注册的。打个 比方,注册定时器(timer)就是设了一个闹钟,到了设定时间就会响;而.onTimer()中定义的, 就是闹钟响的时候要做的事。所以它本质上是一个基于时间的“回调”(callback)方法,通过 时间的进展来触发;在事件时间语义下就是由水位线(watermark)来触发了。

与.processElement()类似,定时方法.onTimer()也有三个参数:时间戳(timestamp),上下 文(ctx),以及收集器(out)。这里的 timestamp 是指设定好的触发时间,事件时间语义下当然就是水位线了。另外这里同样有上下文和收集器,所以也可以调用定时服务(TimerService), 以及任意输出处理之后的数据。

7.1.3 处理函数的分类

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

​ 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)做连接之后的产物。

7.2 按键分区处理函数(KeyedProcessFunction)

在 Flink 程序中,为了实现数据的聚合统计,或者开窗计算之类的功能,我们一般都要先 用 keyBy 算子对数据流进行“按键分区”,得到一个 KeyedStream。也就是指定一个键(key), 按照它的哈希值(hash code)将数据分成不同的“组”,然后分配到不同的并行子任务上执行 计算;这相当于做了一个逻辑分流的操作,从而可以充分利用并行计算的优势实时处理海量数据。

只有在 KeyedStream 中才支持使用 TimerService 设置定时器的 操作。所以一般情况下,我们都是先做了 keyBy 分区之后,再去定义处理操作。

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

KeyedProcessFunction 的一个特色,就是可以灵活地使用定时器。 定时器(timers)是处理函数中进行时间相关操作的主要机制。在.onTimer()方法中可以实 现定时处理的逻辑,而它能触发的前提,就是之前曾经注册过定时器、并且现在已经到了触发时间。注册定时器的功能,是通过上下文中提供的“定时服务”(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 内部会用一个优先队列将它 们的时间戳(timestamp)保存起来,排队等待执行。

7.2.2 KeyedProcessFunction 的使用

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

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

KeyedProcessFunction 也是继承自 AbstractRichFunction 的一个抽象类,源码中定 义如下:

public abstract class KeyedProcessFunction<K, 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 {}
	public abstract class Context {...}
...
}

KeyedProcessFunction与 ProcessFunction 的定义几乎完全一样,区别只是在于类型参数多了一个 K, 这是当前按键分区的 key 的类型。同样地,必须实现一个.processElement()抽象方法,用 来处理流中的每一个数据;另外还有一个非抽象方法.onTimer(),用来定义定时器触发时的回调操作。由于定时器只能在 KeyedStream 上使用,所以在 KeyedProcessFunction 这里,才真正对时间有了精细的控制,定时方法.onTimer()才能发挥他的作用。

7.3 窗口处理函数

​ 除 了 KeyedProcessFunction , 另 外 一 大 类 常 用 的 处 理 函 数 , 就 是 基 于 窗 口 的 ProcessWindowFunction 和 ProcessAllWindowFunction 。

7.3.1 窗口处理函数的使用

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

​ 窗 口 处 理 函 数 ProcessWindowFunction 的 使 用 与 其 他 窗 口 函 数 类 似 , 也 是 基 于 WindowedStream 直接调用方法就可以,只不过这时调用的是.process()。

stream.keyBy( t -> t.f0 )
    .window( TumblingEventTimeWindows.of(Time.seconds(10)) )
    .process(new MyProcessWindowFunction())
// 处理时间语义,不需要分配时间戳和watermark
SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource());

// 要用定时器,必须基于KeyedStream
stream.keyBy(data -> true)//.keyBy(data -> true)是将所有数据的 key 都指定为了 true,其实就是所有数据拥有相同的 key,会分配到同一个分区。
    .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();
7.3.2 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。

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);
}

7.4 应用案例——Top N

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

简单的增量聚合可以得到 url 链接的访问量,但是后续的排序输出 TopN 就很难 实现了。所以接下来我们用窗口处理函数进行实现。

7.4.1 使用 ProcessAllWindowFunction

一种最简单的想法是,不区分 url 链接,而是将所有访问数据都收集起来,统一 进行统计计算。所以可以不做 keyBy,直接基于 DataStream 开窗,然后使用全窗口函数 ProcessAllWindowFunction 来进行处理。

在窗口中可以用一个 HashMap 来保存每个 url 的访问次数,只要遍历窗口中的所有数据, 自然就能得到所有 url 的热门度。最后把 HashMap 转成一个列表 ArrayList,然后进行排序、 取出前两名输出就可以了。

代码:

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessAllWindowFunction;
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 org.apache.flink.util.Collector;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;

public class ProcessAllWindowTopN {
    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 value) throws Exception {
                    return value.url;
                }
            })
            .windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))    // 开滑动窗口
            .process(new ProcessAllWindowFunction<String, String, TimeWindow>() {
                @Override
                public void process(Context context, Iterable<String> elements, Collector<String> out) throws Exception {
                    HashMap<String, Long> urlCountMap = new HashMap<>();
                    // 遍历窗口中数据,将浏览量保存到一个 HashMap 中
                    for (String url : elements) {
                        if (urlCountMap.containsKey(url)) {
                            long count = urlCountMap.get(url);
                            urlCountMap.put(url, count + 1L);
                        } else {
                            urlCountMap.put(url, 1L);
                        }
                    }
                    ArrayList<Tuple2<String, Long>> mapList = new ArrayList<Tuple2<String, Long>>();
                    // 将浏览量数据放入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");
                    out.collect(result.toString());
                }
            });

        result.print();

        env.execute();
    }
}

ClickSource.java

import org.apache.flink.streaming.api.functions.source.SourceFunction;

import java.util.Calendar;
import java.util.Random;

public class ClickSource implements SourceFunction<Event> {
    // 声明一个布尔变量,作为控制数据生成的标识位
    private Boolean running = true;
    @Override
    public void run(SourceContext<Event> ctx) throws Exception {
        Random random = new Random();
        // 在指定的数据集中随机选取数据
        String[] users = {"Mary", "Alice", "Bob", "Cary"};
        String[] urls = {"./home", "./cart", "./fav", "./prod?id=1", "./prod?id=2"};

        while (running) {
            ctx.collect(new Event(
                    users[random.nextInt(users.length)],
                    urls[random.nextInt(urls.length)],
                    Calendar.getInstance().getTimeInMillis()
            ));
            // 隔1秒生成一个点击事件,方便观测
            Thread.sleep(1000);
        }
    }
    @Override
    public void cancel() {
        running = false;
    }
}

Event.java

import java.sql.Timestamp;

public class Event {
    public String user;
    public String url;
    public Long timestamp;

    public Event() {
    }

    public Event(String user, String url, Long timestamp) {
        this.user = user;
        this.url = url;
        this.timestamp = timestamp;
    }

    @Override
    public String toString() {
        return "Event{" +
                "user='" + user + '\'' +
                ", url='" + url + '\'' +
                ", timestamp=" + new Timestamp(timestamp) +
                '}';
    }
}

测试结果:

在这里插入图片描述

7.4.2 使用 KeyedProcessFunction

在全窗口函数中定义了 HashMap 来统计 url 链接的浏览量,计算过程是要先收集齐所有数据、然后再逐一遍历更新 HashMap, 这显然不够高效。如果我们可以利用增量聚合函数的特性,每来一条数据就更新一次对应 url 的浏览量,那么到窗口触发计算时只需要做排序输出就可以了。

基于这样的想法,我们可以从两个方面去做优化:一是对数据进行按键分区,分别统计浏 览量;二是进行增量聚合,得到结果最后再做排序输出。所以,我们可以使用增量聚合函数 AggregateFunction 进行浏览量的统计,然后结合 ProcessWindowFunction 排序输出来实现 Top N 的需求。

总结处理流程如下:

(1)读取数据源;

(2)筛选浏览行为(pv);

(3)提取时间戳并生成水位线;

(4)按照 url 进行 keyBy 分区操作;

(5)开长度为 1 小时、步长为 5 分钟的事件时间滑动窗口;

(6)使用增量聚合函数 AggregateFunction,并结合全窗口函数 WindowFunction 进行窗口 聚合,得到每个 url、在每个统计窗口内的浏览量,包装成 UrlViewCount;

(7)按照窗口进行 keyBy 分区操作;

(8)对同一窗口的统计结果数据,使用 KeyedProcessFunction 进行收集并排序输出。

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.configuration.Configuration;
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.streaming.api.functions.windowing.ProcessWindowFunction;
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 org.apache.flink.util.Collector;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Comparator;

public class KeyedProcessTopN {

    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分组,求出每个url的访问量
        SingleOutputStreamOperator<UrlViewCount> urlCountStream =
                eventStream.keyBy(data -> data.url)//按url分组
                        .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))//开滑动窗口
                        .aggregate(new UrlViewCountAgg(),
                                new UrlViewCountResult());


        // 对结果中同一个窗口的统计数据,进行排序处理
        SingleOutputStreamOperator<String> result = urlCountStream.keyBy(data -> data.windowEnd)
                .process(new TopN(2));

        result.print("result");

        env.execute();
    }

    // 自定义增量聚合
    public static class UrlViewCountAgg implements AggregateFunction<Event, Long, Long> {
        @Override
        public Long createAccumulator() {
            return 0L;
        }

        @Override
        public Long add(Event value, Long accumulator) {
            return accumulator + 1;
        }

        @Override
        public Long getResult(Long accumulator) {
            return accumulator;
        }

        @Override
        public Long merge(Long a, Long b) {
            return null;
        }
    }

    // 自定义全窗口函数,只需要包装窗口信息
    public static class UrlViewCountResult extends ProcessWindowFunction<Long, UrlViewCount, String, TimeWindow> {

        @Override
        public void process(String url, Context context, Iterable<Long> elements, Collector<UrlViewCount> out) throws Exception {
            // 结合窗口信息,包装输出内容
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            out.collect(new UrlViewCount(url, elements.iterator().next(), start, end));
        }
    }

    // 自定义处理函数,排序取top n
    public static class TopN extends KeyedProcessFunction<Long, UrlViewCount, String>{
        // 将n作为属性
        private Integer n;
        // 定义一个列表状态
        private ListState<UrlViewCount> urlViewCountListState;

        public TopN(Integer n) {
            this.n = n;
        }

        @Override
        public void open(Configuration parameters) throws Exception {
            // 从环境中获取列表状态句柄
            urlViewCountListState = getRuntimeContext().getListState(
                    new ListStateDescriptor<UrlViewCount>("url-view-count-list",
                            Types.POJO(UrlViewCount.class)));
        }

        @Override
        public void processElement(UrlViewCount value, Context ctx, Collector<String> out) throws Exception {
            // 将count数据添加到列表状态中,保存起来
            urlViewCountListState.add(value);
            // 注册 window end + 1ms后的定时器,等待所有数据到齐开始排序
            ctx.timerService().registerEventTimeTimer(ctx.getCurrentKey() + 1);
        }

        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
            // 将数据从列表状态变量中取出,放入ArrayList,方便排序
            ArrayList<UrlViewCount> urlViewCountArrayList = new ArrayList<>();
            for (UrlViewCount urlViewCount : urlViewCountListState.get()) {
                urlViewCountArrayList.add(urlViewCount);
            }
            // 清空状态,释放资源
            urlViewCountListState.clear();

            // 排序
            urlViewCountArrayList.sort(new Comparator<UrlViewCount>() {
                @Override
                public int compare(UrlViewCount o1, UrlViewCount o2) {
                    return o2.count.intValue() - o1.count.intValue();
                }
            });

            // 取前两名,构建输出结果
            StringBuilder result = new StringBuilder();
            result.append("========================================\n");
            result.append("窗口结束时间:" + new Timestamp(timestamp - 1) + "\n");
            for (int i = 0; i < this.n; i++) {
                UrlViewCount UrlViewCount = urlViewCountArrayList.get(i);
                String info = "No." + (i + 1) + " "
                        + "url:" + UrlViewCount.url + " "
                        + "浏览量:" + UrlViewCount.count + "\n";
                result.append(info);
            }
            result.append("========================================\n");
            out.collect(result.toString());
        }
    }
}

其中,UrlViewCount是一个POJO类:

import java.sql.Timestamp;

public class UrlViewCount {
    public String url;
    public Long count;
    public Long windowStart;
    public Long windowEnd;

    public UrlViewCount() {
    }

    public UrlViewCount(String url, Long count, Long windowStart, Long windowEnd) {
        this.url = url;
        this.count = count;
        this.windowStart = windowStart;
        this.windowEnd = windowEnd;
    }

    @Override
    public String toString() {
        return "UrlViewCount{" +
                "url='" + url + '\'' +
                ", count=" + count +
                ", windowStart=" + new Timestamp(windowStart) +
                ", windowEnd=" + new Timestamp(windowEnd) +
                '}';
    }
}

7.5 侧输出流(Side Output)

侧输出流可以认为是“主流”上分叉出的“支流”,所以可以由一条流产生出多条流,而且这 些流中的数据类型还可以不一样。利用这个功能可以很容易地实现“分流”操作。

具体应用时,只要在处理函数的.processElement()或者.onTimer()方法中,调用上下文 的.output()方法就可以了。

DataStream<Integer> stream = env.addSource(...);
SingleOutputStreamOperator<Long> longStream = stream.process(new ProcessFunction<Integer, Long>() {
	 @Override
	 public 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));
	 }
});

总结

本文详细介绍了处理函数的功能和底层的结构,重点讲解了最为常用的KeyedProcessFunction 和 ProcessWindowFunction,并实现了电商应用中 Top N 的经典案例,另外还介绍了侧输出流的用法。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半岛铁子_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值