如何使用Flink实现排行榜TopN

TopN

要统计10秒内访问量最多的5条url,5秒钟刷新一次

分析题目,10秒内的数据进行统计,5秒钟刷新一次,首先确定是滑动窗口

第一个方案(差):流水线使用乱序+延迟,计算则不用分区,针对10秒内所有的数据,使用增量函数+全窗口函数,统计所有url的访问次数,对其所有的url的访问次数进行排序,再输出前5条

第二个方案(还行):流水线使用乱序+延迟,计算则针对url进行分区,各个分区使用增量函数+全窗口函数统计出每个url的访问总次数,再把这些分区的url统计结果放到一个分布式缓存里,注册一个延迟触发事件时间定时器,触发时间就是窗口结束时间+1,进行排序后输出。

思考几个问题:

  • 为什么要放入到分布式缓存里?

    如果设置并行度,多个子任务,无论是统计一个url的并行任务,还是统计多个url的并行任务,肯定都不在一个task node上执行,多个task node可能部署在多台机器,数据都不是共享的,需要一个分布式的缓存,能够被所有task node访问以及存储,这样就可以合并多个算子任务结果。

    在这里插入图片描述

    如上图所示,这里的分布式缓存实际上在flink上定义为“列表状态”,每个元素实际上是分区的聚合后的数据

    注册定时器由第一个分区数据到达后来执行,后续的数据到达后注册是无效操作,因为定时器的触发时间是同一个(各个分区的windowEnd相同)。

  • 为什么要使用定时器?

    因为在这些分区将各自url的统计结果 输出给最后同一个算子任务时,实际上这些分区的窗口都已经到了结束时间,大家都不会再等待新数据进来进行计算了,那么有多少个分区数据给最后一个算子任务什么时候能全部收集到这个没办法判断(毕竟不是全量统计),所以就需要在一个分区的数据到达时,就设置一个定时器,并将这个分区的统计结果放入到缓存,其他分区数据到达时,放入到缓存,也设置定时器,但是发现第一个分区已经设置定时器了,就不会再设置定时器,最后在定时器里缓存就保存了所有分区的数据,排名即可。

  • 为什么定时器要延迟触发?还要设置1毫秒?

    这里仅仅是遵循‘允许迟到的分区总数据延迟进来“的原则,可以是1毫秒,也可以是100毫秒

    但是不能单纯依靠这里的延迟去处理迟到数据,前面如果没有设置水位线延迟的话,这里设置的延迟时间实际意义上表示“等待所有分区数据到最后一个算子任务的总时间”,设置的短会出问题。

  • 第二个方案优于第一个方案的点在于:

    • 第一个方案:所有数据的增量聚合是一个算子任务来完成
    • 第二个方案,根据url分区,多个算子任务按照自己的分区增量聚合url的访问次数
    • 最后都是一个窗口里进行排序,这里两个没有区别,毕竟做top就得将所有数据放在一起统计

(1)方案实现的前提:

  • 数据来源的pojo:

       public  static class Event {
            /**
             * 用户姓名
             */
            public String user;
            /**
             * url访问地址路径
             */
            public String url;
            /**
             * 用户访问时间
             */
            public Long timestamp;
    
            public Event() {
            }
    
            public String getUser() {
                return user;
            }
    
            public String getUrl() {
                return url;
            }
    
            public Long getTimestamp() {
                return timestamp;
            }
    
            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) +
                        '}';
            }
        }
    
  • 数据来源:

    模拟的1秒钟随机发一个url请求,不会停止

       public  static class customSource implements SourceFunction<Event> {
            private Boolean running = true;
            private int number = 0;
    
            /**
             * sourceContext.collect会返回数据
             * run一旦结束,数据源就停止发送
             *
             * @param sourceContext
             */
            @Override
            public void run(SourceContext<Event> sourceContext) throws InterruptedException {
                Random random = new Random();
                String[] users = {"Mary", "Alice", "Bob", "Cary"};
                String[] urls = {"./home", "./cart", "./fav", "./prod?id=1",
                        "./prod?id=2"};
                while (running) {
                    sourceContext.collect(new Event(users[random.nextInt(users.length)],
                            urls[random.nextInt(urls.length)], Calendar.getInstance().getTimeInMillis()
                    ));
                    Thread.sleep(1000);
                }
    
            }
    
            /**
             * 中止数据
             */
            @Override
            public void cancel() {
                running = false;
            }
        }
    

(2)第一个方案

  • 整体流程:

    • assignTimestampsAndWatermarks没有特殊要求,使用乱序流,延迟3秒
    • 因为是攒10秒钟的数据,keyBy必须全部都走一个分区,所以返回是true
    • 滑动窗口,所以使用SlidingEventTimeWindows,10秒钟的窗口,滑动5秒
    • 这里因为增量聚合+全窗口,所以aggregate传入两个函数
        public static void main(String[] args) throws Exception {
            StreamExecutionEnvironment env =
                    StreamExecutionEnvironment.getExecutionEnvironment();
            env.getConfig().setAutoWatermarkInterval(200);
            env.addSource(new aggregateTest.customSource())
                    .assignTimestampsAndWatermarks(
                            WatermarkStrategy.<aggregateTest.Event>forBoundedOutOfOrderness(Duration.ofSeconds(3)).
                                    withTimestampAssigner((SerializableTimestampAssigner<aggregateTest.Event>) 
                                                          (event, l) -> event.timestamp))
                    .keyBy(event -> true)
                    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                    .aggregate(new CustomAggregateFunction(), new CustomProcessWindowFunction())
                    .print();
            env.execute();
        }
    
  • 增量聚合函数

    • 初始化一个hashmap,key就是url,value就是访问次数
    • add 就是把所有url进行一次聚合,如果添加了就访问次数+1,如果没添加过就设置初始访问次数为1
        private static class CustomAggregateFunction implements AggregateFunction<aggregateTest.Event, HashMap<String, Long>, HashMap<String, Long>> {
    
    
            @Override
            public HashMap<String, Long> createAccumulator() {
                return new HashMap<>();
            }
    
            @Override
            public HashMap<String, Long> add(aggregateTest.Event event, HashMap<String, Long> tmpHashMap) {
                if (tmpHashMap.containsKey(event.url)) {
                    tmpHashMap.put(event.url, tmpHashMap.get(event.url) + 1);
                } else {
                    tmpHashMap.put(event.url, 1L);
                }
                return tmpHashMap;
            }
    
            @Override
            public HashMap<String, Long> getResult(HashMap<String, Long> tmpHashMap) {
                return tmpHashMap;
            }
    
            @Override
            public HashMap<String, Long> merge(HashMap<String, Long> stringLongHashMap, HashMap<String, Long> acc1) {
                return null;
            }
        }
    
  • 全窗口函数

    • 这里数据肯定是全部的,所以迭代器直接获取第一个数据就行
    • 对map排序,可以利用java 8 lambda,对map 的value进行转成list
       public static class CustomProcessWindowFunction extends ProcessWindowFunction<HashMap<String, Long>, String, Boolean, TimeWindow> {
            /**
             * @param key       分区key返回的值
             * @param context   上下文(可以获取当前处理时间、当前流水线、窗口状态)
             * @param iterable  全量数据的迭代器
             * @param collector
             * @throws Exception
             */
            @Override
            public void process(Boolean key, Context context, Iterable<HashMap<String, Long>> iterable, Collector<String> collector) throws Exception {
                List<HashMap<String, Long>> list = new ArrayList<>();
    
                for (HashMap<String, Long> map : iterable) {
                    list.add(map);
                }
                HashMap<String, Long> valueMap = list.get(0);
                List<Map.Entry<String, Long>> finalValue = sortByMap(valueMap);
                TimeWindow window = context.window();
                long start = window.getStart();
                long end = window.getEnd();
                collector.collect("当前时间:" + new Date() + "分区key:" + key + "窗口起始时间:" + new Date(start) + "\t 窗口结束时间:" + new Date(end) + "\t排在前五名的是:" + finalValue.toString());
            }
    
            private List<Map.Entry<String, Long>> sortByMap(HashMap<String, Long> valueMap) {
                return valueMap.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).collect(Collectors.toList());
    
            }
        }
    
  • 看最后结果:

    • 当前时间就是滑动距离
  • 确认下访问次数,因为1秒钟模拟1个请求,正好随机的url都是5个,那么所有url访问次数之和是10个,对上了
    在这里插入图片描述

(3)第二个方案

  • 整体流程

    • 流水线和窗口与第一个方案,不再叙述

    • 注意,这里keyBy是按照访问的url,不再全部返回true

    • 还有一点跟第一个方案的不同是在aggregate后又增了一个keyBy和 process函数

      .keyBy(urlCount -> urlCount.windowEnd)的原因在于必须是同一个窗口结束时间的才能一起计算

        public static void main(String[] args) throws Exception {
            StreamExecutionEnvironment env =
                    StreamExecutionEnvironment.getExecutionEnvironment();
            env.getConfig().setAutoWatermarkInterval(200);
            SingleOutputStreamOperator<String> returns = env.addSource(new aggregateTest.customSource())
                    .assignTimestampsAndWatermarks(
                            WatermarkStrategy.<aggregateTest.Event>forBoundedOutOfOrderness(Duration.ofSeconds(3)).
                                    withTimestampAssigner((SerializableTimestampAssigner<aggregateTest.Event>) (event, l) -> event.timestamp))
                    .keyBy(event -> event.url)
                    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                    .aggregate(new CustomAggregateFunction(), new CustomProcessWindowFunction())
                    .keyBy(urlCount -> urlCount.windowEnd)
                    .process(new CustomKeyProcessFunction())
                    .returns(String.class);
            returns.print();
            env.execute();
        }
    
  • 分区下的增量函数

    • 这里因为统计单个url的访问次数,所以一个对象就能解决问题
     private static class CustomAggregateFunction implements AggregateFunction<aggregateTest.Event, EventUrlCount, EventUrlCount> {
    
    
            @Override
            public EventUrlCount createAccumulator() {
                return new EventUrlCount("", 0L, 0L);
            }
    
            @Override
            public EventUrlCount add(aggregateTest.Event event, EventUrlCount tmpEventUrlCount) {
                tmpEventUrlCount.count += 1;
                tmpEventUrlCount.url = event.url;
                return tmpEventUrlCount;
            }
    
            @Override
            public EventUrlCount getResult(EventUrlCount urlCount) {
                return urlCount;
            }
    
            @Override
            public EventUrlCount merge(EventUrlCount urlCount1, EventUrlCount urlCount2) {
                return null;
            }
        }
    
  • 分区下的全窗口函数

    • 因为后面最后一个算子任务要根据数据的同一个结束时间来整理,所以这里一定要设置窗口结束时间,给到后面的keyBy使用。
     public static class CustomProcessWindowFunction extends ProcessWindowFunction<EventUrlCount, EventUrlCount, String, TimeWindow> {
    
    
            /**
             * 因为增量和全窗口一起使用,所以迭代器只会有一个数据
             *
             * @param s
             * @param context
             * @param iterable
             * @param collector
             * @throws Exception
             */
            @Override
            public void process(String s, Context context, Iterable<EventUrlCount> iterable, Collector<EventUrlCount> collector) throws Exception {
                EventUrlCount finalEventUrlCount = null;
                for (EventUrlCount urlCount : iterable) {
                    finalEventUrlCount = urlCount;
                }
                finalEventUrlCount.windowEnd = context.window().getEnd();
                collector.collect(finalEventUrlCount);
            }
        }
    
  • 最后合并的函数+定时器

    • open时,会定义一个分布式缓存,实际上是“状态list”,赋值给eventUrlCounts
    • 一旦一个分区数据到达时,放入到list,并注册一个定时器,触发时间就是窗口结束时间+1
    • 定时器实际上就是按照list的count进行排序,最后输出list即可
    • ⚠️这里没有把具体前几名的变量以参数传递进去,注意下
        public static class CustomKeyProcessFunction extends KeyedProcessFunction<Long, EventUrlCount, String> {
    
    
            ListState<EventUrlCount> eventUrlCounts;
    
            @Override
            public void open(Configuration parameters) throws Exception {
                super.open(parameters);
                eventUrlCounts = getRuntimeContext().getListState(new ListStateDescriptor<>("123-demo-list", EventUrlCount.class));
            }
    
          
            @Override
            public void processElement(EventUrlCount urlCount, Context context, Collector<String> collector) throws Exception {
                eventUrlCounts.add(urlCount);
                context.timerService().registerEventTimeTimer(urlCount.windowEnd+1);
            }
    
            @Override
            public void onTimer(long timestamp, KeyedProcessFunction<Long, EventUrlCount, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
                List<EventUrlCount> finalList = sortByUrlCount(this.eventUrlCounts);
              //这里具体看前五名,这里需要作为参数传递进去
              finalList=finalList.sub
                out.collect("当前窗口结束时间:" + new java.sql.Time(timestamp) + "\t 当前排名为:" + JSON.toJSONString(finalList));
                this.eventUrlCounts.clear();
            }
    
            private List<EventUrlCount> sortByUrlCount(ListState<EventUrlCount> eventUrlCounts) throws Exception {
                List<EventUrlCount> list = new ArrayList<>();
                for (EventUrlCount urlCount : eventUrlCounts.get()) {
                    list.add(urlCount);
                }
                list = list.size() <= 5 ? list : list.subList(0, 5);
                List<EventUrlCount> sortList = list.stream().sorted(Comparator.comparing(EventUrlCount::getCount).reversed()).collect(Collectors.toList());
                return sortList;
            }
    
    
        }
    
  • 看最后的结果:
    在这里插入图片描述

    因为数据源跟第一个方案一样,所以所有url访问个数应该都为10

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 当然,在使用 Flink 编写一个 TopN 程序时,您需要遵循以下步骤: 1. 使用 Flink 的 DataStream API 从源(例如 Kafka、Socket 等)读取数据流。 2. 对数据流执行 map 操作,以将输入转换为键值对。 3. 使用 keyBy 操作将数据分区,并为每个分区执行 topN 操作。 4. 使用 Flink 的 window API 设置滑动窗口,按照您所选择的窗口大小进行计算。 5. 使用 reduce 操作聚合每个分区中的 topN 元素。 6. 最后,使用 Flink 的 sink API 将结果写入目的地(例如文件、数据库等)。 下面是一个使用 Flink 实现 TopN 的示例代码: ``` DataStream<String> inputStream = ...; DataStream<Tuple2<String, Integer>> keyValuePairs = inputStream .map(new MapFunction<String, Tuple2<String, Integer>>() { public Tuple2<String, Integer> map(String value) { String[] fields = value.split(","); String key = fields[0]; Integer count = Integer.parseInt(fields[1]); return new Tuple2<>(key, count); } }); KeyedStream<Tuple2<String, Integer>, String> keyedStream = keyValuePairs.keyBy(new KeySelector<Tuple2<String, Integer>, String>() { public String getKey(Tuple2<String, Integer> value) { return value.f0; } }); DataStream<Tuple2<String, Integer>> topN = keyedStream .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5))) .reduce(new ReduceFunction<Tuple2<String, Integer>>() { public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) { return new Tuple2<>(value1.f0, value1.f1 + value2.f1); } }) . ### 回答2: 使用Flink编写一个TopN实现可以使用窗口操作和排序算法来实现。下面是一个使用DataStream的例子: 1. 首先,我们需要定义输入数据流以及TopN的大小: ```java DataStream<Tuple2<String, Integer>> dataStream = ...; // 输入数据流 int n = ...; // TopN的大小 ``` 2. 然后,我们可以使用窗口操作来对输入数据进行分组和聚合。在这个例子中,我们可以使用滚动窗口,将所有数据划分为固定大小的窗口: ```java // 使用滚动窗口,每个窗口包含5个元素 WindowedStream<Tuple2<String, Integer>, String, TimeWindow> windowedStream = dataStream .keyBy(data -> data.f0) // 按照键进行分组 .window(TumblingProcessingTimeWindows.of(Time.seconds(1))); // 定义滚动窗口 ``` 3. 接下来,我们可以使用reduce函数对窗口中的数据进行聚合,并使用排序算法来获取TopN元素: ```java // 使用reduce函数对窗口中的数据进行聚合,并使用排序算法获取TopN元素 SingleOutputStreamOperator<List<Tuple2<String, Integer>>> topNStream = windowedStream .reduce((value1, value2) -> new Tuple2<>(value1.f0, value1.f1 + value2.f1)) // 将窗口中的数据聚合 .windowAll(TumblingProcessingTimeWindows.of(Time.seconds(1))) // 在所有窗口中操作 .process(new TopNFunction(n)); // 自定义的处理函数,用于获取TopN元素 ``` 4. 最后,我们可以在输出流中打印或保存TopN元素: ```java topNStream.print(); // 打印TopN元素 // 自定义的处理函数,用于获取TopN元素 public static class TopNFunction extends ProcessAllWindowFunction<Tuple2<String, Integer>, List<Tuple2<String, Integer>>, TimeWindow> { private final int n; public TopNFunction(int n) { this.n = n; } @Override public void process(Context context, Iterable<Tuple2<String, Integer>> input, Collector<List<Tuple2<String, Integer>>> out) { List<Tuple2<String, Integer>> topN = new ArrayList<>(); for (Tuple2<String, Integer> value : input) { topN.add(value); } topN.sort((value1, value2) -> value2.f1 - value1.f1); // 根据元素的值进行降序排序 topN = topN.subList(0, Math.min(n, topN.size())); // 获取TopN元素 out.collect(topN); } } ``` 这样,我们就使用Flink编写了一个TopN实现。 ### 回答3: 使用Flink编写一个TopN问题的解决方案。我们可以按照以下步骤实现: 1. 从数据源读取数据:使用Flink提供的数据源API,从文件、Kafka等数据源中读取数据,将数据转换为DataStream。 2. 转换数据流:根据具体的需求,对数据流进行转换操作,例如筛选、过滤、聚合等。这些操作可以使用Flink提供的转换算子(Transformation Operators)实现。 3. 对数据流进行分组排序:使用Flink提供的GroupBy和Sort算子,对数据流进行分组和排序操作。可以将数据流按照指定的key进行分组,然后在每个组内按照指定的字段进行排序。 4. 实现TopN逻辑:根据具体的需求,选择合适的算法实现TopN逻辑。例如,可以使用Flink提供的KeyedProcessFunction来维护一个有序列表,然后在每个分组内进行排序,选取前N个元素。 5. 输出结果:根据具体的需求,选择适当的输出方式输出结果。例如,可以将结果写入文件、输出到Kafka等。 总结:使用Flink编写TopN问题的解决方案,需要将数据源转换为DataStream,对数据流进行转换、分组、排序等操作,并实现TopN逻辑,最后输出结果。Flink提供了丰富的API和算子,可以方便地实现TopN问题的解决方案。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值