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

一、背景说明:

有需求需要对数据进行统计,要求每隔5分钟输出最近1小时内点击量最多的前N个商品,数据格式预览如下:

543462,1715,1464116,pv,1511658000
662867,2244074,1575622,pv,1511658000
561558,3611281,965809,pv,1511658000
894923,3076029,1879194,pv,1511658000
834377,4541270,3738615,pv,1511658000
315321,942195,4339722,pv,1511658000
625915,1162383,570735,pv,1511658000
578814,176722,982926,pv,1511658000
....

最后统计输出结果如下:

==============2017-11-26 09:05:00.0==============
Top1 ItemId:5051027 Counts:3
Top2 ItemId:3493253 Counts:3
Top3 ItemId:4261030 Counts:3
Top4 ItemId:4894670 Counts:2
Top5 ItemId:3781391 Counts:2
==============2017-11-26 09:05:00.0==============

==============2017-11-26 09:10:00.0==============
Top1 ItemId:812879 Counts:5
Top2 ItemId:2600165 Counts:4
Top3 ItemId:2828948 Counts:4
Top4 ItemId:2338453 Counts:4
Top5 ItemId:4261030 Counts:4
==============2017-11-26 09:10:00.0==============

二、实现过程

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

  2. 代码细节说明:
    2.1 、第一次聚合代码:

//第一次聚合
SingleOutputStreamOperator<ItemCount> aggregateDS = userBehaviorDS
		.map(new MapFunction<UserBehavior, Tuple2<Long, Integer>>() {
			@Override
			public Tuple2<Long, Integer> map(UserBehavior value) throws Exception {
				return new Tuple2<>(value.getItemId(), 1);
			}})
		.keyBy(data -> data.f0)
		.window(SlidingEventTimeWindows.of(Time.hours(1), Time.minutes(5)))
		.aggregate(new ItemCountAggFunc(), new ItemCountWindowFunc());

①第一次聚合这里将商品id进行提取并转换为Tuple2<id,1>的格式,再对id进行keyby后聚合,避免直接使用对应的JavaBean进行分组聚合提高效率:

②这里使用aggregate算子进行增量计算,Flink的window function来负责一旦窗口关闭, 去计算处理窗口中的每个元素。window function 是如下三种:

  • ReduceFunction (增量聚合函数) 输入及输出类型得一致
  • AggregateFunction(增量聚合函数)输入及输出类型可以不一致
  • ProcessWindowFunction(全窗口函数)

ReduceFunction,AggregateFunction更加高效, 原因就是Flink可以对到来的元素进行增量聚合 .
ProcessWindowFunction 可以得到一个包含这个窗口中所有元素的迭代器, 以及这些元素所属窗口的一些元数据信息.
ProcessWindowFunction不能被高效执行的原因是Flink在执行这个函数之前, 需要在内部缓存这个窗口上所有的元素

2.2、重写AggregateFunction函数代码

public static class ItemCountAggFunc implements AggregateFunction<Tuple2<Long,Integer>,Integer,Integer>{
	@Override
	public Integer createAccumulator() { return 0; }
	@Override
	public Integer add(Tuple2<Long, 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; }
}

这里对AggregateFunction函数里面四个方法进行重写自定义计数规则,入参<IN,ACC,OUT>对应为Tuple2,累加器用Integer过度,输出结果为Integer。

  • createAccumulator
    这个方法首先要创建一个累加器,要进行一些初始化的工作,这里初始值为0.
  • add
    add方法就是做聚合的时候的核心逻辑,这里这是对tuple的第二位整数进行累加。
  • merge
    Flink是一个分布式计算框架,可能计算是分布在很多节点上同时进行的,如果计算在多个节点进行,需要对结果进行合并,这个merge方法就是做这个工作的,所以入参和出参的类型都是中间结果类型ACC。
  • getResult
    这个方法就是将每个用户最后聚合的结果经过处理之后,按照OUT的类型返回,返回的结果也就是聚合函数的输出结果了。

这里也是AggregateFunction和ReduceFunction区别的地方,reduce的input为Tuple2,则output也必须是Tuple2。

三、完整代码

这里处理的是顺序数据,如果是乱序数据,在窗口触发计算后迟到数据统计会有问题,优化思路为在窗口关闭后再触发键控状态的清除,及使用MapState来避免同个产品Id多个结果的问题。

package com.test.topN;

import bean.ItemCount;
import bean.UserBehavior;
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.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
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;
/**
 * @author: Rango
 * @create: 2021-05-24 10:37
 * @description: 每隔5分钟输出最近1小时内点击量最多的前N个商品
 **/
public class ProductTopN {
    public static void main(String[] args) throws Exception {
        //1.建立环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);

        //2.设置watermark及定义事件时间,从socket获取数据并对应到JavaBean,筛选只取pv数据
        WatermarkStrategy<UserBehavior> wms = WatermarkStrategy
                .<UserBehavior>forBoundedOutOfOrderness(Duration.ofSeconds(1))
                .withTimestampAssigner(new SerializableTimestampAssigner<UserBehavior>() {
                    @Override
                    public long extractTimestamp(UserBehavior element, long recordTimestamp) {
                        return element.getTimestamp() * 1000L;
                    }
                });
        SingleOutputStreamOperator<UserBehavior> userBehaviorDS = env
                //.socketTextStream("hadoop102", 9999)
                .readTextFile("input/UserBehavior.csv")
                .map(new MapFunction<String, UserBehavior>() {
                    @Override
                    public UserBehavior map(String value) throws Exception {
                        String[] split = value.split(",");
                        return new UserBehavior(Long.parseLong(split[0]),
                                Long.parseLong(split[1]),
                                Integer.parseInt(split[2]),
                                split[3],
                                Long.parseLong(split[4]));
                    }
                })
                .filter(data -> "pv".equals(data.getBehavior()))
                .assignTimestampsAndWatermarks(wms);

        //3.第一次聚合,按商品id分组开窗聚合,使用aggregate进行增量计算,将商品id用tuple2抽离出来提高效率
        SingleOutputStreamOperator<ItemCount> aggregateDS = userBehaviorDS
                .map(new MapFunction<UserBehavior, Tuple2<Long, Integer>>() {
                    @Override
                    public Tuple2<Long, Integer> map(UserBehavior value) throws Exception {
                        return new Tuple2<>(value.getItemId(), 1);
                    }})
                .keyBy(data -> data.f0)
                .window(SlidingEventTimeWindows.of(Time.hours(1), Time.minutes(5)))
                .aggregate(new ItemCountAggFunc(), new ItemCountWindowFunc());

        //4.第二次聚合,按窗口聚合,基于状态编程实现窗口内有序
        SingleOutputStreamOperator<String> processDS = aggregateDS.keyBy(ItemCount::getTime)
                .process(new ItemCountProcessFunc(5));

        //5.打印结果并执行
        processDS.print();
        env.execute();
    }

    public static class ItemCountAggFunc implements AggregateFunction<Tuple2<Long,Integer>,Integer,Integer>{
        @Override
        public Integer createAccumulator() { return 0; }
        @Override
        public Integer add(Tuple2<Long, 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; }
    }
    public static class ItemCountWindowFunc implements WindowFunction<Integer, ItemCount,Long, TimeWindow>{
        @Override
        public void apply(Long key, TimeWindow window, Iterable<Integer> input, Collector<ItemCount> out) throws Exception {
            Integer next = input.iterator().next();
            out.collect(new ItemCount(key,new Timestamp(window.getEnd()).toString(),next));
        }
    }
    public static class ItemCountProcessFunc extends KeyedProcessFunction<String,ItemCount,String>{
        //定义构造器可以按入参取排名
        private Integer topN;
        public ItemCountProcessFunc(Integer topN) {
            this.topN = topN;
        }

        //使用liststatus并初始化
        private ListState <ItemCount>listState;
        @Override
        public void open(Configuration parameters) throws Exception {
            listState= getRuntimeContext()
                    .getListState(new ListStateDescriptor<ItemCount>("list-state",ItemCount.class));
        }
        //定时器
        @Override
        public void processElement(ItemCount value, Context ctx, Collector<String> out) throws Exception {
            listState.add(value);
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            ctx.timerService().registerEventTimeTimer(sdf.parse(value.getTime()).getTime()+1000L);
        }
        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
            //1.获取状态的数据并转为List
            Iterator<ItemCount> iterator = listState.get().iterator();
            ArrayList<ItemCount> itemCounts = Lists.newArrayList(iterator);

            //2.排序
            itemCounts.sort(((o1, o2) -> o2.getCount() - o1.getCount()));

            //3.获取前n
            StringBuilder sb = new StringBuilder();
            sb.append("==============")
                    .append(new Timestamp(timestamp - 1000L))
                    .append("==============")
                    .append("\n");
            for (int i = 0; i < Math.min(topN,itemCounts.size()); i++) {
                ItemCount itemCount = itemCounts.get(i);
                sb.append("Top").append(i+1);
                sb.append(" ItemId:").append(itemCount.getItem());
                sb.append(" Counts:").append(itemCount.getCount());
                sb.append("\n");
            }
            sb.append("==============")
                    .append(new Timestamp(timestamp - 1000L))
                    .append("==============")
                    .append("\n")
                    .append("\n");
            listState.clear();
            out.collect(sb.toString());
            Thread.sleep(200);//方便查看结果时间休眠
        }}}

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

<think>好的,我现在需要帮助用户了解Flink的时间窗口开窗。首先,用户之前问过如何设置队列最大长度,现在转向了Flink的时间窗口,可能是在处理实时数据流时需要窗口功能,比如统计某段时间的数据。我需要先确认用户的使用场景,可能是实时数据处理或流计算,但用户没有详细说明,所以需要覆盖基础概念和常见窗口类型。 接下来,用户可能对时间窗口的基本概念不太熟悉,所以需要从基础讲起,比如时间窗口的作用和类型。然后,用户可能想知道如何具体在Flink实现这些窗口,包括代码示例。需要分清楚处理时间、事件时间和摄入时间的区别,因为这是Flink中时间语义的重要部分。 用户可能还需要知道不同的窗口类型,比如滚动窗口、滑动窗口和会话窗口,以及它们的应用场景。需要提供每种窗口的定义、特点和使用方法,最好有代码片段帮助理解。此外,用户可能对窗口的生命周期和触发器感兴趣,但根据问题,可能暂时不需要深入这部分,但可以简要提及。 另外,用户之前的问题涉及队列长度设置,可能现在在处理流数据时遇到了窗口相关的性能或配置问题,所以需要强调窗口配置的注意事项,比如时间语义的选择、延迟数据处理和水位线生成。这部分内容能帮助用户正确配置窗口,避免常见错误。 需要检查是否有必要对比不同窗口类型的适用场景,帮助用户根据需求选择合适的窗口。同时,生产环境的建议,比如水位线策略和延迟数据处理,对用户的实际应用可能有很大帮助,需要详细说明。 最后,总结部分要简明扼要,强调重点,让用户能快速回顾核心内容。可能还需要提醒用户根据具体业务需求调整窗口参数,并考虑性能优化。要确保回答结构清晰,步骤明确,覆盖用户可能的需求点,同时保持语言简洁易懂。</think>以下是关于Flink时间窗口开窗的详细技术解析,包含核心概念、窗口类型及实战示例: --- ### 一、时间窗口核心原理 Flink通过**时间窗口(Time Windows)**将无界数据流划分为有限的数据块进行处理。其核心要素包含: 1. **时间语义**: - $处理时间(Processing Time)$:以系统时钟为基准(最快但不可靠) - $事件时间(Event Time)$:以数据自带时间戳为基准(需要水位线机制) - $摄入时间(Ingestion Time)$:以数据进入Flink的时间为基准 2. **窗口生命周期**: ```mermaid graph LR 创建窗口-->数据分配 数据分配-->触发计算 触发计算-->窗口销毁 ``` --- ### 二、基础窗口类型详解 #### 1. 滚动窗口(Tumbling Window) $$ \text{窗口长度}=T,\ \text{窗口间无重叠} $$ ```java // 事件时间滚动窗口(5分钟) DataStream<T> input = ...; input .keyBy(<key selector>) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .reduce(<ReduceFunction>); // 处理时间滚动窗口(10秒) .window(TumblingProcessingTimeWindows.of(Time.seconds(10))) ``` #### 2. 滑动窗口(Sliding Window) $$ \text{窗口长度}=T,\ \text{滑动步长}=S\ (S < T) $$ ```java // 事件时间滑动窗口(1小时窗口,15分钟滑动) .window(SlidingEventTimeWindows.of(Time.hours(1), Time.minutes(15))) // 带偏移量的滑动窗口(解决时区问题) .of(Time.hours(24), Time.hours(1), Time.hours(-8)) // UTC+8时区对齐 ``` #### 3. 会话窗口(Session Window) $$ \text{动态窗口,通过间隙(gap)分割} $$ ```java // 事件时间会话窗口(10分钟不活动则关闭) .window(EventTimeSessionWindows.withGap(Time.minutes(10))) // 动态间隙配置(根据数据特征调整) .withDynamicGap((element) -> { // 根据数据内容返回不同gap时长 return determineGap(element); }); ``` --- ### 三、高级窗口操作 #### 1. 全局窗口 + 触发器 ```java // 自定义每100个元素触发计算 .window(GlobalWindows.create()) .trigger(CountTrigger.of(100)) ``` #### 2. 延迟数据处理(Allowed Lateness) ```java .window(...) .allowedLateness(Time.minutes(5)) // 允许迟到5分钟 .sideOutputLateData(lateOutputTag) // 侧输出迟到数据 ``` #### 3. 窗口函数对比 | 函数类型 | 特点 | 适用场景 | |------------------|-----------------------------|----------------------| | ReduceFunction | 增量计算,输入输出类型一致 | 简单聚合(sum/max) | | AggregateFunction| 中间状态可维护复杂聚合 | 平均值、复杂统计 | | ProcessWindowFunction | 全量访问窗口数据 | 排序、TopN操作 | --- ### 四、水位线(Watermark)关键配置 ```java // 有序事件水位线(无乱序) .assignTimestampsAndWatermarks( WatermarkStrategy .<Event>forMonotonousTimestamps() .withTimestampAssigner((event, ts) -> event.timestamp) ); // 乱序事件水位线(最大允许延迟2秒) .assignTimestampsAndWatermarks( WatermarkStrategy .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2)) .withTimestampAssigner(...) ); ``` --- ### 五、生产环境最佳实践 1. **窗口长度选择公式**: $$ \text{窗口长度} = \frac{\text{业务周期}}{\text{精度要求系数}} $$ 示例:每分钟统计的日粒度数据: $$ \frac{24小时}{1440} = 1分钟 $$ 2. **内存控制**: ```java // 设置状态后端及TTL env.setStateBackend(new RocksDBStateBackend(...)); env.getConfig().setAutoWatermarkInterval(1000); // 1秒生成一次水位线 ``` 3. **性能调优参数**: ```shell # 调整Flink配置 taskmanager.memory.task.heap.size: 4096m taskmanager.numberOfTaskSlots: 4 parallelism.default: 8 ``` --- ### 六、典型问题排查指南 1. **窗口不触发**: - 检查水位线生成是否正常 - 验证事件时间是否远小于系统时间 - 使用`debug`模式输出水位线日志 2. **结果数据缺失**: ```java // 添加迟到数据侧输出 OutputTag<Event> lateDataTag = new OutputTag<>("late-data"){}; .sideOutputLateData(lateDataTag) ``` --- 通过合理选择窗口类型与参数配置,可以实现从秒级实时监控到天级离线统计的全场景覆盖。建议结合Flink Web UI的窗口监控指标进行动态调优。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值