【Flink】Flink编程模型:DataStream与DataSet

Flink编程模型:DataStream与DataSet

前言

上一篇我们深入了解了 Flink 的架构,知道了 JobManager 和 TaskManager 是怎么协作的。但光懂架构还不够,我们还得知道怎么写 Flink 程序

这篇文章我们来聊聊 Flink 的编程模型,搞清楚 DataStream、DataSet 这些 API 到底是什么关系,以及 Flink 1.12 之后的"流批一体"是怎么回事。

🏠个人主页:你的主页


目录


一、Flink编程模型概览

1.1 Flink的API层次

Flink 提供了多个层次的 API,从底层到上层:

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                          API 层次结构                                 │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │                     SQL / Table API                          │    │
│  │              声明式 API,最高层抽象,像写 SQL 一样              │    │
│  │              SELECT user_id, COUNT(*) FROM orders ...        │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                              ↑                                       │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │                   DataStream API                             │    │
│  │              核心 API,流处理的主力,推荐使用                   │    │
│  │              dataStream.map().filter().keyBy()...            │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                              ↑                                       │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │                  ProcessFunction API                         │    │
│  │              底层 API,最灵活,可访问时间、状态                  │    │
│  │              onTimer(), processElement()                     │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

选择建议

  • SQL/Table API:适合 SQL 背景的同学,快速开发,表达能力强
  • DataStream API:最常用,功能完整,绝大多数场景首选
  • ProcessFunction:需要细粒度控制时使用,比如自定义定时器

1.2 两大核心API的历史

在 Flink 1.12 之前,Flink 有两套并行的核心 API:

API 处理类型 数据特点 状态
DataStream API 流处理 无界数据流 推荐使用
DataSet API 批处理 有界数据集 已废弃
复制代码
Flink 1.11 及之前:
┌──────────────────┐    ┌──────────────────┐
│   DataStream     │    │    DataSet       │
│   (流处理)        │    │   (批处理)        │
│                  │    │                  │
│   Kafka/Socket   │    │   File/HDFS      │
└──────────────────┘    └──────────────────┘
        ↓                       ↓
   两套不同的 API,两套不同的运行时

Flink 1.12+ (流批一体):
┌──────────────────────────────────────────┐
│           DataStream API                  │
│    (统一处理流和批,DataSet 被废弃)          │
│                                          │
│   流模式:处理无界数据流(Kafka/Socket)     │
│   批模式:处理有界数据集(File/HDFS)        │
└──────────────────────────────────────────┘
        ↓
   一套 API,根据数据源自动选择执行模式

划重点 :从 Flink 1.12 开始,官方建议统一使用 DataStream API,DataSet API 已被标记为废弃。这就是所谓的"流批一体"。


二、DataStream API详解

DataStream API 是 Flink 的核心和灵魂,我们重点来讲它。

2.1 什么是DataStream?

DataStream 是 Flink 中表示数据流的核心抽象。你可以把它理解为一条"数据河流":

  • 数据像河水一样源源不断地流过来
  • 每条数据是河里的一条"鱼"
  • 各种算子(map/filter/keyBy)就像沿途的"关卡",对鱼进行加工处理
java 复制代码
// DataStream 就像一条数据流水线
DataStream<String> input = env.fromSource(...);  // 源头
DataStream<String> mapped = input.map(...);       // 加工
DataStream<String> filtered = mapped.filter(...); // 过滤
filtered.addSink(...);                            // 输出

2.2 DataStream的类型体系

DataStream 有几个重要的子类型,代表不同状态的数据流:

复制代码
┌────────────────────────────────────────────────────────────────────┐
│                     DataStream 类型体系                              │
│                                                                     │
│  DataStream<T>          普通数据流,最常用                           │
│       │                                                             │
│       ├──→ KeyedStream<T,K>    按 key 分组后的流                    │
│       │         │              通过 keyBy() 得到                    │
│       │         │                                                   │
│       │         └──→ WindowedStream<T,K,W>  开窗后的流               │
│       │                       通过 window() 得到                    │
│       │                                                             │
│       └──→ SingleOutputStreamOperator<T>  单输出流                  │
│                       大多数算子的返回类型                            │
└────────────────────────────────────────────────────────────────────┘

流转过程示例

java 复制代码
// 1. 普通 DataStream
DataStream<Order> orders = env.fromSource(...);

// 2. 经过 keyBy 变成 KeyedStream
KeyedStream<Order, String> keyedOrders = orders.keyBy(order -> order.getUserId());

// 3. 经过 window 变成 WindowedStream
WindowedStream<Order, String, TimeWindow> windowedOrders = 
    keyedOrders.window(TumblingEventTimeWindows.of(Time.minutes(5)));

// 4. 聚合后又变回 DataStream
DataStream<OrderSummary> result = windowedOrders.aggregate(new OrderAggregator());

2.3 DataStream程序骨架

每个 Flink DataStream 程序都遵循固定的套路:

java 复制代码
public class FlinkJobTemplate {
    public static void main(String[] args) throws Exception {
        
        // 1. 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 2. 配置执行环境(可选)
        env.setParallelism(4);                    // 设置并行度
        env.enableCheckpointing(60000);           // 开启 Checkpoint
        
        // 3. 添加数据源(Source)
        DataStream<String> source = env.fromSource(
            KafkaSource.<String>builder()
                .setTopics("input-topic")
                .build(),
            WatermarkStrategy.noWatermarks(),
            "Kafka Source"
        );
        
        // 4. 数据转换(Transformation)
        DataStream<Result> result = source
            .map(new MyMapFunction())          // 转换
            .filter(new MyFilterFunction())    // 过滤
            .keyBy(data -> data.getKey())      // 分组
            .window(TumblingEventTimeWindows.of(Time.minutes(5)))  // 开窗
            .reduce(new MyReduceFunction());   // 聚合
        
        // 5. 输出结果(Sink)
        result.sinkTo(
            KafkaSink.<Result>builder()
                .setRecordSerializer(...)
                .build()
        );
        
        // 6. 执行作业(别忘了这一步!)
        env.execute("My Flink Job");
    }
}

6个步骤,缺一不可

步骤 说明 是否必须
创建环境 程序入口,一切的开始 必须
配置环境 并行度、Checkpoint等 可选
添加Source 告诉Flink从哪读数据 必须
Transformation 对数据进行各种加工 必须
添加Sink 告诉Flink往哪写数据 必须
execute 触发执行,不调用程序不会跑 必须

2.4 执行环境的三种类型

java 复制代码
// 1. 本地环境(开发测试用)
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();

// 2. 远程环境(指定集群地址)
StreamExecutionEnvironment env = StreamExecutionEnvironment.createRemoteEnvironment(
    "jobmanager-host", 8081, "path/to/jar"
);

// 3. 自动获取(推荐!自动识别运行环境)
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

推荐使用 getExecutionEnvironment():它会根据运行方式自动选择:

  • 在 IDE 里跑,自动创建本地环境
  • 提交到集群,自动连接远程集群

三、DataSet API简介

虽然 DataSet API 已经被废弃,但你可能在老代码里见到它,简单了解一下。

3.1 DataSet是什么?

DataSet 是用于批处理的 API,处理的是有界数据集(Bounded Dataset):

java 复制代码
// DataSet API 示例(已废弃)
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

// 从文件读取数据
DataSet<String> text = env.readTextFile("hdfs://path/to/file");

// 批处理转换
DataSet<Tuple2<String, Integer>> counts = text
    .flatMap(new Tokenizer())
    .groupBy(0)
    .sum(1);

// 输出
counts.writeAsText("hdfs://path/to/output");

// 执行
env.execute("Batch WordCount");

3.2 DataSet vs DataStream

对比项 DataSet DataStream
数据类型 有界数据集 无界/有界数据流
执行模式 批处理 流处理/批处理
执行环境 ExecutionEnvironment StreamExecutionEnvironment
分组方式 groupBy() keyBy()
状态 已废弃 推荐使用

3.3 为什么废弃DataSet?

  1. 维护两套API成本高:流和批两套代码,bug要修两遍
  2. 流批一体是趋势:批是流的特例,统一更优雅
  3. 用户学习成本:学一套API比学两套简单
  4. DataStream已足够强大:流模式 + 批模式,覆盖所有场景

四、流批一体的演进

4.1 什么是流批一体?

流批一体的核心思想:批处理是流处理的特例

用大白话说:

  • 流处理:处理永远不会结束的数据流(比如 Kafka 消息)
  • 批处理:处理有明确结束点的数据流(比如一个文件)

既然批只是"有终点的流",那为什么要用两套 API 呢?统一成一套不就好了!

复制代码
┌────────────────────────────────────────────────────────────────────┐
│                        流批一体思想                                  │
│                                                                     │
│   无界流(Unbounded Stream)                                         │
│   ─────●────●────●────●────●────●────●────●────►  (永不结束)         │
│        │    │    │    │    │    │    │    │                        │
│        ↓ 来一条处理一条                                              │
│                                                                     │
│   有界流(Bounded Stream)                                           │
│   ─────●────●────●────●────●────■                                   │
│        │    │    │    │    │    │                                   │
│        ↓                        ↓ 有明确的结束点                      │
│        流处理模式       或      批处理模式                            │
│                                                                     │
│   Flink 用同一套 DataStream API 处理两种情况!                        │
└────────────────────────────────────────────────────────────────────┘

4.2 执行模式的选择

从 Flink 1.12 开始,DataStream API 支持两种执行模式:

java 复制代码
// 方式1:代码中设置
env.setRuntimeMode(RuntimeExecutionMode.STREAMING);  // 流模式
env.setRuntimeMode(RuntimeExecutionMode.BATCH);      // 批模式
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);  // 自动选择(默认)

// 方式2:提交作业时指定
// flink run -Dexecution.runtime-mode=BATCH xxx.jar

AUTOMATIC 模式的判断逻辑

  • 如果所有 Source 都是有界的 → 批模式
  • 只要有一个 Source 是无界的 → 流模式

4.3 流模式 vs 批模式的执行差异

虽然代码一样,但底层执行策略不同:

方面 流模式(STREAMING) 批模式(BATCH)
数据处理 来一条处理一条 攒一批再处理
状态管理 持续维护状态 任务结束后清理
容错机制 Checkpoint 重新计算失败分区
Shuffle 流式shuffle(网络传输) 落盘shuffle(更高效)
延迟 低延迟 延迟不重要
吞吐 较高 更高(针对批优化)

举个例子

java 复制代码
// 同样的代码
DataStream<String> lines = env.fromSource(fileSource, ...);
DataStream<Tuple2<String, Integer>> counts = lines
    .flatMap(new Tokenizer())
    .keyBy(t -> t.f0)
    .sum(1);

// 流模式:每来一行就更新计数,结果不断输出
// 批模式:读完整个文件后,一次性输出最终计数

4.4 流批一体的最佳实践

java 复制代码
public class UnifiedJob {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 让 Flink 自动选择执行模式
        env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        
        // 从参数获取数据源(可能是 Kafka,也可能是文件)
        DataStream<String> source;
        if (args[0].startsWith("kafka://")) {
            // 无界源 -> 流模式
            source = env.fromSource(buildKafkaSource(args[0]), ...);
        } else {
            // 有界源 -> 批模式
            source = env.fromSource(FileSource.forRecordStreamFormat(...), ...);
        }
        
        // 后续处理逻辑完全一样!
        DataStream<Result> result = source
            .map(...)
            .keyBy(...)
            .window(...)
            .aggregate(...);
        
        result.sinkTo(...);
        env.execute("Unified Batch/Stream Job");
    }
}

五、编程范式与执行流程

5.1 声明式编程范式

Flink 采用声明式编程:你只需要描述"做什么",不需要关心"怎么做"。

java 复制代码
// 你写的代码(声明式)
dataStream
    .filter(x -> x > 10)     // 只要大于10的
    .map(x -> x * 2)         // 乘以2
    .keyBy(x -> x % 10)      // 按个位数分组
    .sum(0);                 // 求和

// 你不需要关心:
// - 数据在哪台机器上执行
// - filter 和 map 是否会合并
// - keyBy 后数据怎么重分布
// - 多个 TaskManager 怎么协调
// Flink 帮你搞定!

5.2 懒加载执行

Flink 程序是懒加载 的:调用各种算子时并不会立即执行,只有调用 execute() 时才真正开始。

java 复制代码
// 这些都不会立即执行,只是在构建执行计划
DataStream<String> source = env.fromSource(...);   // 没执行
DataStream<String> mapped = source.map(...);       // 没执行
DataStream<String> filtered = mapped.filter(...);  // 没执行
filtered.addSink(...);                             // 还是没执行

// 直到调用 execute(),才会真正执行!
env.execute("Job Name");  // 开始执行了!

为什么这样设计? 为了优化!Flink 需要看到完整的执行计划,才能做全局优化(比如算子链合并)。

5.3 代码到执行的转换过程

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    从代码到执行的四层转换                              │
│                                                                      │
│  你的代码                                                             │
│      │                                                               │
│      │ 构建                                                          │
│      ↓                                                               │
│  ┌────────────────────────────────────────────────────────────┐     │
│  │                    StreamGraph(流图)                       │     │
│  │  最原始的图,每个算子一个节点                                   │     │
│  │  Source ──→ Map ──→ Filter ──→ KeyBy ──→ Sum ──→ Sink       │     │
│  └────────────────────────────────────────────────────────────┘     │
│      │                                                               │
│      │ 客户端优化                                                    │
│      ↓                                                               │
│  ┌────────────────────────────────────────────────────────────┐     │
│  │                    JobGraph(作业图)                        │     │
│  │  优化后的图,可 chain 的算子合并                               │     │
│  │  [Source→Map→Filter] ──→ [KeyBy→Sum] ──→ [Sink]             │     │
│  └────────────────────────────────────────────────────────────┘     │
│      │                                                               │
│      │ 提交到 JobManager                                            │
│      ↓                                                               │
│  ┌────────────────────────────────────────────────────────────┐     │
│  │                 ExecutionGraph(执行图)                     │     │
│  │  按并行度展开,分配到具体 Slot                                 │     │
│  │  [Task0] [Task1]  ──→  [Task0] [Task1]  ──→  [Sink0][Sink1] │     │
│  └────────────────────────────────────────────────────────────┘     │
│      │                                                               │
│      │ 部署执行                                                      │
│      ↓                                                               │
│  ┌────────────────────────────────────────────────────────────┐     │
│  │                    物理执行图                                │     │
│  │  每个 SubTask 在具体的 TaskManager Slot 上运行               │     │
│  └────────────────────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────────────────┘

六、常用算子分类

6.1 算子分类总览

复制代码
┌────────────────────────────────────────────────────────────────────┐
│                        Flink 算子分类                                │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                     Source(数据源)                           │  │
│  │    Kafka / File / Socket / Collection / 自定义               │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              ↓                                      │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                 Transformation(转换算子)                     │  │
│  │                                                               │  │
│  │   基础转换:map / flatMap / filter / project                  │  │
│  │   分区操作:keyBy / shuffle / rebalance / rescale             │  │
│  │   聚合操作:reduce / sum / min / max / aggregate              │  │
│  │   窗口操作:window / timeWindow / countWindow                 │  │
│  │   连接操作:union / connect / coMap / join                    │  │
│  │   侧输出:  process + OutputTag                               │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              ↓                                      │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                      Sink(数据输出)                          │  │
│  │    Kafka / File / MySQL / Redis / Elasticsearch / 自定义     │  │
│  └──────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────┘

6.2 基础转换算子

最常用的几个转换算子:

java 复制代码
// 1. map:一对一转换
DataStream<Integer> doubled = numbers.map(n -> n * 2);
// 输入:1, 2, 3
// 输出:2, 4, 6

// 2. flatMap:一对多转换
DataStream<String> words = lines.flatMap((String line, Collector<String> out) -> {
    for (String word : line.split(" ")) {
        out.collect(word);
    }
});
// 输入:"hello world"
// 输出:"hello", "world"

// 3. filter:过滤
DataStream<Integer> positive = numbers.filter(n -> n > 0);
// 输入:-1, 0, 1, 2
// 输出:1, 2

// 4. keyBy:按 key 分组(非常重要!)
KeyedStream<Order, String> keyed = orders.keyBy(order -> order.getUserId());
// 相同 key 的数据会被发送到同一个分区

// 5. reduce:聚合
DataStream<Integer> sum = keyed.reduce((a, b) -> a + b);

6.3 算子的数据流向

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        数据流向示意                                   │
│                                                                      │
│   map/filter/flatMap: 一对一,数据不会重分布                          │
│   ──●────●────●────●──→  ──●────●────●────●──                        │
│                                                                      │
│   keyBy: 按 key 重分布,相同 key 到同一分区                           │
│   ──●────●────●────●──→  ┌─●─●─┐  ┌─●─●─┐                           │
│      (key: a,b,a,b)      │ a a │  │ b b │                           │
│                          └─────┘  └─────┘                           │
│                          分区1     分区2                             │
│                                                                      │
│   shuffle: 随机打散到各分区                                           │
│   rebalance: 轮询分发到各分区                                         │
│   rescale: 局部轮询(只在上下游对应的分区间)                          │
└─────────────────────────────────────────────────────────────────────┘

七、动手实践:流批一体案例

7.1 场景描述

写一个统计单词出现次数的程序,要求:

  • 既能处理实时流(Socket输入)
  • 也能处理批数据(文件输入)
  • 用同一套代码!

7.2 完整代码

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

/**
 * 流批一体 WordCount 示例
 * 
 * 流模式运行:
 * 1. 先启动 nc -lk 9999
 * 2. 运行程序,参数:--mode stream
 * 
 * 批模式运行:
 * 运行程序,参数:--mode batch --input /path/to/file
 */
public class UnifiedWordCount {
    
    public static void main(String[] args) throws Exception {
        // 1. 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 2. 解析参数,决定运行模式
        String mode = getParam(args, "mode", "stream");
        String input = getParam(args, "input", "");
        
        DataStream<String> source;
        
        if ("batch".equals(mode)) {
            // 批模式:从文件读取
            env.setRuntimeMode(RuntimeExecutionMode.BATCH);
            source = env.readTextFile(input);
            System.out.println(">>> 批模式启动,读取文件: " + input);
        } else {
            // 流模式:从 Socket 读取
            env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
            source = env.socketTextStream("localhost", 9999);
            System.out.println(">>> 流模式启动,监听 localhost:9999");
        }
        
        // 3. 核心处理逻辑(流批通用!)
        DataStream<Tuple2<String, Integer>> result = source
            // 切分单词
            .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
                @Override
                public void flatMap(String line, Collector<Tuple2<String, Integer>> out) {
                    // 按空格切分
                    for (String word : line.toLowerCase().split("\\s+")) {
                        if (word.length() > 0) {
                            out.collect(Tuple2.of(word, 1));
                        }
                    }
                }
            })
            // 按单词分组
            .keyBy(tuple -> tuple.f0)
            // 累加计数
            .sum(1);
        
        // 4. 输出结果
        result.print();
        
        // 5. 执行
        env.execute("Unified WordCount - " + mode + " mode");
    }
    
    // 简单的参数解析
    private static String getParam(String[] args, String key, String defaultValue) {
        for (int i = 0; i < args.length - 1; i++) {
            if (args[i].equals("--" + key)) {
                return args[i + 1];
            }
        }
        return defaultValue;
    }
}

7.3 运行效果

流模式

bash 复制代码
# 终端1:启动 netcat
nc -lk 9999

# 终端2:运行程序
java -jar wordcount.jar --mode stream

# 在终端1输入:
hello flink
hello world

# 程序输出(实时):
(hello,1)
(flink,1)
(hello,2)
(world,1)

批模式

bash 复制代码
# 准备输入文件 input.txt:
hello flink
hello world
flink is great

# 运行程序
java -jar wordcount.jar --mode batch --input input.txt

# 程序输出(一次性):
(flink,2)
(great,1)
(hello,2)
(is,1)
(world,1)

八、总结

这篇文章我们系统学习了 Flink 的编程模型:

核心要点

主题 要点
API层次 ProcessFunction → DataStream → SQL/Table,越上层越简单
DataStream 核心API,表示数据流,推荐使用
DataSet 已废弃,被 DataStream 的批模式取代
流批一体 批是有界流的特例,用同一套 API 处理
执行模式 STREAMING / BATCH / AUTOMATIC
编程范式 声明式 + 懒加载
算子分类 Source / Transformation / Sink

Flink程序六步曲

  1. 创建 StreamExecutionEnvironment
  2. 配置执行环境(可选)
  3. 添加数据源 Source
  4. 数据转换 Transformation
  5. 添加数据输出 Sink
  6. 调用 execute() 触发执行

版本演进

复制代码
Flink 1.11-    两套 API:DataStream(流)+ DataSet(批)
Flink 1.12     流批一体:DataStream 统一处理流和批
Flink 1.14+    DataSet 正式废弃

下一篇文章,我们将深入学习 Flink 数据源 Source,看看怎么从 Kafka、文件、数据库等各种地方读取数据。


热门专栏推荐

等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持


文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊

希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏

如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟

相关推荐
面向Google编程2 小时前
Flink源码阅读:如何生成ExecutionGraph
大数据·flink
飞凌嵌入式2 小时前
AIoT出海背景下,嵌入式主控的国际认证之路与价值思考
大数据·人工智能·嵌入式硬件·区块链·嵌入式
面向Google编程3 小时前
Flink源码阅读:状态管理
大数据·flink
面向Google编程3 小时前
Flink源码阅读:Checkpoint机制(下)
大数据·flink
WZGL12303 小时前
数字化模式全面赋能,“智能+养老”破题养老痛点
大数据·人工智能·科技·生活·智能家居
专注API从业者3 小时前
构建企业级 1688 数据管道:商品详情 API 的分布式采集与容错设计
大数据·开发语言·数据结构·数据库·分布式
做cv的小昊3 小时前
【TJU】信息检索与分析课程笔记和练习(3)学术评价
大数据·人工智能·经验分享·笔记·学习·全文检索
Jackyzhe3 小时前
Flink源码阅读:Checkpoint机制(上)
大数据·flink