Flink编程模型:DataStream与DataSet
前言
上一篇我们深入了解了 Flink 的架构,知道了 JobManager 和 TaskManager 是怎么协作的。但光懂架构还不够,我们还得知道怎么写 Flink 程序。
这篇文章我们来聊聊 Flink 的编程模型,搞清楚 DataStream、DataSet 这些 API 到底是什么关系,以及 Flink 1.12 之后的"流批一体"是怎么回事。
🏠个人主页:你的主页
目录
- 一、Flink编程模型概览
- [二、DataStream API详解](#二、DataStream API详解)
- [三、DataSet API简介](#三、DataSet API简介)
- 四、流批一体的演进
- 五、编程范式与执行流程
- 六、常用算子分类
- 七、动手实践:流批一体案例
- 八、总结
一、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?
- 维护两套API成本高:流和批两套代码,bug要修两遍
- 流批一体是趋势:批是流的特例,统一更优雅
- 用户学习成本:学一套API比学两套简单
- 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程序六步曲
- 创建
StreamExecutionEnvironment - 配置执行环境(可选)
- 添加数据源 Source
- 数据转换 Transformation
- 添加数据输出 Sink
- 调用
execute()触发执行
版本演进
Flink 1.11- 两套 API:DataStream(流)+ DataSet(批)
Flink 1.12 流批一体:DataStream 统一处理流和批
Flink 1.14+ DataSet 正式废弃
下一篇文章,我们将深入学习 Flink 数据源 Source,看看怎么从 Kafka、文件、数据库等各种地方读取数据。
热门专栏推荐
- Agent小册
- Java基础合集
- Python基础合集
- Go基础合集
- 大数据合集
- 前端小册
- 数据库合集
- Redis 合集
- Spring 全家桶
- 微服务全家桶
- 数据结构与算法合集
- 设计模式小册
- Ai工具小册
等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟