Flink DataStream API多流操作与实践

一、概览

scss 复制代码
多流操作 DataStream API
├─ union()                         同类型多流合并
├─ connect()                       两条不同类型流连接
│  ├─ CoMapFunction / CoFlatMapFunction
│  ├─ CoProcessFunction
│  └─ KeyedCoProcessFunction
├─ broadcast() + connect()         规则流 / 配置流广播
│  ├─ BroadcastProcessFunction
│  └─ KeyedBroadcastProcessFunction
├─ intervalJoin()                  KeyedStream 双流时间区间 Join
├─ join()                          窗口 Join
├─ coGroup()                       窗口 CoGroup,类似全量分组关联
└─ side output                     主流拆出多条旁路流,常用于告警/脏数据

二、union():同类型多流合并

适合多个来源结构完全一致的流,例如多个 Kafka topic、多个业务线的订单流。

ini 复制代码
DataStream<Order> appOrders = env.fromSource(appSource, wm, "app-orders");
DataStream<Order> webOrders = env.fromSource(webSource, wm, "web-orders");
DataStream<Order> posOrders = env.fromSource(posSource, wm, "pos-orders");

DataStream<Order> allOrders = appOrders
    .union(webOrders, posOrders);

特点:

  • 只能合并相同数据类型的流。
  • 不做去重、不做排序,只是把多条流合成一条流。
  • 多流 watermark 会取各输入流 watermark 的最小值,因此某一路延迟会拖慢整体事件时间推进。

最佳实践:

  • 多源合并前尽量统一 schema。
  • 如果多个源可能重复生产同一事件,union() 后要基于业务主键做去重。
  • 对空闲 source 配置 idleness,避免某个分区无数据导致 watermark 卡住。
scss 复制代码
WatermarkStrategy<Order> wm = WatermarkStrategy
    .<Order>forBoundedOutOfOrderness(Duration.ofSeconds(5))
    .withTimestampAssigner((e, ts) -> e.getEventTime())
    .withIdleness(Duration.ofMinutes(1));

三、connect():两条不同类型流连接处理

connect()可以连接两条不同类型的流,得到ConnectedStreams,再用双输入函数处理。

CoMapFunction / CoFlatMapFunction适合简单双流转换。

typescript 复制代码
DataStream<Order> orders = ...;
DataStream<UserEvent> userEvents = ...;

DataStream<String> result = orders
    .connect(userEvents)
    .flatMap(new CoFlatMapFunction<Order, UserEvent, String>() {
        @Override
        public void flatMap1(Order order, Collector<String> out) {
            out.collect("order: " + order.getOrderId());
        }

        @Override
        public void flatMap2(UserEvent event, Collector<String> out) {
            out.collect("event: " + event.getUserId());
        }
    });

其中flatMap1() 处理第一条流,flatMap2()处理第二条流,两条流类型可以不同,但输出类型相同。

CoProcessFunction 适合需要状态、定时器、复杂逻辑的双流处理,例如订单流关联支付流,订单先到则缓存,支付先到也缓存,超时则输出未支付订单。

scss 复制代码
DataStream<Order> orders = ...;
DataStream<Payment> payments = ...;

DataStream<Result> result = orders
    .keyBy(Order::getOrderId)
    .connect(payments.keyBy(Payment::getOrderId))
    .process(new KeyedCoProcessFunction<String, Order, Payment, Result>() {

        private ValueState<Order> orderState;
        private ValueState<Payment> paymentState;

        @Override
        public void open(Configuration parameters) {
            orderState = getRuntimeContext().getState(
                new ValueStateDescriptor<>("orderState", Order.class));

            paymentState = getRuntimeContext().getState(
                new ValueStateDescriptor<>("paymentState", Payment.class));
        }

        @Override
        public void processElement1(
                Order order,
                Context ctx,
                Collector<Result> out) throws Exception {

            Payment payment = paymentState.value();

            if (payment != null) {
                out.collect(Result.matched(order, payment));
                paymentState.clear();
            } else {
                orderState.update(order);
                ctx.timerService().registerEventTimeTimer(
                    order.getEventTime() + 15 * 60 * 1000L);
            }
        }

        @Override
        public void processElement2(
                Payment payment,
                Context ctx,
                Collector<Result> out) throws Exception {

            Order order = orderState.value();

            if (order != null) {
                out.collect(Result.matched(order, payment));
                orderState.clear();
            } else {
                paymentState.update(payment);
            }
        }

        @Override
        public void onTimer(
                long timestamp,
                OnTimerContext ctx,
                Collector<Result> out) throws Exception {

            Order order = orderState.value();
            if (order != null) {
                out.collect(Result.timeout(order));
                orderState.clear();
            }
        }
    });

最佳实践:

  • 双流关联优先使用 keyBy(...).connect(...).process(...)。
  • 两条流必须按相同业务 key 分区,否则同一个订单和支付可能到不同 Task。
  • 状态一定要清理,避免长期膨胀。
  • 有超时语义时,优先使用事件时间定时器。
  • 状态较大时开启 RocksDB StateBackend 和 TTL。

四、broadcast():广播流关联主流

适合"配置流 / 规则流 / 维表变更流"广播到所有并行实例。

典型场景:

  • 风控规则实时更新。
  • 活动配置实时生效。
  • 黑名单、白名单、阈值规则更新。
ini 复制代码
MapStateDescriptor<String, Rule> ruleStateDesc =
    new MapStateDescriptor<>("rules", String.class, Rule.class);

BroadcastStream<Rule> ruleBroadcastStream = rules.broadcast(ruleStateDesc);

DataStream<Alert> alerts = events
    .keyBy(Event::getUserId)
    .connect(ruleBroadcastStream)
    .process(new KeyedBroadcastProcessFunction<String, Event, Rule, Alert>() {

        @Override
        public void processElement(
                Event event,
                ReadOnlyContext ctx,
                Collector<Alert> out) throws Exception {

            ReadOnlyBroadcastState<String, Rule> rules =
                ctx.getBroadcastState(ruleStateDesc);

            for (Map.Entry<String, Rule> entry : rules.immutableEntries()) {
                Rule rule = entry.getValue();
                if (rule.match(event)) {
                    out.collect(new Alert(event, rule));
                }
            }
        }

        @Override
        public void processBroadcastElement(
                Rule rule,
                Context ctx,
                Collector<Alert> out) throws Exception {

            BroadcastState<String, Rule> state =
                ctx.getBroadcastState(ruleStateDesc);

            state.put(rule.getRuleId(), rule);
        }
    });

最佳实践:

  • 广播流数据量不能太大,因为每个并行实例都会保存一份。
  • 规则要有版本号、更新时间、启停状态。
  • 支持删除规则,不要只支持新增/覆盖。
  • 广播状态适合小规模动态配置,不适合大维表。
  • 大维表优先考虑异步 IO、外部 KV、Lookup Join 或 Table/SQL。

五、intervalJoin():双流时间区间 Join

适合两条 keyed 流按事件时间做区间关联。

例如:订单事件和支付事件,支付时间在订单后 15 分钟内即认为匹配。

sql 复制代码
DataStream<OrderPayment> result = orders
    .keyBy(Order::getOrderId)
    .intervalJoin(payments.keyBy(Payment::getOrderId))
    .between(Time.minutes(0), Time.minutes(15))
    .process(new ProcessJoinFunction<Order, Payment, OrderPayment>() {
        @Override
        public void processElement(
                Order order,
                Payment payment,
                Context ctx,
                Collector<OrderPayment> out) {
            out.collect(new OrderPayment(order, payment));
        }
    });

特点:

  • 只能用于 KeyedStream。
  • 基于事件时间。
  • 会根据时间区间缓存两边数据。
  • 不匹配的数据默认不会输出。

最佳实践:

  • 用于明确的时间窗口关联,比如"点击后 10 分钟内下单"。
  • 时间区间不要过大,否则状态会膨胀。
  • 要求两条流都有正确 watermark。
  • 如果需要输出未匹配数据,intervalJoin() 不够灵活,可以用 KeyedCoProcessFunction 自己实现。

六、join():窗口 Join

适合按窗口做内连接。

sql 复制代码
DataStream<OrderPayment> result = orders
    .join(payments)
    .where(Order::getOrderId)
    .equalTo(Payment::getOrderId)
    .window(TumblingEventTimeWindows.of(Time.minutes(5)))
    .apply(new JoinFunction<Order, Payment, OrderPayment>() {
        @Override
        public OrderPayment join(Order order, Payment payment) {
            return new OrderPayment(order, payment);
        }
    });

特点:

  • 两边在同一个窗口内且 key 相等时才会 join。
  • 本质是窗口内笛卡尔匹配。
  • 没有匹配的数据不会输出。

最佳实践:

  • 适合批式窗口关联,例如每 5 分钟内订单和支付匹配。
  • 如果关联语义是"订单后 15 分钟内支付",优先用 intervalJoin()。
  • 窗口 join 对迟到数据敏感,要配置 watermark 和 allowed lateness。

七、coGroup():窗口内双流分组处理

coGroup()比join()更灵活,可以拿到窗口内两边的全部元素。

css 复制代码
DataStream<Result> result = orders
    .coGroup(payments)
    .where(Order::getOrderId)
    .equalTo(Payment::getOrderId)
    .window(TumblingEventTimeWindows.of(Time.minutes(5)))
    .apply(new CoGroupFunction<Order, Payment, Result>() {
        @Override
        public void coGroup(
                Iterable<Order> orderGroup,
                Iterable<Payment> paymentGroup,
                Collector<Result> out) {

            List<Order> orderList = Lists.newArrayList(orderGroup);
            List<Payment> paymentList = Lists.newArrayList(paymentGroup);

            if (paymentList.isEmpty()) {
                for (Order order : orderList) {
                    out.collect(Result.unpaid(order));
                }
            } else {
                for (Order order : orderList) {
                    for (Payment payment : paymentList) {
                        out.collect(Result.matched(order, payment));
                    }
                }
            }
        }
    });

适合:

  • 需要处理未匹配数据。
  • 需要一对多、多对多关联。
  • 需要自定义窗口内关联逻辑。

八、Side Output:一条流拆出多条旁路流

虽然不是典型"多输入流",但常用于多流处理架构中,把主流拆成告警流、脏数据流、迟到数据流等。

ini 复制代码
OutputTag<DirtyRecord> dirtyTag = new OutputTag<DirtyRecord>("dirty") {};

SingleOutputStreamOperator<Event> cleanStream = rawStream
    .process(new ProcessFunction<RawEvent, Event>() {
        @Override
        public void processElement(
                RawEvent value,
                Context ctx,
                Collector<Event> out) {

            if (value.isValid()) {
                out.collect(value.toEvent());
            } else {
                ctx.output(dirtyTag, new DirtyRecord(value));
            }
        }
    });

DataStream<DirtyRecord> dirtyStream = cleanStream.getSideOutput(dirtyTag);

最佳实践:

  • 脏数据、异常数据不要直接丢弃,优先通过 side output 输出到旁路。
  • 迟到数据可以输出到独立 Kafka topic 供补偿处理。
  • 主流程和异常流程解耦,便于监控和回放。

九、选型建议

场景 推荐 API
多条同类型流合并 union()
两条不同类型流简单处理 connect() + CoMapFunction / CoFlatMapFunction
双流复杂状态关联 connect() + KeyedCoProcessFunction
主流关联动态规则流 broadcast() + KeyedBroadcastProcessFunction
两条 keyed 流按时间范围关联 intervalJoin()
两条流按窗口内 key 关联 join()
需要处理未匹配或复杂窗口分组 coGroup()
主流拆告警/脏数据/迟到数据 side output
相关推荐
大大大大晴天2 天前
深入浅出 Flink DataStream API:原理与使用
flink
大大大大晴天3 天前
聊聊Flink的几种血缘方案
前端·flink
大大大大晴天️4 天前
主流CDC 组件对比选型指南
大数据·flink·cdc
大大大大晴天5 天前
Flink CDC 深度解析:从原理到实践的全链路指南
大数据·flink
数据开发 Yang 同学8 天前
【Flink 时间语义与窗口】Watermark 机制、迟到数据处理与生产踩坑
flink
本旺11 天前
Flink 2.x状态演进:理解解 1.x 与 2.x 状态存储机制
大数据·flink
Apache Flink11 天前
从结构化到多模态:Apache Flink,多模态数据处理的流式底座
大数据·flink·apache
婉然从物11 天前
Flink SQL 元数据持久化实战:从“每次重启表消失”到“Hive Metastore 永久存储”
hive·sql·flink
@SmartSi12 天前
新一代实时数据集成框架 Flink CDC 3.0 架构解析
flink·cdc