第3章 DataStream API基础:执行环境、数据源、转换、数据汇与程序骨架

好,咱们进入实战前的第一道硬菜——DataStream API。说白了,这就是Flink流处理的“手脚架”。你写任何流作业,都绕不开这几个核心部件:执行环境、Source、Transformation、Sink。我刚开始学Flink时,总觉得这些概念太抽象,直到自己动手搭了一个实时同步任务,才真正理解它们是怎么串起来的。

3.1 执行环境(Execution Environment)

执行环境,就是你的Flink程序跑在哪个“舞台”上。它负责管理资源、调度任务、分配内存。我个人习惯,写任何Flink作业的第一步,就是先拿到这个环境对象。

Flink提供了三种执行环境:

类型 获取方式 适用场景
StreamExecutionEnvironment StreamExecutionEnvironment.getExecutionEnvironment() 本地IDE调试或生产集群提交,自动识别
LocalStreamEnvironment StreamExecutionEnvironment.createLocalEnvironment() 本地单机测试,方便断点调试
RemoteStreamEnvironment StreamExecutionEnvironment.createRemoteEnvironment(host, port, jarFiles) 远程提交作业到Flink集群

我的小技巧:开发阶段我通常用getExecutionEnvironment(),它会根据你运行的环境自动切换。在IDE里跑就是本地模式,打成jar包提交到集群就是分布式模式。省心。

拿到环境后,你还可以设置一些全局参数,比如并行度、时间特性、检查点配置等。举个例子:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);                    // 设置全局并行度
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);  // 使用事件时间
env.enableCheckpointing(5000);            // 每5秒做一次检查点

嗯,这里要注意:并行度不是越大越好。我曾经在一个小集群上设了32并行度,结果资源争抢严重,反而比8并行度还慢。要根据你的数据量和集群规模来调。

3.2 数据源(Source)

数据源,就是数据从哪里来。Flink支持的数据源类型非常丰富,我把它分成三类:

  • 基于文件:readTextFile()、readFile(),适合批处理或离线同步
  • 基于Socket:socketTextStream(),测试时常用
  • 基于消息队列:Kafka、RabbitMQ、Pulsar等,生产环境的主力

咱们做数据同步,最常用的就是Kafka Source。我给大家看一个典型的Kafka消费代码:

Properties props = new Properties();
props.setProperty("bootstrap.servers", "localhost:9092");
props.setProperty("group.id", "flink-sync-group");

DataStream<String> stream = env
    .addSource(new FlinkKafkaConsumer<>(
        "sync-topic",
        new SimpleStringSchema(),
        props
    ));

避坑指南:我曾经犯过一个低级错误——Kafka消费端的group.id没设置,导致每次重启作业都从最新偏移量开始消费,丢了一堆数据。记住:生产环境一定要配group.id,并且要考虑消费起始位置。

除了内置Source,你还可以自定义Source。实现SourceFunction接口,或者继承RichSourceFunction。比如模拟数据生成器:

DataStream<String> mockStream = env.addSource(new RichSourceFunction<String>() {
    @Override
    public void run(SourceContext<String> ctx) throws Exception {
        while (isRunning) {
            ctx.collect("{\"id\":1, \"name\":\"test\"}");
            Thread.sleep(1000);
        }
    }
    @Override
    public void cancel() { isRunning = false; }
});

3.3 数据转换(Transformation)

数据转换,是Flink最核心的部分。说白了,就是把输入的数据流变成你想要的样子。常用的转换算子有这些:

算子 作用 我常用的场景
map 一对一转换,输入一条输出一条 字段映射、格式转换
flatMap 一对多转换,一条输入可输出0到多条 拆分JSON数组、日志解析
filter 过滤,满足条件才保留 过滤无效数据、脏数据清洗
keyBy 按key分组,后续可做聚合 按表名、数据库名分组同步
reduce 分组后的增量聚合 合并增量变更记录
process 最灵活的算子,可访问状态和定时器 复杂业务逻辑、延迟数据处理

举个例子,假设我们从Kafka拿到的是JSON字符串,需要解析成Java对象,然后过滤掉无效数据:

DataStream<UserEvent> parsedStream = kafkaStream
    .map(json -> JsonUtil.parse(json, UserEvent.class))
    .filter(event -> event != null && event.getUserId() > 0);

你想想看,如果不用filter,脏数据流到下游Sink,写入目标库时就会报错。我在做MySQL同步时,就遇到过因为空指针异常导致整个作业挂掉的情况。所以,数据清洗一定要在Transformation阶段做干净。

3.4 数据汇(Sink)

数据汇,就是处理完的数据往哪写。Flink内置了很多Sink,比如:

  • 文件Sink:writeAsText()、StreamingFileSink
  • Socket Sink:writeToSocket()
  • JDBC Sink:JdbcSink,适合写入关系型数据库
  • Kafka Sink:FlinkKafkaProducer,适合写入消息队列

咱们做数据同步,最常用的是JDBC Sink和Kafka Sink。比如写入MySQL:

stream.addSink(JdbcSink.sink(
    "INSERT INTO user_events (user_id, event_type, event_time) VALUES (?, ?, ?)",
    (statement, event) -> {
        statement.setInt(1, event.getUserId());
        statement.setString(2, event.getEventType());
        statement.setTimestamp(3, event.getEventTime());
    },
    JdbcExecutionOptions.builder()
        .withBatchSize(1000)
        .withBatchIntervalMs(200)
        .build(),
    new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
        .withUrl("jdbc:mysql://localhost:3306/sync_db")
        .withDriverName("com.mysql.cj.jdbc.Driver")
        .withUsername("root")
        .withPassword("password")
        .build()
));

重点:JDBC Sink一定要设置批量写入参数。我见过有人一条一条插入,结果吞吐量只有几百条/秒。设置batchSize=1000后,轻松跑到几万条/秒。但也要注意,batchSize太大可能导致内存溢出,要根据你的数据大小来调。

3.5 Flink程序骨架

好了,前面四个部件都讲完了。把它们拼起来,就是一个完整的Flink程序。我总结了一个“四步法”骨架:

  1. 获取执行环境:StreamExecutionEnvironment env = ...
  2. 添加数据源:DataStream<T> source = env.addSource(...)
  3. 数据转换:DataStream<R> result = source.map(...).filter(...).keyBy(...)
  4. 输出数据汇:result.addSink(...)

最后别忘了调用env.execute("job name")来启动作业。我见过新手写了半天代码,结果忘了execute,程序跑完啥也没发生。

下面是一个完整的示例,把前面讲的内容串起来:

public class DataSyncJob {
    public static void main(String[] args) throws Exception {
        // 1. 获取执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);
        env.enableCheckpointing(10000);

        // 2. 添加Kafka数据源
        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "localhost:9092");
        props.setProperty("group.id", "sync-group");
        DataStream<String> source = env.addSource(
            new FlinkKafkaConsumer<>("input-topic", new SimpleStringSchema(), props)
        );

        // 3. 数据转换:解析JSON,过滤无效数据
        DataStream<SyncRecord> transformed = source
            .map(json -> JsonUtil.parse(json, SyncRecord.class))
            .filter(record -> record != null && record.isValid());

        // 4. 输出到MySQL
        transformed.addSink(createJdbcSink());

        // 5. 启动作业
        env.execute("Flink Data Sync Job");
    }
}

这个骨架,你以后写任何Flink流处理作业都可以套用。说白了,就是“取数据→处理数据→写数据”这三个步骤,Flink帮你把分布式、容错、并行这些复杂的事情都封装好了。

Flink DataStream API 程序骨架 执行环境 StreamExecutionEnvironment 数据源 Source (Kafka/File/Socket) 数据转换 map/filter/keyBy/process 数据汇 Sink (JDBC/Kafka/File) 核心流程说明 1. 获取执行环境,设置并行度、检查点等全局参数 2. 添加数据源,从Kafka/文件/Socket读取数据 3. 通过map/filter/keyBy等算子进行数据转换 4. 将结果写入目标系统(MySQL/Kafka/文件)

这张图就是Flink程序的“骨架”。你以后写任何作业,都可以照着这个结构来搭。记住:环境是舞台,Source是入口,Transformation是核心逻辑,Sink是出口。把这四个部件搞明白,DataStream API你就掌握了八成。