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% 都落在 mapkeyBywindow 这些转换算子组成的链条上。

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


一、Transformation 分类全景

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

  1. 基础转换mapflatMapfilter------单流内逐条处理,无状态,最常见。
  2. 重分区keyByrebalancebroadcastglobalshufflerescale------改变数据流向。
  3. 多流操作unionconnectjoincoGroup------多股流合并或关联。
  4. 分组聚合keyBy + sum/reducewindow + 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 内均衡,开销最小

最容易被忽略的性能原则keyByrebalancebroadcast 都会打断算子链、触发网络 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 要求输入输出同类型;需要变换类型的聚合用窗口 aggregateAggregatingState
  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") 或明确知道数据量小。
相关推荐
TDengine (老段)2 小时前
TDgpt 使用 — 部署、SQL、算法
大数据·数据库·sql·算法·时序数据库·tdengine·涛思数据
字节跳动数据平台2 小时前
PDF 文档解析算子全新升级!不仅“读得懂”,更让解析结果直接可用
大数据
爱吃火鸡面呀2 小时前
HDFS 文件读取流程深度解析:从客户端请求到数据落地的完整链路
大数据·hadoop·hdfs
回忆2012初秋2 小时前
DBX:一个现代化的轻量级、跨平台的开源数据库管理工具
大数据·服务器·网络
YangYang9YangYan2 小时前
校招视角|电商运营岗位项目、工具、复盘完整备考指南
大数据
GIS数据转换器2 小时前
遥感GIS一体化技术应用平台
大数据·人工智能·python·安全·数据挖掘
AI职业加油站3 小时前
2026大数据运维行业趋势复盘:大数据运维工程师赋能发展
大数据·运维·人工智能·学习·数据分析·职场发展
GlobalInfo3 小时前
34.2%年复合增长率背后,AI量子计算市场规模影响因素会是什么?
大数据·人工智能·量子计算
kaoa0003 小时前
Linux入门攻坚——89、Hadoop-1-架构及概念
大数据·linux·hadoop·分布式