Flink基础之Flink批流数据读取处理案例剖析:从文件到 Kafka 的完整代码


前面三篇分别讲了有界/无界流、有状态计算、应用场景。这一篇把视角从「原理」拉回「写代码」:一个 Flink 作业到底是怎么把数据读进来、处理掉、再写出去的。不管什么业务,落到代码上都是同一套三段式结构------Source 读取 → Transform 转换 → Sink 写出------差异只在于数据源有界还是无界。

下面用两个能直接编译运行的完整案例,把批处理和流处理各走一遍,再讲清两者的区别和统一。


一、数据读取处理的全流程:三段式结构

一个 Flink 作业无论复杂与否,主干永远是下面这条流水线:

  • Source(读取) :从有界源(文件、Hive、MySQL 快照)或无界源(Kafka、Pulsar)把数据读进来,转成 DataStream
  • Transform(转换)mapfilterkeyBywindowjoin 等对数据做加工,这是业务逻辑的落点。
  • Sink(写出):把结果写到文件、数据库、消息队列或数据湖。

关键结论先说:三段式结构对批流完全一致 ,真正决定「批」还是「流」的,是 Source 的有界性,以及 execution.runtime-mode 这个配置。下面的两个案例,代码骨架几乎一样,差异只在 Source、Sink 和聚合方式。


二、批处理案例:读取文件、分组聚合、写出结果

场景:订单日志文件 orders.log,每行一条记录,格式为 order_id,amount。目标是统计每个订单的累计金额,处理完输出最终结果并退出。这是典型的有界批处理。

完整代码(Flink 1.18,可直接编译运行):

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

public class BatchReadProcessDemo {
    public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env =
            StreamExecutionEnvironment.getExecutionEnvironment();
        // 显式声明批执行模式:有界源走阻塞式算子,读完输出最终结果后退出
        env.setRuntimeMode(RuntimeExecutionMode.BATCH);

        // ① Source:逐行读取有界文件,每行是一条记录
        DataStream<String> lines = env.readTextFile("hdfs:///data/orders.log");

        // ② Transform:解析 → 分组 → 聚合
        DataStream<Tuple2<String, Double>> result = lines
            .map(line -> {
                // 每行 "1001,9.9" 切分成 [orderId, amount]
                String[] p = line.split(",");
                return Tuple2.of(p[0], Double.parseDouble(p[1]));
            })
            // keyBy:按订单 ID 分组,同 key 进同一子任务,为聚合做准备
            .keyBy(t -> t.f0)
            // sum(1):对 Tuple 的第 1 列(amount)求和,内部是 ValueState 累积
            .sum(1);

        // ③ Sink:写出结果。print 打印到控制台,生产可换 writeAsText 写文件
        result.print();

        env.execute("batch-read-process-demo");
    }
}

几个要点:

  1. env.setRuntimeMode(RuntimeExecutionMode.BATCH) 显式声明批模式。有界源读完后作业自然退出;即使不声明(默认 STREAMING),结果也一致,但 BATCH 模式会额外启用排序、全局聚合、Sort-based Shuffle 等阻塞式优化,大吞吐场景更快。
  2. sum(1)1 是 Tuple 字段下标(从 0 开始),f0 是 key、f1 是金额,所以对 f1 求和写 sum(1)。这里容易写错------下标错一位,聚合对象就错了。
  3. keyBy(t -> t.f0) 用了 Tuple2f0 字段;若换成自定义 POJO,sum("amount") 按字段名反射访问会更可读。

三、流处理案例:消费 Kafka、窗口聚合、写回 Kafka

场景:订单事件实时写入 Kafka topic orders,目标是按分类统计每分钟的订单金额,结果写回 Kafka 供大屏消费。这是典型的无界流处理。

完整代码(Flink 1.18,依赖 flink-connector-kafka):

java 复制代码
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;

import java.time.Duration;

public class StreamReadProcessDemo {

    // 订单事件 POJO;sum("amount") 要求字段可被反射访问
    public static class Order {
        public String orderId;
        public double amount;
        public long eventTime;   // 事件发生时间(毫秒时间戳)
        public String category;

        public Order() {}

        public Order(String orderId, double amount, long eventTime, String category) {
            this.orderId = orderId;
            this.amount = amount;
            this.eventTime = eventTime;
            this.category = category;
        }

        // 每行 "orderId,amount,eventTime,category" 解析成 Order
        public static Order parse(String line) {
            String[] p = line.split(",");
            return new Order(p[0], Double.parseDouble(p[1]),
                             Long.parseLong(p[2]), p[3]);
        }
    }

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

        // ① Source:KafkaSource 消费无界流
        KafkaSource<String> source = KafkaSource.<String>builder()
            .setBootstrapServers("localhost:9092")
            .setTopics("orders")
            .setGroupId("flink-order-group")
            // 从 group.id 记录的 offset 继续消费(首次则从 earliest)
            .setStartingOffsets(OffsetsInitializer.committedOffsets())
            .setValueOnlyDeserializer(new SimpleStringSchema())
            .build();

        DataStream<String> raw = env.fromSource(
            source, WatermarkStrategy.noWatermarks(), "Kafka Source");

        // ② Transform:解析 → 事件时间 → 分组 → 窗口 → 聚合
        DataStream<Order> result = raw
            .map(Order::parse)
            // 声明事件时间 + 允许 5 秒乱序的 Watermark
            .assignTimestampsAndWatermarks(
                WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                    .withTimestampAssigner((order, ts) -> order.eventTime))
            // 按分类分组,聚合分布到各并行子任务
            .keyBy(order -> order.category)
            // 事件时间滚动窗口:每 1 分钟聚合一次
            .window(TumblingEventTimeWindows.of(Time.minutes(1)))
            .sum("amount");

        // ③ Sink:写回 Kafka 供下游消费(生产用 KafkaSink),这里先 print 调试
        result.print();

        // 常驻运行,不主动退出
        env.execute("stream-read-process-demo");
    }
}

批流案例放在一起看,差异一目了然:

  1. Source 不同 :批处理用 readTextFile 读有界文件;流处理用 KafkaSource 消费无界队列,并要指定 OffsetsInitializercommittedOffsets 从上次提交位点续读,earliest 从头,latest 只读新数据)。
  2. 聚合方式不同 :批处理用全局 sum,读完给出最终值;流处理必须用 window 把无界流切成有限块,否则「永远算不完」。
  3. 时间语义不同:流处理显式声明了事件时间和 Watermark,窗口按事件发生时间触发;批处理读文件通常不涉及乱序问题,用默认时间即可。

四、批流读取处理的统一:Table API / SQL

如果觉得 DataStream 的批流写法差异太大,可以用 Table API / SQL 把两者收敛成同一句。上一篇提过批流一体,这里落到代码:

sql 复制代码
-- 有界表:连文件,批执行
CREATE TABLE orders_file (
  order_id BIGINT, amount DOUBLE, category STRING, ts TIMESTAMP(3)
) WITH ('connector' = 'filesystem', 'path' = 'hdfs:///data/orders', 'format' = 'csv');

-- 无界表:连 Kafka,流执行,声明 Watermark
CREATE TABLE orders_kafka (
  order_id BIGINT, amount DOUBLE, category STRING, ts TIMESTAMP(3),
  WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH ('connector' = 'kafka', 'topic' = 'orders',
        'properties.bootstrap.servers' = 'localhost:9092', 'format' = 'json');

-- 同一句聚合:连有界表跑批,连无界表跑流,语义完全一致
SELECT category,
       TUMBLE_START(ts, INTERVAL '1' MINUTE) AS window_start,
       SUM(amount) AS total
FROM orders_kafka
GROUP BY TUMBLE(ts, INTERVAL '1' MINUTE), category;

这就是 Flink 批流一体的价值:读文件就是批,读 Kafka 就是流,业务逻辑只写一次。DataStream API 适合需要精细控制的复杂逻辑,Table SQL 适合数据团队、口径对齐的场景,两者可以混用。


五、数据读取处理环节的五个常见坑

  1. OffsetsInitializer.latest() 丢了历史数据 。流处理上线前,如果 Source 配了 latest,那么启动之前就已经存在的消息不会被消费,历史回填会「漏一批」。首次上线要做全量初始化,应该用 earliestcommittedOffsets,等追上后再切 latest
  2. 事件时间窗口不配 Watermark,窗口不触发ts 字段有了,但没写 Watermark,事件时间窗口等不到「Watermark 越过窗口结束时间」,作业跑起来不出结果。乱序场景下 Watermark 的延迟参数(如 5 秒)要按业务可接受的迟到范围设置。
  3. 无界源硬设 BATCH 模式,提交直接报错BATCH 要求数据源有界(能读完),Kafka 这类无界源会在作业提交阶段就失败。反之,有界源设 STREAMING 不会报错,只是享受不到批执行优化。
  4. sum 字段下标写错,聚合到错误的列Tuple2 的下标从 0 开始,sum(1) 聚合的是 f1。用自定义 POJO 时改用 sum("字段名"),可读性更好,也不容易错位。
  5. Sink 不支持两阶段提交,exactly-once 退化。上一篇讲过,端到端 exactly-once 依赖 Sink 支持两阶段提交(Kafka 事务、Iceberg/Hudi)。接到一个普通 JDBC Sink,Flink 只能退化成 at-least-once,重复数据要靠下游去重兜底。

Flink 的数据读取处理,剥开所有业务细节,本质就一件事:Source 决定批流,Transform 承载逻辑,Sink 决定落点,三段式结构从批到流一以贯之 。把「有界走批、无界走流、SQL 统一」这三句话吃透,再看任何 Flink 作业的代码,都能一眼看清它的数据从哪来、怎么算、到哪去。

Hudi)。接到一个普通 JDBC Sink,Flink 只能退化成 at-least-once,重复数据要靠下游去重兜底。

相关推荐
陕西企来客1 小时前
2026年8月咸阳家用雨棚上门测量怎么选
大数据·人工智能·咸阳家用雨棚上门测量
木卫四科技1 小时前
Agent Supply Chain Security:MCP、Skills 与 AI Agent 软件供应链安全新边界 | 木卫四科技
大数据·人工智能·安全·ai-bom·robot security·机器人安全·具身安全
ha_lydms2 小时前
HDFS的读写流程
大数据·hadoop·hdfs·mapreduce·yarn·maxcompute·odps
牛油果子哥q2 小时前
Prompt工程核心落地:结构化约束、Few-Shot示例、输出稳定性控
大数据·人工智能·prompt
千里码aicood2 小时前
Flask-基于python的智能健康监控系统-随机森林
大数据
IT毕设实战小研2 小时前
基于大数据的白鹿的抖音评论分析与可视化
大数据·机器学习·信息可视化·数据分析·毕业设计·旅游
诸神缄默不语9 小时前
备战 AI 出海 DAY 0:海外数据采集
大数据·人工智能
洋洋不叫杨杨10 小时前
揭秘当下知名的SEO优化渠道,你知道几个?
大数据·python
大大大大晴天11 小时前
用 Helm、CRD 与 Operator 管大数据:声明式运维如何替代脚本化部署
大数据