Flink基础之Transformation API详解及代码实现:从map到connect

摘要

讲透 Flink Transformation API 的完整体系:基础转换(map/flatMap/filter)、重分区(keyBy/rebalance/broadcast 等六种策略)、多流操作(union/connect/join 的选型差异)、分组聚合,附 7 个可直接运行的实战代码片段、重分区对性能的影响与 6 个真实踩坑点。

关键词

Flink、Transformation、map、flatMap、keyBy、rebalance、broadcast、union、connect、join、重分区、算子链


上一篇讲完了 Source------数据怎么进来。这一篇讲数据进来之后怎么处理 ,也就是 Transformation,它是 Flink 作业的「主战场」:你写的业务逻辑 90% 都落在 map、keyBy、window 这些转换算子组成的链条上。

但多数人对 Transformation 的理解停留在「会用几个算子」。这篇把它们分成四类讲透,重点放在三类容易被误解的地方:重分区 (数据怎么流)、多流操作 (几股流怎么合并)、算子链(性能为什么差这么多),每个知识点配可运行代码。


一、Transformation 分类全景

Flink 的转换算子按职责分四类:

  1. 基础转换 :map、flatMap、filter------单流内逐条处理,无状态,最常见。
  2. 重分区 :keyBy、rebalance、broadcast、global、shuffle、rescale------改变数据流向。
  3. 多流操作 :union、connect、join、coGroup------多股流合并或关联。
  4. 分组聚合 :keyBy + sum/reduce、window + aggregate------跨记录累积(有状态)。

选型顺序一句话:单条处理用基础转换,跨记录累积先 keyBy,多流合并先想 union 够不够,要关联才 join。


二、实战一:基础转换------map / flatMap / filter

三个最常用的算子,覆盖 80% 的数据清洗逻辑:

java 复制代码
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

public class BasicTransformDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // 输入:每行 "orderId,amount,category"
        DataStream<String> lines = env.socketTextStream("localhost", 9999);

        // map:1 进 1 出 ------ 解析 + 转换字段
        DataStream<String> mapped = lines
            .map(line -> {
                String[] p = line.split(",");
                return "order=" + p[0] + " cat=" + p[2];
            });

        // flatMap:1 进 0..N 出 ------ 把一行拆成多个词
        DataStream<String> words = lines
            .flatMap((String line, Collector<String> out) -> {
                for (String w : line.split(",")) {
                    out.collect(w.trim());
                }
            })
            .returns(String.class);  // flatMap 的 lambda 需要显式指定返回类型

        // filter:按条件保留
        DataStream<String> expensive = lines
            .filter(line -> Double.parseDouble(line.split(",")[1]) > 100);

        mapped.print("mapped");
        words.print("words");
        expensive.print("expensive");

        env.execute("basic-transform-demo");
    }
}

三个要点:

  1. flatMap 的 lambda 要配 .returns(TypeInformation):因为 lambda 无法推断泛型类型,编译期不报错、运行期才炸------这是 flatMap 最常见的坑。
  2. map 可以返回不同类型 :输入 String 输出 Tuple2 都行,map 会做类型推断。
  3. 这三个算子都是无状态的:输出只依赖当前输入,可以随意重启重放,性能开销最小。

三、实战二:keyBy 分组聚合

跨记录累积的第一步是 keyBy------它把相同 key 的数据路由到同一个子任务,之后的 sum/reduce 才能正确累积:

java 复制代码
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class KeyByAggDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // 输入:每行 "category,amount"
        DataStream<String> lines = env.socketTextStream("localhost", 9999);

        DataStream<Tuple3<String, Double, Integer>> parsed = lines
            .map(line -> {
                String[] p = line.split(",");
                return Tuple3.of(p[0], Double.parseDouble(p[1]), 1);
            });

        // keyBy 按分类分组 → sum 对第 1 列(amount)增量求和
        parsed.keyBy(t -> t.f0)
              .sum(1)
              .print("category-sum");

        // reduce:自定义归约 ------ 求每个分类的累计金额和笔数
        parsed.keyBy(t -> t.f0)
              .reduce((a, b) -> {
                  a.f1 = a.f1 + b.f1;   // 累计金额
                  a.f2 = a.f2 + b.f2;   // 累计笔数
                  return a;             // 返回合并结果,继续参与下次 reduce
              })
              .print("category-agg");

        env.execute("keyby-agg-demo");
    }
}

关键认知:reduce 的输入输出必须是同一类型 (这里都是 Tuple3),因为它拿上一条的聚合结果继续和当前记录合并。要「输入类型 → 不同输出类型」的聚合(比如累加后算平均值),用 AggregatingState 或窗口 aggregate。


四、重分区机制:六种分区策略

数据在算子之间怎么流动,由重分区算子决定。这是性能调优最容易忽略的一环:

算子 规则 典型场景
keyBy hash(key) % 并行度 聚合/窗口的前提(语义必需)
rebalance 轮询均分到所有下游 上游数据倾斜时负载均衡
broadcast 每条复制到所有下游 维表/规则广播(数据量必须小)
global 全部压到第 1 个实例 全局聚合(单点,慎用)
shuffle 随机分配 测试
rescale 本地就近均衡,不跨节点 同 TM 内均衡,开销最小

最容易被忽略的性能原则 :keyBy、rebalance、broadcast 都会打断算子链、触发网络 shuffle。能链在一起就别重分区------这是 Flink 性能的第一原则。


五、实战三:rebalance 与 broadcast

java 复制代码
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class PartitionDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<String> lines = env.socketTextStream("localhost", 9999);

        // rebalance:轮询发到下游所有实例,缓解上游倾斜
        DataStream<String> balanced = lines.rebalance();

        // broadcast:每条复制到所有下游实例(配合广播状态做规则下发)
        DataStream<String> broadcasted = lines.broadcast();

        // rescale:只在本地(同 TM)实例间均衡,不触发跨节点网络传输
        DataStream<String> rescaled = lines.rescale();

        balanced.print("balanced");
        broadcasted.print("broadcasted");
        rescaled.print("rescaled");

        env.execute("partition-demo");
    }
}

选型判断 :数据倾斜导致某个子任务 CPU 打满、其他空闲 → 用 rebalance(代价是全网络 shuffle);如果倾斜发生在单机内部(比如并行度小、单 TM 多 slot),rescale 更划算------它只在本地均衡,不产生跨节点网络开销。


六、多流操作:union / connect / join

三股流的处理方式差异极大,选错代价很高:

6.1 union:纯合并(最轻量)

java 复制代码
// 要求:两条流类型必须完全一致
DataStream<String> streamA = env.fromElements("a1", "a2");
DataStream<String> streamB = env.fromElements("b1", "b2");
DataStream<String> merged = streamA.union(streamB);  // 无状态纯拼接

6.2 connect:双流连接(类型可不同,可共享状态)

java 复制代码
import org.apache.flink.streaming.api.functions.co.CoFlatMapFunction;
import org.apache.flink.util.Collector;

// 规则流(String)+ 业务流(String),类型可以不同
// coFlatMap 里可以分别处理两条流的数据
DataStream<String> rules = env.fromElements("rule:amount>100", "rule:cat=book");
DataStream<String> events = env.socketTextStream("localhost", 9999);

DataStream<String> result = events.connect(rules)
    .flatMap(new CoFlatMapFunction<String, String, String>() {
        private String currentRule = "";

        @Override
        public void flatMap1(String event, Collector<String> out) {
            // 处理业务流:用当前规则判断
            if (currentRule.isEmpty() || event.contains(currentRule.split(":")[1])) {
                out.collect("PASS: " + event);
            } else {
                out.collect("REJECT: " + event);
            }
        }

        @Override
        public void flatMap2(String rule, Collector<String> out) {
            // 处理规则流:更新当前规则
            currentRule = rule;
        }
    });

result.print();

connect 是规则引擎的经典写法 :flatMap2 更新规则(可用 RichCoFlatMap 存到广播状态),flatMap1 用规则处理业务数据,规则变更实时生效、无需重启作业。

6.3 join:窗口关联(代价最高)

java 复制代码
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;

// 订单流 + 支付流水流,按订单 ID 关联
// join 必须先 keyBy + window:窗口内缓存双方数据,按 key 配对输出
// orderStream.join(payStream)
//     .where(order -> order.orderId)
//     .equalTo(pay -> pay.orderId)
//     .window(TumblingEventTimeWindows.of(Time.minutes(5)))
//     .apply((order, pay) -> order + "|" + pay);

join 的代价 :窗口内要为两条流分别缓存状态,key 多、窗口长时内存压力大;且 keyBy 必然打断算子链。能不用 join 就别用------很多场景用 connect + 定时器 或维表关联可以替代。


七、算子链与性能

为什么前面反复提「打断算子链」?因为算子链是 Flink 性能的核心机制 :满足条件的相邻算子(相同并行度 + 数据 one-to-one 传播)会合并成一个 Task,在同一线程内串行执行,链内数据传递零序列化、零网络开销。

  • map → filter → map(并行度相同)→ 合成一个 Task,一条链跑完;
  • 中间插一个 keyBy → 链被打断,前后各成一条链,中间网络 shuffle。

实战原则:

  1. 重分区算子能少用就少用,它直接打断链、引入网络开销;
  2. 想验证链效果:Web UI 看作业图,链内算子在同一个方块里;
  3. 排查性能问题时,先看数据是不是在算子之间「过网」了------如果一条链被莫名打断,多半是中间有 keyBy/rebalance 或者并行度不一致。

八、六个真实踩坑

  1. flatMap 的 lambda 不写 .returns() 。编译期正常、运行期报 TypeExtractionException------泛型被 lambda 抹掉了。凡是 lambda 实现 flatMap/process 这类泛型函数,都要显式 .returns(...)。
  2. reduce 的输入输出类型不一致 。reduce 要求输入输出同类型;需要变换类型的聚合用窗口 aggregate 或 AggregatingState。
  3. union 两边类型不一致 。union 要求类型完全一致,否则编译报错;类型不同但想合并,先 map 成同一类型或用 connect。
  4. join 忘了 keyBy/窗口 。窗口 join 必须先 keyBy(where/equalTo)再 window,缺一步直接编译失败。另外窗口 join 是内连接 ,配不上的数据不会输出------需要全量输出用 coGroup。
  5. broadcast 放大数据量 N 倍。广播到并行度 N 的下游 = 每条数据复制 N 份。用广播传大表会让网络直接打爆;广播只适合小维表/规则。
  6. global 造成单点瓶颈 。global() 把所有数据压到第一个实例,并行度形同虚设。全局聚合的正确姿势是 keyBy("常量key") 或明确知道数据量小。

Transformation 是 Flink 作业的中枢,它决定了数据怎么被加工、怎么流动、怎么合并。把「基础转换无状态最轻」「重分区打断链要慎用」「多流操作 union 最轻、join 最重」「有状态聚合必须先 keyBy」这四句话记住,再配合 Web UI 观察实际的数据流图,Transformation 的调优就不再是玄学。

直接打爆;广播只适合小维表/规则。

  1. global 造成单点瓶颈 。global() 把所有数据压到第一个实例,并行度形同虚设。全局聚合的正确姿势是 keyBy("常量key") 或明确知道数据量小。
相关推荐
动恰客流统计2 小时前
景区客流统计怎么做?兼顾管控与运营的实施方案解析
大数据·前端·人工智能
weixin_443883012 小时前
合规整改倒计时:高等级签名证书的应用场景
大数据·人工智能·法大大·法大大电子签·电子合同
dozenyaoyida3 小时前
AI与大模型新闻日报 | 2026-09-30
大数据·人工智能·大模型·新闻
辻弋2014 小时前
【无标题】
大数据·服务器·前端·搜索引擎·开源软件
衡石科技4 小时前
让问数更自然:衡石 Data Agent 实践
大数据·bi·数据权限·data agent·自然语言问数
实验室管理云平台4 小时前
应用盛元广通的SPF系统后,养殖场死亡率降低约20%
大数据·人工智能
火山引擎和TA的超级拍档们7 小时前
产品情报局 04|客服Agent:大模型时代的智能客服新范式
大数据·人工智能
鲸能云7 小时前
【AI Agent】光储运维从 “展示数据“ 到 “自主决策“:运维 AI Agent 落地实践拆解
大数据·人工智能·ai agent·智能运维·光伏储能
Elastic 中国社区官方博客7 小时前
Kubernetes attributes processor v1:它对 EDOT Collector 意味着什么
java·大数据·elasticsearch·搜索引擎·贪心算法·kubernetes·全文检索
2601_955662467 小时前
文本转语音(TTS)技术选型与工程实践
大数据·人工智能·音视频·语音识别·媒体