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

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

一、背景说明:

在上篇文章实现了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
<think>好的,我现在需要帮助用户解决如何使用Flink实现TopN计算的问题。用户提到了示例代码或解决方案,所以我要确保回答中包含具体的实现方法和可能的代码示例。首先,我需要回忆一下Flink中处理TopN的常用方法,比如使用窗口、KeyedProcessFunction,或者Flink SQL。 根据用户提供的引用,特别是引用[2]中提到的Flink SQL实现TopN的注意事项,这可能是一个重要的点。另外,引用[1]中的代码示例是关于窗口处理的,可能可以结合使用。引用[3]提到Flink 2.0的状态管理和SQL算子的增强,这可能意味着SQL方法在较新版本中更高效。 用户可能需要两种方法:一种是基于DataStream API的,另一种是基于SQL的。我需要分别考虑这两种方法,并确保示例代码正确无误。对于DataStream API,通常的步骤是分组、开窗,然后使用ProcessWindowFunction或KeyedProcessFunction来收集数据排序TopN。需要注意的是状态管理和时间处理的问题。 对于SQL方法,用户需要编写正确的SQL查询,使用ROW_NUMBER()和PARTITION BY进行分组排序,然后在外层筛选TopN。引用[2]中提到内层计算的排行榜大小必须比外层大,这点很重要,需要在示例中说明,避免用户犯错。 同时,我需要检查是否有常见的陷阱,比如窗口未正确触发,或者状态没有及时清理,导致内存泄漏。另外,处理乱序事件可能需要设置允许的延迟时间,使用侧输出等机制。 还要考虑用户可能需要的是实时TopN,所以需要基于时间窗口或者滚动窗口来处理。示例代码需要简洁明了,关键部分加上注释,帮助用户理解。 最后,生成的相关问题需要围绕用户可能遇到的后续问题,比如性能优化、处理大数据量、状态管理等。确保问题覆盖用户可能的进一步需求。</think>以下是如何使用Apache Flink实现TopN计算的详细解决方案,包含两种常用方法: ### 一、基于DataStream API的实现 ```java DataStream<UserBehavior> input = ...; // 输入数据流 // 按商品ID分组,开5分钟滚动窗口 input.keyBy(UserBehavior::getItemId) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .process(new TopNHotItems(5)) // 处理函数 .print(); // 自定义窗口处理函数 public static class TopNHotItems extends ProcessWindowFunction<UserBehavior, Tuple2<String, Long>, String, TimeWindow> { private final int topSize; public TopNHotItems(int topSize) { this.topSize = topSize; } @Override public void process(String key, Context context, Iterable<UserBehavior> elements, Collector<Tuple2<String, Long>> out) { // 1.统计当前商品在窗口内的点击量 long count = StreamSupport.stream(elements.spliterator(), false).count(); // 2.将结果存入状态 ListState<Tuple2<String, Long>> state = context.globalState() .getListState(new ListStateDescriptor<>("item-count", Types.TUPLE(Types.STRING, Types.LONG))); state.add(Tuple2.of(key, count)); // 3.注册定时器,在窗口结束时触发排序 context.registerEventTimeTimer(context.window().getEnd()); } @Override public void onTimer(long timestamp, OnTimerContext ctx, Collector<Tuple2<String, Long>> out) { // 4.从状态获取所有商品点击量 List<Tuple2<String, Long>> allItems = new ArrayList<>(); for (Tuple2<String, Long> item : ctx.globalState().getListState( new ListStateDescriptor<>("item-count", Types.TUPLE(Types.STRING, Types.LONG)))) { allItems.add(item); } // 5.按点击量降序排序并取TopN allItems.sort((o1, o2) -> (int)(o2.f1 - o1.f1)); List<Tuple2<String, Long>> topItems = allItems.subList(0, Math.min(topSize, allItems.size())); // 6.输出结果 for (Tuple2<String, Long> item : topItems) { out.collect(item); } } } ``` 关键要点: 1. 使用窗口函数聚合统计量 2. 通过状态管理保存中间结果 3. 定时器触发最终排序逻辑 4. 支持事件时间处理乱序数据[^1] ### 二、基于Flink SQL的实现 ```sql -- 创建视图作为中间结果 CREATE VIEW item_counts AS SELECT item_id, COUNT(*) AS cnt, HOP_END(rowtime, INTERVAL '1' MINUTE, INTERVAL '5' MINUTE) AS window_end FROM user_behavior GROUP BY item_id, HOP(rowtime, INTERVAL '1' MINUTE, INTERVAL '5' MINUTE); -- 外层查询获取TopN SELECT * FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY window_end ORDER BY cnt DESC ) as row_num FROM item_counts ) WHERE row_num <= 50; ``` 注意事项: 1. 内层窗口大小应大于最终需要的TopN大小(示例使用滑动窗口确保数据完整性) 2. 使用HOP窗口(滑动窗口)避免数据遗漏[^2] 3. ROW_NUMBER()函数实现高效排序 4. 窗口时间对齐保证结果正确性 ### 三、性能优化建议 1. 状态后端选择:对于大规模状态存储,建议使用RocksDBStateBackend 2. 并行度设置:根据数据倾斜程度动态调整 3. 增量计算使用ReduceFunction优化窗口计算 4. 异步I/O:与外部系统交互时启用异步模式[^3]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值