Flink 滑动窗口实战:从 KeyedProcessFunction 到 AggregateFunction & WindowFunction 的完整旅程

一、业务背景

我们要在 Flink 实时流上统计 每个用户-品牌组合最近 1 小时的最晚行为时间,并且每 5 分钟更新一次结果。
数据来自 Kafka,事件类型为 CartEvent

public class CartEvent {
    public String userId;
    public String brandId;
    public long   ts;      // 事件时间戳
}

二、三种实现模式对比速查表

方案适用场景优点缺点
KeyedProcessFunction需要完全自己管理定时器、状态最灵活,可定制任何逻辑代码量大,易出错
AggregateFunction + WindowFunction(老 API)预聚合后再做最终加工性能好,API 简单只能拿到预聚合结果
Reduce/Aggregate + ProcessWindowFunction(新 API)预聚合后还能拿到窗口元数据兼具性能与信息相对复杂

三、KeyedProcessFunction 版本(最底层)

3.1 核心思想

  • MapState<String, Long> 保存每个用户-品牌的最晚时间。
  • 注册事件时间定时器,1 小时后触发输出。
  • 每来一条数据就更新状态并重新注册定时器(滑动要靠我们自己做)。

3.2 代码骨架

public class MaxTimeProcessFunc
        extends KeyedProcessFunction<String, CartEvent, Tuple3<String,String,Long>> {

    // 状态:userId_brandId -> maxTs
    private MapState<String, Long> maxTsState;

    @Override
    public void open(Configuration parameters) throws Exception {
        MapStateDescriptor<String, Long> desc =
                new MapStateDescriptor<>("maxTs", String.class, Long.class);
        maxTsState = getRuntimeContext().getMapState(desc);
    }

    @Override
    public void processElement(CartEvent value,
                               Context ctx,
                               Collector<Tuple3<String,String,Long>> out) throws Exception {
        String key = value.userId + "\001" + value.brandId;
        Long old = maxTsState.get(key);
        if (old == null || value.ts > old) {
            maxTsState.put(key, value.ts);
        }
        // 每 5 min 滑动 -> 注册“窗口结束时间”定时器
        long windowEnd = ctx.timestamp() - ctx.timestamp() % 300_000L + 300_000L;
        ctx.timerService().registerEventTimeTimer(windowEnd);
    }

    @Override
    public void onTimer(long timestamp,
                        OnTimerContext ctx,
                        Collector<Tuple3<String,String,Long>> out) throws Exception {
        for (Map.Entry<String, Long> e : maxTsState.entries()) {
            String[] arr = e.getKey().split("\001");
            out.collect(Tuple3.of(arr[0], arr[1], e.getValue()));
        }
        // 可选:清理 1h 之前的状态
    }
}

缺点:得自己算窗口边界、清理过期状态,容易踩坑。


四、滑动窗口 + AggregateFunction(推荐)

4.1 需求映射

  • 窗口长度 = 1 小时
  • 滑动间隔 = 5 分钟

4.2 AggregateFunction 签名解析

public class MaxTimeAgg
        implements AggregateFunction<CartEvent, Long, Long> {

    @Override
    public Long createAccumulator() {
        return Long.MIN_VALUE;         // 初始极小值
    }

    @Override
    public Long add(CartEvent value, Long acc) {
        return Math.max(acc, value.ts); // 每条数据进来取最大
    }

    @Override
    public Long getResult(Long acc) {
        return acc;                    // 窗口触发时返回最大时间
    }

    @Override
    public Long merge(Long acc1, Long acc2) {
        return Math.max(acc1, acc2);   // 用于会话窗口或多线程合并
    }
}
泛型含义
CartEventIN:输入事件
LongACC:累加器,只保存当前最大值
LongOUT:最终输出

注意:AggregateFunction 只能拿到 窗口内聚合后的结果拿不到窗口元数据(起止时间、key 等),所以需要再配合 WindowFunction。


五、AggregateFunction + WindowFunction 组合

5.1 主流程

DataStream<CartEvent> source = env.addSource(...);

source
    .keyBy(e -> e.userId + "\001" + e.brandId)
    .window(SlidingProcessingTimeWindows.of(Time.hours(1), Time.minutes(5)))
    .aggregate(new MaxTimeAgg(), new WindowEndFunc())
    .print();

5.2 WindowFunction 把窗口信息补回来

public class WindowEndFunc
        implements WindowFunction<Long, WindowResult, String, TimeWindow> {

    @Override
    public void apply(String key,
                      TimeWindow window,
                      Iterable<Long> maxTsIt,
                      Collector<WindowResult> out) {
        Long maxTs = maxTsIt.iterator().next();  // 只有一条
        String[] arr = key.split("\001");
        out.collect(new WindowResult(arr[0], arr[1], maxTs,
                                     "brand", "T"));
    }
}

5.3 WindowResult POJO

public class WindowResult {
    public String userId;
    public String value;   // brandId
    public long   time;    // 最晚行为时间
    public String dimType; // 维度类型
    public String list;    // 附加字段

    public WindowResult() {}
    public WindowResult(String userId, String value,
                        long time, String dimType, String list) {
        this.userId  = userId;
        this.value   = value;
        this.time    = time;
        this.dimType = dimType;
        this.list    = list;
    }

    @Override
    public String toString() {
        return "WindowResult{" +
               "userId='" + userId + '\'' +
               ", value='" + value + '\'' +
               ", time=" + time +
               ", dimType='" + dimType + '\'' +
               ", list='" + list + '\'' +
               '}';
    }
}

六、时间与输出节奏

现象解释
作业启动后 1 小时内 看不到任何结果必须等第一条窗口完整闭合(1 小时)。
之后 每 5 分钟 都会输出一次滑动窗口每滑动一格(5 min)触发一次。

七、调优 & 踩坑小结

  1. 状态 TTL
    滑动窗口会产生大量过期窗口,建议设置 StateTtlConfig 清理 1.5 小时前的状态。

  2. 并行度
    如果 key 量大,可以把 userId_brandId 做二次分区,避免热点 key。

  3. 迟到数据
    允许迟到 1 分钟:allowedLateness(Time.minutes(1))

  4. 空闲数据源
    使用 WatermarkStrategy.withIdleness() 防止空闲分区不触发窗口。


八、一句话总结

  • 想最省事 —— 直接 aggregate(new MaxTimeAgg(), new WindowEndFunc())
  • 想最灵活 —— 用 KeyedProcessFunction 自己管定时器和状态
  • 想兼顾性能与信息 —— 用 AggregateFunction + ProcessWindowFunction(新 API)

至此,我们已经把 Flink 滑动窗口的三种写法、时间语义、状态管理和调优思路全部串了一遍。祝实战愉快!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

AlgoCraft

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

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

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

打赏作者

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

抵扣说明:

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

余额充值