Flink使用二次聚合实现TopN计算-乱序数据

本文介绍了如何使用Flink处理迟到数据,以实现每隔5秒输出最近10分钟内访问量最多的URL。通过设置watermark策略、使用SlidingEventTimeWindows和MapState,确保了迟到数据的正确处理。在KeyedProcessFunction中,使用两个定时器分别在窗口结束1秒后和61001毫秒后触发,以完成排序输出和清理状态。完整代码展示了这一过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、背景说明:

在上篇文章实现了TopN计算,但是碰到迟到数据则会无法在当前窗口计算,需要对其中的键控状态优化

Flink使用二次聚合实现TopN计算

本次需求是对数据进行统计,要求每隔5秒,输出最近10分钟内访问量最多的前N个URL,数据流预览如下(每次一条从端口传入):

208.115.111.72 - - 17/05/2015:10:25:49 +0000 GET /?N=A&page=21   //15:50-25:50窗口数据
208.115.111.72 - - 17/05/2015:10:25:50 +0000 GET /?N=A&page=21
208.115.111.72 - - 17/05/2015:10:25:51 +0000 GET /?N=A&page=21
208.115.111.72 - - 17/05/2015:10:25:52 +0000 GET /?N=A&page=21   //第一次触发计算,15:50-25:50窗口
208.115.111.72 - - 17/05/2015:10:25:47 +0000 GET /?N=A&          //迟到数据,不同url
208.115.111.72 - - 17/05/2015:10:25:53 +0000 GET /?N=A&page=21   //第二次触发计算,15:50-25:50窗口
208.115.111.72 - - 17/05/2015:10:25:46 +0000 GET /?N=A&page=21   //迟到数据
208.115.111.72 - - 17/05/2015:10:25:54 +0000 GET /?N=A&page=21   //第三次触发计算

最后统计输出结果如下(迟到数据均在25:50窗口):

==============2015-05-17 10:25:50.0==============               //第一次触发计算结果
Top1 Url:/?N=A&page=21 Counts:1
==============2015-05-17 10:25:50.0==============

==============2015-05-17 10:25:50.0==============               //第二次触发计算结果
Top1 Url:/?N=A&page=21 Counts:1
Top2 Url:/?N=A& Counts:1
==============2015-05-17 10:25:50.0==============

==============2015-05-17 10:25:50.0==============               //第三次触发计算结果
Top1 Url:/?N=A&page=21 Counts:2
Top2 Url:/?N=A& Counts:1
==============2015-05-17 10:25:50.0==============

二、实现过程

  1. 实现思路:
    ①建立环境,设置并行度及CK。
    ②定义watermark策略及事件时间,获取数据并对应到JavaBean。
    ③第一次聚合,按url分组开窗聚合,使用aggregate算子进行增量计算。
    ④第二次聚合,按窗口聚合,使用MapState存放数据,定义第一个定时器,在watermark达到后1秒触发,对窗口数据排序输出,定义第二个定时器,窗口关闭后才清楚状态。
    ⑤打印结果及执行。

ps:乱序数据不能使用读取本地文本文件的方式测试,文件读取加载比较快,无法观察到迟到数据处理效果,乱序数据的开发测试这里从服务器端口获取数据的方式测试

  1. 代码细节说明:

只针对优化部分代码说明,其他代码可以在顺序数据篇文章查看,这里提取重写KeyedProcessFunction里面方法的部分代码

@Override
public void processElement(UrlCount value, Context ctx, Collector<String> out) throws Exception {
	//状态装入数据
	mapState.put(value.getUrl(), value);
	//定时器,窗口一秒后触发
	ctx.timerService().registerEventTimeTimer(value.getWindowEnd()+1L);
	//再加一个定时器来清除状态用,在窗口关闭后再清除状态,这样延迟数据到达后窗口还能做排序
	ctx.timerService().registerEventTimeTimer(value.getWindowEnd()+61001L);
}
//定时器内容
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
	if (timestamp == ctx.getCurrentKey()+61001L){
		mapState.clear();
		return;}
...
  • 这里改用MapState,如若使用ListState,进来迟到数据后,则会出现同个url在同个窗口的统计出现多个计数的情况,列表状态不具备去重功能,故在这里使用map状态来实现去重。
  • 这里使用定时器来清除状态,原写法是在onTimer最后排序完直接清除状态,则会导致迟到数据到达后,原窗口其他数据被清除掉无法实现排名的输出,这里定时器的时间是在61001毫秒后清除状态数据。
  • 定时器61001毫秒 = 允许迟到数据1秒(forBoundedOutOfOrderness)+窗口迟到数据1分钟(allowedLateness)+第一个定时器1毫秒。

三、完整代码

package com.test.topN;

import bean.ApacheLog;
import bean.UrlCount;
import org.apache.commons.compress.utils.Lists;
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.functions.MapFunction;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple2;
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.WindowFunction;
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.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
/**
 * @author: Rango
 * @create: 2021-05-26 10:16
 * @description: 每隔5秒,输出最近10分钟内访问量最多的前N个URL
 **/
public class URLTopN3 {
    public static void main(String[] args) throws Exception {

        //1.建立环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);

        //2.读取端口数据并映射到JavaBean,并定义watermark时间语义
        WatermarkStrategy<ApacheLog> wms = WatermarkStrategy
                .<ApacheLog>forBoundedOutOfOrderness(Duration.ofSeconds(1))
                .withTimestampAssigner(new SerializableTimestampAssigner<ApacheLog>() {
                    @Override
                    public long extractTimestamp(ApacheLog element, long recordTimestamp) {
                        return element.getTs();
                    }});

        SingleOutputStreamOperator<ApacheLog> apacheLogDS = env.socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, ApacheLog>() {
                    @Override
                    public ApacheLog map(String value) throws Exception {
                        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy:HH:mm:ss");
                        String[] split = value.split(" ");
                        return new ApacheLog(split[0],
                                split[2],
                                sdf.parse(split[3]).getTime(),
                                split[5],
                                split[6]);
                    }})
                .assignTimestampsAndWatermarks(wms);

        //3.第一次聚合,按url转为tuple2分组,开窗,增量聚合
        SingleOutputStreamOperator<UrlCount> aggregateDS = apacheLogDS
                .map(new MapFunction<ApacheLog, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(ApacheLog value) throws Exception {
                return new Tuple2<>(value.getUrl(), 1);
            }}).keyBy(data -> data.f0)
                .window(SlidingEventTimeWindows.of(Time.minutes(10),Time.seconds(5)))
                .allowedLateness(Time.minutes(1))
                .aggregate(new HotUrlAggFunc(), new HotUrlWindowFunc());

        //4.第二次聚合,对第一次聚合输出按窗口分组,再全窗口聚合,建立定时器你,每5秒钟触发一次
        SingleOutputStreamOperator<String> processDS = aggregateDS
                .keyBy(data -> data.getWindowEnd())
                .process(new HotUrlProcessFunc(5));

        processDS.print();
        env.execute();
    }
    //实现AggregateFunction类中的方法
    public static class HotUrlAggFunc implements AggregateFunction<Tuple2<String, Integer>,Integer,Integer>{
        @Override
        public Integer createAccumulator() {return 0;}
        @Override
        public Integer add(Tuple2<String, Integer> value, Integer accumulator) { return accumulator+1;}
        @Override
        public Integer getResult(Integer accumulator) {return accumulator;}
        @Override
        public Integer merge(Integer a, Integer b) {return a+b; }
    }
    //实现窗口函数的apply方法,把累加函数输出的整数结果,转换为javabean类urlcount来做输出,方便后续按窗口聚合
    public static class HotUrlWindowFunc implements WindowFunction<Integer, UrlCount,String, TimeWindow> {
        @Override
        public void apply(String urls, TimeWindow window, Iterable<Integer> input, Collector<UrlCount> out) throws Exception {
            //获取按key相加后的次数并新建javabean(urlcount)作为返回
            Integer count = input.iterator().next();
            out.collect(new UrlCount(urls,window.getEnd(),count));
        }
    }
    //继承KeyedProcessFunction方法,重写processElemnt方法
    public static class HotUrlProcessFunc extends KeyedProcessFunction<Long,UrlCount,String>{
        //定义TopN为入参
        private Integer TopN;
        public HotUrlProcessFunc(Integer topN) {
            TopN = topN;
        }
        //定义状态
        private MapState <String,UrlCount>mapState;
        //open方法中初始化状态
        @Override
        public void open(Configuration parameters) throws Exception {
            mapState = getRuntimeContext()
                    .getMapState(new MapStateDescriptor<String, UrlCount>("map-state",String.class,UrlCount.class));
        }
        @Override
        public void processElement(UrlCount value, Context ctx, Collector<String> out) throws Exception {
            //状态装入数据
            mapState.put(value.getUrl(), value);
            //定时器,窗口一秒后触发
            ctx.timerService().registerEventTimeTimer(value.getWindowEnd()+1L);
            //再加一个定时器来清除状态用,在窗口关闭后再清除状态,这样延迟数据到达后窗口还能做排序
            ctx.timerService().registerEventTimeTimer(value.getWindowEnd()+61001L);
        }
        //定时器内容
        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
            if (timestamp == ctx.getCurrentKey()+61001L){
                mapState.clear();
                return;}

            //取出状态数据
            Iterator<Map.Entry<String, UrlCount>> iterator = mapState.iterator();
            ArrayList<Map.Entry<String, UrlCount>> entries = Lists.newArrayList(iterator);

            //排序
            entries.sort(((o1, o2) -> o2.getValue().getCount()-o1.getValue().getCount()));

            //排序后装入StringBulider作为输出TopN
            StringBuilder sb = new StringBuilder();
            sb.append("==============")
                    .append(new Timestamp(timestamp - 1L))
                    .append("==============")
                    .append("\n");
            for (int i = 0; i < Math.min(TopN,entries.size()); i++) {
                UrlCount urlCount = entries.get(i).getValue();
                sb.append("Top").append(i+1);
                sb.append(" Url:").append(urlCount.getUrl());
                sb.append(" Counts:").append(urlCount.getCount());
                sb.append("\n");
            }
            sb.append("==============")
                    .append(new Timestamp(timestamp - 1L))
                    .append("==============")
                    .append("\n")
                    .append("\n");

            out.collect(sb.toString());
            Thread.sleep(200);
            }}}
           

映射数据源的JavaBean

package bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApacheLog {
    private String ip;
    private String userId;
    private Long ts;
    private String method;
    private String url;
}

第一次聚合输出的JavaBean

package bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UrlCount {
    private String url;
    private Long windowEnd;
    private Integer count;
}

学习交流,有任何问题还请随时评论指出交流。

<think>好的,我现在要解决用户关于Flink如何实现TopN计算的问题。首先,我需要回忆一下自己了解的Flink相关知识,尤其是窗口操作和状态管理方面的内容。 用户的问题是“如何实现一个TopN计算”,这通常涉及到实时流处理中统计某个时间段内排名前N的数据。比如,每5分钟统计点击量最高的前10个商品。Flink作为流处理框架,处理这种需求应该是可行的,但具体步骤可能需要注意一些细节。 首先,TopN实现可能需要以下几个步骤: 1. **数据源**:数据应该是以流的形式进入Flink,比如Kafka中的消息。 2. **按Key分组**:比如按商品ID分组,统计每个商品的点击量。 3. **窗口划分**:确定统计的时间范围,比如滚动窗口或滑动窗口。 4. **聚合计算**:对每个窗口内的数据进行聚合,得到每个商品的点击总数。 5. **求TopN**:在每个窗口结束时,对所有商品的聚合结果进行排序,取前N名。 接下来要考虑的是,如何在窗口触发后收集所有数据并进行排序。因为Flink是分布式的,每个窗口的数据可能分布在不同的并行任务中。这时候可能需要使用全窗口函数,比如ProcessWindowFunction,来收集所有数据再进行排序。 另外,状态管理也是一个关键点。如果窗口比较大或者数据量多,直接收集所有数据可能会影响性能。有没有优化方法呢?比如使用增量聚合,结合AggregateFunction和ProcessWindowFunction,先做部分聚合,再全量处理。 然后,代码的大致结构应该是怎样的?比如,先keyBy,然后开窗口,接着用aggregate或process处理。在ProcessWindowFunction中,收集所有元素的计数,然后排序,取前N。 可能的示例步骤: - 数据源:DataStream<Event> - 转换:keyBy(Event::getItemId),然后开窗口 - 聚合使用sum或者自定义聚合函数统计每个商品的点击量 - 窗口函数处理:将所有商品的计数收集起来,排序,取TopN 但这里可能会有问题,比如,如果直接在每个窗口内对所有数据收集,当数据量很大时,内存和性能会有压力。这时候可能需要优化,比如使用分层排序或者近似算法,但用户的问题可能更关注基础实现。 另外,需要考虑的是,当窗口触发时,如何确保所有同一窗口的数据都被正确处理。Flink的窗口机制本身会处理这个问题,保证在窗口结束时触发计算。 可能还需要注意时间类型,比如事件时间还是处理时间。如果使用事件时间,需要正确处理水印和延迟数据。 现在,我应该把这些思考整理成一个步骤清晰的解释,并给出示例代码结构。同时,指出潜在的性能问题和优化方法,比如使用增量聚合减少状态大小,或者使用KeyedProcessFunction结合定时器来处理。 最后,总结实现步骤,强调关键点:分组、窗口、聚合排序TopN,以及可能的优化方式。</think>Flink 实现 TopN 计算的典型步骤如下(以统计**每5分钟商品点击量 Top10**为例): --- ### **1. 数据分组 + 窗口划分** ```java DataStream<Event> input = ...; // 输入流(假设已定义Event对象) DataStream<ItemViewCount> windowedData = input .keyBy(Event::getItemId) // 按商品ID分组 .window(TumblingEventTimeWindows.of(Time.minutes(5))) // 5分钟滚动窗口 .aggregate(new CountAgg(), new WindowResult()); // 聚合计算单商品点击量 ``` - **`CountAgg`**:增量聚合函数(如 `ReduceFunction`),实时累加单个商品的点击量 - **`WindowResult`**:包装窗口结束时的结果(含商品ID、窗口结束时间、总点击量) --- ### **2. 按窗口分组 + 全量排序** ```java DataStream<String> result = windowedData .keyBy(ItemViewCount::getWindowEnd) // 按窗口时间分组(相同窗口的数据进入同一分区) .process(new TopNHotItems(10)); // 处理并输出TopN ``` - **关键点**:将同一窗口的所有商品数据分配到同一个算子实例中处理 --- ### **3. 核心排序逻辑(ProcessFunction 示例)** ```java public class TopNHotItems extends KeyedProcessFunction<Long, ItemViewCount, String> { private final int topSize; private ListState<ItemViewCount> itemState; // 存储当前窗口所有商品数据 @Override public void processElement(ItemViewCount value, Context ctx, Collector<String> out) { itemState.add(value); // 将数据存入状态 ctx.timerService().registerEventTimeTimer(value.getWindowEnd() + 1); // 注册定时器 } @Override public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) { List<ItemViewCount> allItems = new ArrayList<>(); itemState.get().forEach(allItems::add); // 从状态中取出所有数据 allItems.sort((a, b) -> Long.compare(b.getViewCount(), a.getViewCount())); // 降序排序 StringBuilder result = new StringBuilder("Top10 商品:\n"); for (int i = 0; i < Math.min(topSize, allItems.size()); i++) { result.append("No.").append(i+1).append(": ").append(allItems.get(i)).append("\n"); } out.collect(result.toString()); itemState.clear(); // 清理状态 } } ``` --- ### **优化方向** 1. **增量聚合**:在窗口计算使用 `AggregateFunction` + `ProcessWindowFunction` 组合,减少全量数据存储 2. **状态清理**:及时清除过期窗口的状态,避免内存泄漏 3. **旁路输出**:使用 `sideOutput` 处理迟到数据,更新TopN结果 4. **分层排序**:对海量数据采用分桶排序等优化算法 --- ### **典型应用场景** - 实时热搜榜 - 直播间人气排名 - 电商实时销量排行榜 - 网络流量监控(异常IP识别) 代码实现需根据实际数据特征调整时间语义(事件时间/处理时间)、窗口类型(滚动/滑动/会话)及状态后端配置。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值