Flink综合总复习笔记

依据:老师划重点 md + 第 1~8 章 PPT 整理。

本手册按"会选择、会辨析、会填空、会看代码、会补全代码"的标准编写,不按 PPT 页码机械复述。重点放在老师明确划出的概念、区别、代码骨架和易错点。


一、总体框架:先建立全课程地图

Flink 的核心目标是:数据流上的有状态计算(Stateful Computations over Data Streams)。

可以这样理解:

  • Flink 是一个用于处理有界流无界流的分布式处理框架与引擎。
  • 它既能做实时流处理,也能做批处理。
  • 流处理时,程序往往不仅看"当前这一条数据",还要结合此前保存的统计结果、窗口数据、偏移量等信息;这些额外保存的信息就是状态(State)

2. 有界流与无界流

类型 开始与结束 处理特点 常见对应
有界流 Bounded Stream 有开始,也有结束 可以等所有数据到齐后统一计算、排序 批处理
无界流 Unbounded Stream 有开始,没有结束 数据持续到来,不能等数据"全部到齐" 实时流处理

易错点:

  • 无界流不是"数据很多",而是理论上没有结束
  • 批处理不代表不能用 DataStream API。Flink 1.12 之后,推荐统一使用 DataStream API ,通过设置运行模式为 BATCH 来完成批处理。

重点记忆:

  • 高吞吐、低延迟;
  • 支持事件时间与处理时间;
  • 支持 Exactly-Once 状态一致性;
  • 可连接 Kafka、Hive、JDBC、HDFS、Redis 等外部系统;
  • 支持故障恢复、动态扩缩容和高可用部署。
项目 Spark Streaming Flink
根本处理模式 批处理 流处理
数据处理方式 微批次 逐事件流式处理
运行结构 DAG 划分为多个 Stage,前一阶段完成后再进行下一阶段 事件在一个节点处理后可直接流向下游节点
数据模型 RDD / DStream 数据流与事件序列

一句话记忆:

Spark Streaming 本质上偏"微批",Flink 本质上是标准的"逐事件流处理"。

从高到低:

text 复制代码
SQL
Table API
DataStream / DataSet API
有状态流处理 / Process Function
层级 特点
SQL 最高层,使用 SQL 语句表达计算逻辑
Table API 面向表的声明式 API
DataStream API Flink 最核心的流处理 API
有状态流处理 / Process Function 最底层、最灵活,可实现复杂业务逻辑

考试易答:

  • 最高层:SQL
  • DataStream API:核心 API
  • 最底层:有状态流处理 / 处理函数
  • DataSet API:Flink 1.12 后已不推荐使用,DataStream API 可以统一处理流和批。

老师重点文档说明:第二章概念不多,但 WordCountStreamDemo 必须会写 ,尤其是 keyBy() 中接口对象、匿名内部类和 Lambda 的写法。

1. 流式 WordCount 的处理流程

text 复制代码
读取数据
→ flatMap 按空格拆单词
→ 每个单词变为 (word, 1)
→ keyBy 按单词分组
→ sum 对次数累加
→ print 输出
→ execute 触发执行

典型逻辑:

text 复制代码
hello flink
hello world

会变成:

text 复制代码
(hello, 1)
(flink, 1)
(hello, 1)
(world, 1)

再按单词分组并求和:

text 复制代码
(hello, 2)
(flink, 1)
(world, 1)

2. 必须记住的 Stream WordCount 骨架

java 复制代码
StreamExecutionEnvironment env =
        StreamExecutionEnvironment.getExecutionEnvironment();

DataStreamSource<String> lines =
        env.socketTextStream("hadoop102", 7777);

SingleOutputStreamOperator<Tuple2<String, Integer>> wordAndOne = lines
        .flatMap((String line, Collector<Tuple2<String, Integer>> out) -> {
            String[] words = line.split(" ");
            for (String word : words) {
                out.collect(Tuple2.of(word, 1));
            }
        })
        .returns(Types.TUPLE(Types.STRING, Types.INT));

SingleOutputStreamOperator<Tuple2<String, Integer>> result = wordAndOne
        .keyBy(value -> value.f0)
        .sum(1);

result.print();

env.execute();

3. keyBy() 的三种常见写法

假设流中元素类型为:

java 复制代码
Tuple2<String, Integer>

写法一:Lambda 表达式

最常用:

java 复制代码
.keyBy(value -> value.f0)

含义:取元组的第一个字段 f0 作为分组键。

写法二:匿名内部类

java 复制代码
.keyBy(new KeySelector<Tuple2<String, Integer>, String>() {
    @Override
    public String getKey(Tuple2<String, Integer> value) {
        return value.f0;
    }
})

含义与 Lambda 完全一致,只是写法更完整。

写法三:单独定义 KeySelector 函数类

java 复制代码
public class MyKeySelector
        implements KeySelector<Tuple2<String, Integer>, String> {

    @Override
    public String getKey(Tuple2<String, Integer> value) {
        return value.f0;
    }
}

使用:

java 复制代码
.keyBy(new MyKeySelector())

4. 为什么 flatMap 后有时需要 .returns()

Java 有泛型擦除 问题。对于某些 Lambda 表达式,Flink 只能推断出结果是 Tuple2,但无法准确识别其完整泛型类型,例如:

java 复制代码
Tuple2<String, Integer>

因此需要手动补充类型信息:

java 复制代码
.returns(Types.TUPLE(Types.STRING, Types.INT))

考试中如果问原因,可以答:

因为 Java 泛型擦除会导致 Flink 对 Lambda 返回类型推断不完整,需要显式指定类型信息,以便正确完成序列化和反序列化。


老师特别强调:三种部署模式必须知道,且要能说出区别。

1. Standalone 模式

含义

Standalone 是 Flink 独立运行模式,不依赖 YARN、Kubernetes 等外部资源管理平台。

特点

  • Flink 自己运行 JobManager 和 TaskManager;
  • 不依赖外部资源调度平台;
  • 资源不足、节点故障时,不具备自动扩缩容和自动资源重分配保证;
  • 多数情况下需要人工处理。

适用场景

  • 本地开发;
  • 功能测试;
  • 作业较少、资源需求较简单的环境。

易错判断

Standalone 并不是不能运行生产作业,而是资源调度和故障处理能力相对弱,因此通常更适合开发测试或简单场景。

2. YARN 模式的基本运行过程

YARN 模式下:

text 复制代码
Client 提交 Flink 应用
→ YARN ResourceManager 申请资源
→ NodeManager 分配 Container
→ Container 中启动 JobManager 和 TaskManager
→ Flink 根据作业所需 Slot 动态申请 TaskManager 资源

注意区分:

  • YARN ResourceManager:YARN 平台的资源管理器;
  • Flink ResourceManager:Flink 内部组件,负责管理 Flink 的 Slot。

二者名字相同,但不是同一个组件。

3. 三种部署模式对比

对比点 会话模式 Session Mode 单作业模式 Per-Job Mode 应用模式 Application Mode
集群创建时机 先启动集群,再提交作业 提交作业后,为该作业创建集群 提交应用后,为该应用创建集群
多个作业是否共用集群
资源是否共享 是,作业竞争资源 否,资源隔离 否,资源隔离
作业完成后集群是否关闭 不一定,Session 仍可继续接收作业
main() 执行位置 Client Client JobManager
适用场景 小规模、执行时间短、作业多 稳定生产作业 减少客户端压力、应用级隔离

4. 会话模式 Session Mode

核心思想

先申请一个 Flink 集群,之后不断向这个集群提交作业。

text 复制代码
先启动 Session Cluster
→ Client 提交 Job1
→ Client 提交 Job2
→ Client 提交 Job3

特点

  • 集群资源在启动时就确定;
  • 多个作业共享资源;
  • 作业之间可能互相竞争内存、Slot、CPU;
  • 适合许多小作业、短作业。

一句话记忆:

会话模式:一个集群,多个作业,共享资源。

5. 单作业模式 Per-Job Mode

核心思想

每提交一个作业,就为该作业单独启动一个 Flink 集群。

text 复制代码
提交 Job1 → 创建 Cluster1 → Job1 完成 → Cluster1 关闭
提交 Job2 → 创建 Cluster2 → Job2 完成 → Cluster2 关闭

特点

  • 每个作业独享集群;
  • 作业之间资源隔离;
  • 作业结束后,集群和资源一起释放;
  • 生产环境更稳定;
  • 一般需要依赖 YARN、Kubernetes 等资源管理平台。

一句话记忆:

单作业模式:一个作业,一个集群,作业结束,集群释放。

6. 应用模式 Application Mode

核心思想

应用程序不再主要由客户端解析和执行,而是直接交给 JobManager 执行。

与单作业模式的区别

两者都具有"一个应用或作业对应一个集群"的特点,但最关键区别是:

项目 单作业模式 应用模式
应用 main() 的执行位置 Client JobManager
Client 工作负担 较重,需要解析应用、传递依赖 较轻
网络传输压力 Client 需要传输依赖、二进制数据 压力较小

一句话记忆:

应用模式:让 JobManager 直接执行应用程序,减少 Client 的资源和网络压力。


本章重点是:组件职责、并行度设置、算子链、任务槽与并行度的关系。

1. JobManager、TaskManager 总体关系

text 复制代码
Client
  ↓ 提交作业
JobManager
  ↓ 调度、申请 Slot、分配任务
TaskManager
  ↓
真正执行具体计算

2. JobManager 的三个核心组件

2.1 JobMaster

JobMaster 是 JobManager 中最核心的组件。

关键点:

  • 一个 Job 对应一个 JobMaster
  • 多个 Job 可以同时存在于一个 Flink 集群;
  • 每个 Job 都有自己的 JobMaster;
  • JobGraph 转换为 ExecutionGraph
  • 向 ResourceManager 申请 Slot;
  • 获取资源后把任务分配给 TaskManager;
  • 协调检查点 Checkpoint。

高频描述题

"负责单个作业、生成执行图、申请 Slot、协调检查点"的组件是什么?

答案:JobMaster

2.2 ResourceManager

Flink 内部的 ResourceManager 负责资源分配与管理。

它管理的核心资源是:

text 复制代码
TaskManager 的 Task Slot

注意:

  • Flink 集群中只有一个 Flink ResourceManager;
  • 它主要负责管理 Slot;
  • 每个 Task 要被分配到 Slot 中运行;
  • 不要与 YARN 的 ResourceManager 混淆。

高频描述题

"管理 TaskManager 的 Slot、负责资源分配"的组件是什么?

答案:ResourceManager

2.3 Dispatcher

Dispatcher 的职责:

  • 提供 REST 接口;
  • 接收作业提交;
  • 为新作业启动 JobMaster;
  • 启动 Web UI;
  • 展示和监控作业信息。

高频描述题

"负责 REST 作业提交、启动 JobMaster、提供 Web UI"的组件是什么?

答案:Dispatcher

3. TaskManager

TaskManager 是 Flink 的工作进程。

主要职责:

  • 真正执行数据流计算;
  • 内部包含多个 Slot;
  • 启动后向 ResourceManager 注册 Slot;
  • 将 Slot 提供给 JobMaster;
  • 接收 JobMaster 分配的任务并执行。

一句话:

JobManager 负责"管、调、分",TaskManager 负责"真正干活"。

4. 并行度 Parallelism

4.1 什么是并行度

一个算子可以被拆分成多个并行子任务执行。

text 复制代码
map 算子
→ map[1]
→ map[2]
→ map[3]

某个算子的子任务数量,就是该算子的并行度。

例如:

text 复制代码
Source 并行度 = 2
Map 并行度 = 2
Window 并行度 = 2
Sink 并行度 = 1

则整个作业的并行度通常看作:

text 复制代码
最大并行度 = 2

4.2 并行度的三种设置方式

方式一:单独设置某个算子

java 复制代码
stream.map(word -> Tuple2.of(word, 1L))
      .setParallelism(2);

特点:

  • 只影响当前算子;
  • 是最细粒度的设置方式。

方式二:代码中设置全局并行度

java 复制代码
env.setParallelism(2);

特点:

  • 设置当前程序的默认并行度;
  • 没有单独设置并行度的算子,默认使用该值;
  • 实际生产中通常不建议硬编码全局并行度,否则扩缩容不够灵活。

方式三:提交作业时设置

bash 复制代码
bin/flink run -p 2 -c 全类名 jar包

特点:

  • 作用类似全局并行度;
  • 比直接写死在代码中更灵活;
  • Web UI 提交作业时也可以填写并行度。

方式四:配置文件设置

flink-conf.yaml 中:

yaml 复制代码
parallelism.default: 2

特点:

  • 作为集群默认并行度;
  • 当代码和提交命令均未设置时使用;
  • 开发环境中默认并行度通常为本机 CPU 核数。

易错点

keyBy() 不能设置并行度:

java 复制代码
.keyBy(...)

原因:

keyBy() 不是普通算子,它是逻辑上的按键分区操作。

5. 算子链 Operator Chain

5.1 算子之间的两种数据传输方式

一对一 One-to-One

特点:

  • 不重新分区;
  • 数据顺序和分区关系保持;
  • 上游一个子任务对应下游一个子任务;
  • 常见于 mapfilterflatMap
text 复制代码
Source[1] → Map[1]
Source[2] → Map[2]

重分区 Redistributing

特点:

  • 数据会重新分配给下游任务;
  • 下游可能接收多个上游任务的数据;
  • 类似 Spark 的 Shuffle。

常见场景:

text 复制代码
map → keyBy / window
keyBy / window → Sink

5.2 什么是算子链

满足以下条件的算子可以合并为同一个 Task:

text 复制代码
并行度相同
+
一对一的数据传输关系

例如:

text 复制代码
Source → map

通常可以合并。

text 复制代码
Source + map
→ 一个 Task

好处:

  • 减少线程切换;
  • 减少缓冲区数据交换;
  • 降低延迟;
  • 提升吞吐量。

Flink 默认会自动进行算子链合并。

禁止合并算子链

java 复制代码
.map(word -> Tuple2.of(word, 1L))
.disableChaining();

从当前位置开始新的算子链

java 复制代码
.map(word -> Tuple2.of(word, 1L))
.startNewChain();

6. 任务槽 Task Slot

6.1 什么是 Slot

TaskManager 是一个 JVM 进程,可以运行多个线程执行多个子任务。

为了划分资源,Flink 将 TaskManager 的资源划分为多个 Slot。

text 复制代码
TaskManager
├─ Slot 1
├─ Slot 2
└─ Slot 3

每个 Slot 表示一部分相对独立的计算资源,主要用于隔离内存资源。

6.2 Slot 数量设置

flink-conf.yaml 中:

yaml 复制代码
taskmanager.numberOfTaskSlots: 8

通常可设置为机器 CPU 核数附近。

注意:

Slot 主要隔离内存,不是严格的 CPU 隔离。

6.3 Slot 共享

默认情况下,Flink 允许同一作业中不同任务节点的子任务共享 Slot。

例如:

text 复制代码
Source → flatMap → reduce → sink

在默认 Slot 共享机制下,属于同一条流水线、对应并行子任务的任务可以放进同一个 Slot。

这样做的好处:

  • 资源密集型与非资源密集型任务可共享资源;
  • 更容易保证整条作业管道在 TaskManager 故障时仍具备恢复能力;
  • 可以减少需要的 Slot 数量。

指定 Slot 共享组:

java 复制代码
.map(word -> Tuple2.of(word, 1L))
.slotSharingGroup("group1");

只有同一个 Slot Sharing Group 中的子任务才能共享 Slot。

6.4 Task Slot 与 Parallelism 的区别

概念 Task Slot Parallelism
本质 集群资源单位 程序实际并发能力
属性 静态资源概念 动态执行概念
配置 taskmanager.numberOfTaskSlots parallelism.default-psetParallelism()
作用 决定集群可提供多少并发资源 决定作业实际启动多少并行子任务

一句话:

Slot 是"有多少工位",并行度是"实际安排多少人同时干活"。

6.5 YARN 下 TaskManager 数量计算

text 复制代码
TaskManager 数量
=
向上取整(Job 并行度 / 每个 TaskManager 的 Slot 数)

例如:

text 复制代码
Job 并行度 = 10
每个 TM Slot 数 = 3

TM 数量 = ceil(10 / 3) = 4

第 5 章 DataStream API:本课程代码重点最多的一章

本章重点包括执行环境、Source、Transformation、物理分区、分流合流、Kafka 与 JDBC Sink。

1. DataStream 程序基本结构

text 复制代码
Execution Environment
→ Source
→ Transformation
→ Sink
→ execute()

对应代码结构:

java 复制代码
StreamExecutionEnvironment env =
        StreamExecutionEnvironment.getExecutionEnvironment();

// Source
DataStreamSource<String> source = ...;

// Transformation
SingleOutputStreamOperator<String> result = source.map(...);

// Sink
result.print();

// Execute
env.execute();

2. 创建执行环境

执行环境类:

java 复制代码
StreamExecutionEnvironment

2.1 getExecutionEnvironment():最重点

java 复制代码
StreamExecutionEnvironment env =
        StreamExecutionEnvironment.getExecutionEnvironment();

特点:

  • 本地直接运行时,自动返回本地执行环境;
  • 打成 Jar 后提交到集群时,自动返回集群执行环境;
  • 最常用、最推荐。

2.2 createLocalEnvironment():了解

java 复制代码
StreamExecutionEnvironment localEnv =
        StreamExecutionEnvironment.createLocalEnvironment();

特点:

  • 强制创建本地执行环境;
  • 不传并行度时,默认并行度通常为本机 CPU 核数。

2.3 createRemoteEnvironment():了解

java 复制代码
StreamExecutionEnvironment remoteEnv =
        StreamExecutionEnvironment.createRemoteEnvironment(
                "host",
                1234,
                "path/to/jarFile.jar"
        );

参数含义:

参数 含义
"host" JobManager 主机名
1234 JobManager 端口
"path/to/jarFile.jar" 要提交到集群执行的 Jar 包

3. 执行模式 Execution Mode

模式 含义
Streaming 默认模式,用于无界流实时处理
Batch 批处理模式
Automatic 根据数据源是否有界自动选择模式

批处理命令行方式:

bash 复制代码
bin/flink run -Dexecution.runtime-mode=BATCH ...

代码方式:

java 复制代码
env.setRuntimeMode(RuntimeExecutionMode.BATCH);

考试中建议记:

批处理优先通过提交命令设置 execution.runtime-mode=BATCH,比代码硬编码更灵活。

4. execute():为什么必须调用

java 复制代码
env.execute();

Flink 是延迟执行、事件驱动的。

写完:

java 复制代码
result.print();

只是定义了数据流图,不代表任务已真正执行。

只有调用:

java 复制代码
env.execute();

才真正触发作业执行。

5. 源算子 Source

Source 是整个程序的输入端。

常见数据源:

数据源 常见方法或连接器 适用情况
集合 fromCollection() 测试
文件 文件连接器 批处理、日志文件
Socket socketTextStream() 测试实时流
Kafka Kafka Connector 常见生产数据源
数据生成器 DataGen Connector 测试、性能测试
自定义 Source SourceFunction 特殊数据源

5.1 旧版与新版 Source 写法

旧架构:

java 复制代码
env.addSource(...)

新版 Source 架构:

java 复制代码
env.fromSource(...)

Flink 1.12 之后更推荐新 Source 架构。

5.2 Socket Source

java 复制代码
env.socketTextStream("hadoop102", 7777);

参数:

参数 含义
"hadoop102" Socket 服务所在主机
7777 Socket 端口

启动测试 Socket 服务:

bash 复制代码
nc -lk 7777

注意:

socketTextStream() 是非并行 Source,吞吐较低,通常只用于测试。

5.3 自定义 Source 的并行度

类型 是否并行
SourceFunction 并行度通常为 1
RichSourceFunction 并行度通常为 1
ParallelSourceFunction 可以并行
RichParallelSourceFunction 可以并行,且具有丰富功能

6. 基本转换算子

6.1 map()

特点:

text 复制代码
输入一个元素
→ 输出一个元素

即"一一映射"。

java 复制代码
stream.map(value -> value * 2);

适用于:

  • 字段转换;
  • 数据类型转换;
  • 给每条数据补充信息;
  • 简单加工。

6.2 filter()

filter() 根据布尔值决定是否保留元素。

java 复制代码
stream.filter(value -> value > 10);

规则:

filter() 返回值 结果
true 保留该数据并输出
false 过滤掉该数据

高频易错点:

true 不是"过滤掉",而是"通过过滤条件,保留"。

6.3 flatMap()

特点:

text 复制代码
输入一个元素
→ 输出 0 个、1 个或多个元素

适合:

  • 拆分字符串;
  • 拆分集合;
  • 一条记录生成多条记录;
  • WordCount 的按空格拆词。

例如:

text 复制代码
输入:"hello flink"
输出:"hello"、"flink"

map() 的区别:

算子 一个输入元素可产生多少输出元素
map 恰好 1 个
flatMap 0 到多个

7. 聚合相关:keyBy 不是聚合算子

这是老师特别强调的易错点。

7.1 keyBy() 的本质

keyBy() 是:

text 复制代码
按键分区

不是聚合,也不是普通转换算子。

它把一个 DataStream 按照 key 逻辑划分为多个分区,得到:

text 复制代码
KeyedStream

例如:

java 复制代码
stream.keyBy(value -> value.f0);

表示按 f0 字段分组。

7.2 keyBy() 的作用

text 复制代码
相同 key 的数据
→ 发送到同一个逻辑分区
→ 后续可以在该 key 范围内做聚合、窗口、状态计算

底层大致思路:

text 复制代码
hash(key) % 分区数量

因此如果 key 是自定义 POJO,需要正确实现:

java 复制代码
hashCode()

7.3 简单聚合算子

必须先 keyBy(),再聚合。

java 复制代码
stream
    .keyBy(value -> value.f0)
    .sum(1);

常见方法:

方法 含义
sum() 求和
min() 指定字段最小值
max() 指定字段最大值
minBy() 返回最小字段所在的整条记录
maxBy() 返回最大字段所在的整条记录

min()minBy() 的区别

假设有对象:

text 复制代码
(id, score, name)
  • min("score"):只保证 score 是最小值,其他字段可能保留旧记录值;
  • minBy("score"):返回真正拥有最小 score 的那条完整数据。

max()maxBy() 同理。

7.4 reduce()

reduce() 用于自定义归约逻辑。

java 复制代码
stream
    .keyBy(value -> value.f0)
    .reduce((value1, value2) -> {
        // 返回同类型结果
        return ...;
    });

特点:

  • 每来一条新数据,就和当前聚合状态继续计算;
  • 输入类型与输出类型相同;
  • 每个 key 都维护自己的聚合状态;
  • 无界流中状态不会自动无限清空,因此适合 key 数量有限的场景。

8. UDF:用户自定义函数

三种主要形式:

形式 说明
函数类 自己实现 MapFunctionFilterFunctionReduceFunction 等接口
匿名内部类 在调用算子时直接写匿名实现
Lambda 表达式 写法最简洁
富函数类 Rich Function 可获取运行时上下文,具有生命周期方法

Rich Function 生命周期

常见富函数:

java 复制代码
RichMapFunction
RichFilterFunction
RichReduceFunction

关键方法:

java 复制代码
open()
  • 初始化方法;
  • 一个并行子任务生命周期内通常调用一次;
  • map()filter() 等真正工作方法之前调用。
java 复制代码
close()
  • 清理资源;
  • 生命周期结束时调用一次。

注意:

text 复制代码
open()/close():每个并行子任务通常一次
map()/filter():每条数据都会调用

9. 物理分区算子

老师特别强调:

物理分区算子负责决定上游数据如何发送到下游并行任务。

keyBy() 是逻辑按键分区,不属于物理分区算子。

常见物理分区:

算子 分配方式 特点
shuffle() 随机 随机均匀发送到下游
rebalance() 轮询 面向全部下游并行任务轮流分发
rescale() 局部轮询 只在下游部分任务间轮询
broadcast() 广播 每条数据复制给所有下游任务
global() 全局 所有数据发送到下游第一个子任务
partitionCustom() 自定义 自己定义分区规则

9.1 shuffle()

java 复制代码
stream.shuffle();

特点:

  • 随机分配;
  • 大致均匀;
  • 每次运行时数据具体去向可能不同。

9.2 rebalance()

java 复制代码
stream.rebalance();

特点:

  • Round-Robin 轮询;
  • 将数据平均分配给所有下游并行任务;
  • 类似发牌。

9.3 rescale()

java 复制代码
stream.rescale();

特点:

  • 也是轮询;
  • 但不是发给所有下游任务;
  • 只会发送给某一部分下游并行任务;
  • 可以理解为"分小组发牌"。

9.4 broadcast()

java 复制代码
stream.broadcast();

特点:

  • 每条数据复制到所有下游并行任务;
  • 下游会得到重复副本;
  • 常用于配置流、规则流等需要所有下游任务都知道的数据。

9.5 global()

java 复制代码
stream.global();

特点:

  • 所有数据都发往下游第一个并行子任务;
  • 相当于强行把下游并行处理能力压到 1;
  • 容易形成性能瓶颈,使用要谨慎。

10. 分流与合流

10.1 union()

java 复制代码
stream1.union(stream2, stream3);

特点:

  • 可以合并多个流;
  • 所有流的数据类型必须一致;
  • 只是把数据合到一个流;
  • 不去重。

一句话:

union:多条同类型流合并为一条流,不去重。

10.2 connect()

java 复制代码
stream1.connect(stream2);

特点:

  • 只能连接两条流;
  • 两条流可以是不同类型;
  • 两条流仍然可以分别处理;
  • 后续一般通过 CoMapFunctionCoFlatMapFunction 等分别处理两条流。

一句话:

connect:两条流可以类型不同,连接后仍能分别处理。

10.3 unionconnect 对比

对比项 union() connect()
可连接流数量 两条或多条 只能两条
数据类型 必须相同 可以不同
是否保留两条流的处理差异 否,合并为同类型流 是,可以分别处理
是否去重 不去重 不涉及去重

11. 输出算子 Sink

Flink 计算结果最终通常要写入外部系统。

常见 Sink:

text 复制代码
控制台 print
文件 FileSink
Kafka
MySQL / JDBC
自定义 Sink

旧版写法:

java 复制代码
stream.addSink(...);

新版写法:

java 复制代码
stream.sinkTo(...);

print() 本身也是一种 Sink。

12. Kafka 连接器重点

Kafka 连接器依赖:

xml 复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka</artifactId>
    <version>${flink.version}</version>
</dependency>

Kafka Source 的关键参数一定要认识:

参数 含义
Bootstrap Servers Kafka Broker 地址
Topic 消费的主题
Group ID 消费者组
Starting Offsets 从哪里开始消费,例如 earliest、latest
Deserializer 如何把 Kafka 中的字节数据反序列化为 Java 对象

典型新 Source 架构:

java 复制代码
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
        .setBootstrapServers("hadoop102:9092")
        .setTopics("topic1")
        .setGroupId("g1")
        .setStartingOffsets(OffsetsInitializer.latest())
        .setValueOnlyDeserializer(new SimpleStringSchema())
        .build();

DataStreamSource<String> stream = env.fromSource(
        kafkaSource,
        WatermarkStrategy.noWatermarks(),
        "kafka-source"
);

逐项理解:

java 复制代码
.setBootstrapServers("hadoop102:9092")

指定 Kafka 地址。

java 复制代码
.setTopics("topic1")

指定消费主题。

java 复制代码
.setGroupId("g1")

指定消费者组。

java 复制代码
.setStartingOffsets(OffsetsInitializer.latest())

指定从最新 Offset 开始消费。

java 复制代码
.setValueOnlyDeserializer(new SimpleStringSchema())

指定消息 value 按字符串反序列化。

13. JDBC / MySQL Sink 重点

JDBC 连接器依赖:

xml 复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-jdbc</artifactId>
    <version>1.17-SNAPSHOT</version>
</dependency>

MySQL 驱动:

xml 复制代码
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
</dependency>

典型代码结构:

java 复制代码
stream.addSink(
        JdbcSink.sink(
                "insert into ws(id, ts, vc) values (?, ?, ?)",

                new JdbcStatementBuilder<WaterSensor>() {
                    @Override
                    public void accept(
                            PreparedStatement ps,
                            WaterSensor value
                    ) throws SQLException {

                        ps.setString(1, value.getId());
                        ps.setLong(2, value.getTs());
                        ps.setInt(3, value.getVc());
                    }
                },

                JdbcExecutionOptions.builder()
                        .withBatchSize(1000)
                        .build(),

                new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
                        .withUrl("jdbc:mysql://localhost:3306/test")
                        .withDriverName("com.mysql.cj.jdbc.Driver")
                        .withUsername("root")
                        .withPassword("root")
                        .build()
        )
);

accept() 方法的重点

accept() 会在需要写入一条数据时执行。

java 复制代码
ps.setString(1, value.getId());
ps.setLong(2, value.getTs());
ps.setInt(3, value.getVc());

对应 SQL 中的三个占位符:

sql 复制代码
insert into ws(id, ts, vc) values (?, ?, ?)

位置必须一一对应:

SQL 占位符 Java 设置方法 对应字段
第 1 个 ? setString(1, ...) id
第 2 个 ? setLong(2, ...) ts
第 3 个 ? setInt(3, ...) vc

第 6 章 时间与窗口:四大基石之一

老师强调:Flink 四大重要基础中,本章是"时间和窗口"。必须掌握窗口分类、时间语义、水印、迟到数据和侧输出。

1. 什么是窗口 Window

Flink 主要处理无界流,而无界流不会自然结束。

窗口的作用:

text 复制代码
把无限数据流
切分成有限的数据块
再对每一块数据进行计算

正确理解:

窗口不是一个静态"框",更像是一个动态创建的数据桶。

数据落到某个时间范围内,就进入对应窗口;窗口达到触发条件后再进行计算。

2. 窗口分类

2.1 按驱动类型分类

类型 划分依据 记忆方式
时间窗口 Time Window 时间范围 定点发车
计数窗口 Count Window 元素数量 人齐发车

时间窗口

例如:

text 复制代码
每 30 秒统计一次

窗口根据时间开始和结束。

计数窗口

例如:

text 复制代码
每收集 10 条数据统计一次

达到指定数量后触发。

2.2 按分配规则分类

类型 窗口关系 数据是否可能进入多个窗口
滚动窗口 Tumbling Window 首尾相接,无重叠、无间隔 不会
滑动窗口 Sliding Window 有固定大小和滑动步长 可能
会话窗口 Session Window 按会话间隔划分 不会重叠
全局窗口 Global Window 同一 key 全部放入同一窗口 默认不自动触发

3. 滚动窗口 Tumbling Window

特点:

  • 固定窗口大小;
  • 窗口之间首尾相接;
  • 不重叠;
  • 无间隔;
  • 每条数据只属于一个窗口;
  • 只有一个参数:窗口大小。

例如:

text 复制代码
每 30 秒统计一次最近 30 秒数据

代码思路:

java 复制代码
.window(TumblingEventTimeWindows.of(Time.seconds(30)))

参数数量:1 个。

4. 滑动窗口 Sliding Window

特点:

  • 固定窗口大小;
  • 有固定滑动步长;
  • 参数有两个:窗口大小和滑动步长;
  • 当滑动步长小于窗口大小时,窗口会重叠;
  • 一条数据可能被分配到多个窗口。

例如:

text 复制代码
每 30 秒统计一次最近 1 分钟数据

代码:

java 复制代码
.window(
        SlidingEventTimeWindows.of(
                Time.minutes(1),
                Time.seconds(30)
        )
)

参数顺序:

text 复制代码
窗口大小 size
→ 滑动步长 slide

也就是:

java 复制代码
SlidingEventTimeWindows.of(size, slide)

滚动窗口与滑动窗口的关系

text 复制代码
size = slide

时:

text 复制代码
滑动窗口 = 特殊的滚动窗口

例如:

java 复制代码
SlidingEventTimeWindows.of(
        Time.seconds(30),
        Time.seconds(30)
)

效果上等价于 30 秒滚动窗口。

5. 会话窗口 Session Window

会话窗口只能基于时间定义。

核心参数:

text 复制代码
session gap

即会话超时时间、两个会话之间允许的最大间隔。

判断逻辑:

text 复制代码
相邻数据间隔 < gap
→ 属于同一个会话窗口

相邻数据间隔 > gap
→ 进入新会话窗口

特点:

  • 窗口长度不固定;
  • 起始结束时间不固定;
  • 窗口之间不会重叠;
  • 适用于用户会话、登录行为、连续点击等场景。

6. 全局窗口 Global Window

特点:

  • 相同 key 的所有数据放到同一个窗口;
  • 没有结束时间;
  • 默认不会自动触发计算;
  • 需要自定义 Trigger 才能触发;
  • Count Window 底层就是依赖 Global Window 实现。

一句话:

全局窗口不会自己结束,也不会自己自动计算。

7. Keyed Window 与 Non-Keyed Window

7.1 Keyed Window

先分组:

java 复制代码
stream
    .keyBy(...)
    .window(...)

特点:

  • 每个 key 有自己独立的一组窗口;
  • 相同 key 的数据进入同一逻辑分区;
  • 可以并行执行;
  • 实际业务最常用。

7.2 Non-Keyed Window

不做 keyBy()

java 复制代码
stream
    .windowAll(...)

特点:

  • 所有数据进入同一个窗口;
  • 非并行操作;
  • 相当于窗口计算并行度为 1;
  • 即使手动调高并行度也无效。

易错点:

windowAll() 本身就是非并行操作。

8. 窗口函数

窗口函数负责定义窗口中数据如何计算。

分为两类:

类型 代表方法 特点
增量聚合函数 reduce()sum()min()max()aggregate() 每来一条数据就更新中间结果
全窗口函数 apply()process() 先收集完整窗口数据,触发时再统一处理

增量聚合函数

优点:

  • 节省内存;
  • 不需要缓存所有窗口数据;
  • 适合求和、最大值、最小值等。

例如:

java 复制代码
.sum("vc")

全窗口函数

特点:

  • 需要获取窗口完整数据;
  • 可以获取窗口起始时间、结束时间等上下文;
  • 适合复杂计算。

常见:

java 复制代码
.apply(...)
.process(...)

9. 时间语义

时间语义 含义
Event Time 数据真实发生、产生时的时间
Processing Time Flink 实际处理该数据时的系统时间

例如一条支付记录:

text 复制代码
用户支付发生时间:10:00:00
Flink 收到并处理时间:10:00:05

则:

text 复制代码
Event Time = 10:00:00
Processing Time = 10:00:05

为什么实际业务常用 Event Time

因为网络、Kafka、分布式传输可能造成延迟或乱序。

如果统计:

text 复制代码
8:00 ~ 9:00 的活跃用户

通常应该按用户行为真正发生的时间统计,而不是按数据什么时候到达服务器统计。

从 Flink 1.12 起,事件时间成为默认时间语义。

10. Watermark 水印:本章最重要概念之一

10.1 为什么需要 Watermark

事件时间数据经常乱序。

例如真实事件时间顺序:

text 复制代码
10:08
10:09
10:11
10:15

但 Flink 接收到的顺序可能是:

text 复制代码
10:08
10:15
10:09
10:11

如果没有水印,窗口很难判断:

text 复制代码
"这个窗口的数据是不是已经基本到齐,可以计算了?"

10.2 Watermark 的本质

Watermark 可以理解为:

事件时间进度标记,也是允许一定乱序后的延迟触发机制。

常见表达:

text 复制代码
Watermark
=
当前已观察到的最大事件时间
-
最大允许乱序时间

例如:

text 复制代码
当前最大事件时间 = 10:30
允许乱序时间 = 2 秒

Watermark = 10:28

注意:

水印不会修改原始事件时间。

窗口边界仍按 Event Time 划分,水印只影响"什么时候触发窗口计算"。

10.3 窗口触发条件

对于事件时间窗口,通常需要同时满足:

text 复制代码
1. 窗口中有数据
2. Watermark >= 窗口结束时间

即:

text 复制代码
Watermark >= Window End Time

例如窗口:

text 复制代码
[10:00:00, 10:10:00)

当:

text 复制代码
Watermark >= 10:10:00

时,该窗口才可以触发计算。

10.4 水印代码骨架

java 复制代码
WatermarkStrategy<WaterSensor> watermarkStrategy =
        WatermarkStrategy
                .<WaterSensor>forBoundedOutOfOrderness(
                        Duration.ofSeconds(3)
                )
                .withTimestampAssigner(
                        (event, timestamp) -> event.getTs() * 1000L
                );

SingleOutputStreamOperator<WaterSensor> stream = source
        .assignTimestampsAndWatermarks(watermarkStrategy);

关键理解:

java 复制代码
forBoundedOutOfOrderness(Duration.ofSeconds(3))

表示:

最多允许约 3 秒的乱序。

java 复制代码
withTimestampAssigner(...)

表示:

从哪一个字段中提取事件时间戳。

11. 迟到数据、Allowed Lateness 与侧输出

11.1 水印可以解决什么

水印可以解决一定范围内的短期乱序和延迟。

但它不能无限等待,否则窗口永远无法关闭。

11.2 allowedLateness()

java 复制代码
.allowedLateness(Time.seconds(5))

含义:

Watermark 到达窗口结束时间后,窗口先触发计算,但暂时不关闭,继续等待一段允许迟到时间。

未设置 allowedLateness 时:

text 复制代码
Watermark 到达窗口结束时间
→ 窗口计算
→ 窗口关闭

设置 allowedLateness 后:

text 复制代码
Watermark 到达窗口结束时间
→ 窗口先计算
→ 暂不关闭
→ 在允许迟到时间内继续接收迟到数据
→ 有迟到数据到达时可能再次触发计算

11.3 Side Output 侧输出流

如果数据已经:

text 复制代码
晚于 Watermark
+
又晚于 allowedLateness

则它已经无法再放回原窗口。

这时可以通过侧输出流收集:

java 复制代码
OutputTag<WaterSensor> lateTag =
        new OutputTag<WaterSensor>("late-data") {};

SingleOutputStreamOperator<String> result = stream
        .keyBy(WaterSensor::getId)
        .window(TumblingEventTimeWindows.of(Time.seconds(10)))
        .allowedLateness(Time.seconds(5))
        .sideOutputLateData(lateTag)
        .process(...);

DataStream<WaterSensor> lateData =
        result.getSideOutput(lateTag);

侧输出的用途:

  • 收集超时迟到数据;
  • 后续单独补偿;
  • 写入 Kafka;
  • 写入数据库;
  • 人工分析或修复。

一句话:

正常窗口处理不了的超级迟到数据,放进 Side Output 交给开发者自行处理。


第 7 章 状态与检查点:四大基石之二

本章重点是状态的分类、Keyed State 各类型、Checkpoint 与 Savepoint 的区别及恢复命令。

1. 什么是状态 State

无状态算子:

text 复制代码
当前输出只依赖当前输入

例如:

java 复制代码
map()
filter()
flatMap()

有状态算子:

text 复制代码
当前输出依赖当前输入
+
此前保存的数据

例如:

text 复制代码
聚合算子
窗口算子

有状态算子的典型过程:

text 复制代码
接收数据
→ 读取当前状态
→ 更新状态
→ 输出结果

2. 托管状态与原始状态

类型 含义
Managed State 托管状态 Flink 统一管理状态存储、恢复、重分配
Raw State 原始状态 用户自己管理序列化、存储和恢复

实际开发中通常使用:

text 复制代码
Managed State

因为故障恢复、状态重组等工作由 Flink 处理。

3. Operator State 与 Keyed State

3.1 共同点

无论 Operator State 还是 Keyed State:

  • 都由本地并行子任务维护;
  • 不同并行子任务之间状态不共享;
  • 每个并行子任务只能访问自己维护的状态。

3.2 Operator State 算子状态

作用范围:

text 复制代码
当前算子子任务实例

也就是说:

text 复制代码
Task 1 有自己的状态
Task 2 有自己的状态
Task 3 有自己的状态

同一个 Task 中处理的所有数据访问同一份算子状态。

常见应用:

  • Source 的 Offset;
  • 当前任务处理到第几条数据;
  • 当前子任务的局部缓存数据。

使用时通常需要实现:

java 复制代码
CheckpointedFunction

3.3 Keyed State 按键分区状态

Keyed State 必须建立在:

java 复制代码
keyBy(...)

之后的 KeyedStream 上。

作用范围:

text 复制代码
当前并行子任务
+
当前 key

例如:

text 复制代码
key = user1 → 一份状态
key = user2 → 一份状态
key = user3 → 一份状态

重点理解:

Keyed State 不仅区分 Task,也区分 key。

不同 key 的状态彼此隔离。

没有 keyBy() 的普通 DataStream,即使使用富函数,也不能直接通过运行时上下文访问 Keyed State。

4. Keyed State 的类型

状态类型 存储内容 常用方法
ValueState<T> 单个值 value()update()
ListState<T> 列表 add()addAll()get()
ReducingState<T> 按 ReduceFunction 聚合后的结果 add()get()
AggregatingState<IN, OUT> 按 AggregateFunction 聚合后的结果 add()get()
MapState<K, V> Map 键值对 put()get()entries()
FoldingState 旧的折叠状态 已废弃,推荐用 AggregatingState 替代

4.1 ValueState

适合保存单个值。

例如记录某个用户最近一次访问时间:

java 复制代码
ValueState<Long> lastVisitTime;

更新:

java 复制代码
lastVisitTime.update(value);

获取:

java 复制代码
Long time = lastVisitTime.value();

4.2 ListState

适合保存多个元素。

例如保存某用户最近若干条行为记录:

java 复制代码
ListState<String> behaviorState;

添加:

java 复制代码
behaviorState.add("click");

获取:

java 复制代码
Iterable<String> values = behaviorState.get();

4.3 ReducingState

适合每次输入新数据时,都按 ReduceFunction 做归约。

例如不断累加某个 key 的统计值。

4.4 AggregatingState

适合输入类型和输出类型不同的复杂聚合。

例如:

text 复制代码
输入:订单对象
中间状态:金额与数量
输出:平均订单金额

4.5 MapState

适合维护键值对结构。

例如:

text 复制代码
商品 ID → 商品购买数量

5. Checkpoint 检查点

5.1 Checkpoint 的作用

Checkpoint 可以理解为自动"存档"。

Flink 定期保存:

  • 算子状态;
  • Keyed State;
  • Source Offset;
  • 窗口状态;
  • 聚合结果等。

发生故障后:

text 复制代码
从最近检查点恢复
→ 恢复状态
→ 从对应 Offset 继续处理

目标是使故障恢复后的结果与故障前保持一致。

因此 Checkpoint 也称:

text 复制代码
一致性检查点

5.2 开启 Checkpoint

java 复制代码
env.enableCheckpointing(1000);

含义:

text 复制代码
每 1000 毫秒,即每 1 秒进行一次检查点快照。

保存到 HDFS:

java 复制代码
env.setStateBackend(
        new FsStateBackend(
                "hdfs://bigdata01:9820/flink/checkpoint"
        )
);

取消作业后保留检查点:

java 复制代码
env.getCheckpointConfig()
        .enableExternalizedCheckpoints(
                CheckpointConfig.ExternalizedCheckpointCleanup
                        .RETAIN_ON_CANCELLATION
        );

含义:

作业被取消时,不自动删除外部存储中的 Checkpoint。

6. Savepoint 保存点

Savepoint 与 Checkpoint 的核心原理、状态快照算法基本相同。

最大区别在于:

对比项 Checkpoint Savepoint
触发方式 Flink 自动、周期性触发 用户手动触发
主要作用 故障恢复 运维、升级、迁移、暂停恢复
创建频率 经常 按需
是否带额外元数据 相对少 有额外元数据

Savepoint 常用于:

  • 版本升级;
  • 修改程序;
  • 调整并行度;
  • 暂停应用;
  • 恢复应用;
  • 作业归档。

6.1 创建 Savepoint

bash 复制代码
bin/flink savepoint <jobId> [targetDirectory]

例如:

bash 复制代码
bin/flink savepoint \
79f53c5c0bb3563b6b6ed3011176c411 \
hdfs://hadoop102:8020/flink/savepoint

6.2 从 Checkpoint 或 Savepoint 恢复

关键参数:

bash 复制代码
-s

命令骨架:

bash 复制代码
bin/flink run -c 全类名 -s 保存点或检查点路径 jar包

例如:

bash 复制代码
bin/flink run \
-c com.bigdata.day06._01CheckPointDemo \
-s hdfs://bigdata01:9820/flink/checkpoint/xxx \
xxx.jar

必背:

-s 用于指定 Savepoint 或 Checkpoint 路径,从对应状态快照恢复作业。


第 8 章 Table API 与 SQL:连接器、窗口、Watermark 是重点

本章重点包括表环境、表流互转、Kafka/JDBC Connector、TUMBLE()HOP()WATERMARK FOR

1. Table API 是什么

Table API 是 Flink 面向表的高层 API。

特点:

  • 支持流处理与批处理统一;
  • 以关系模型为基础;
  • 可以使用 selectjoingroup byaggregate 等操作;
  • SQL 支持基于 Apache Calcite;
  • 可与 DataStream API 相互转换。

2. Table API 开发五步

text 复制代码
1. 创建表环境
2. 创建表
3. 查询表
4. 输出表
5. 表流转换

3. 创建表环境

依赖:

xml 复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-planner_2.12</artifactId>
    <version>${flink.version}</version>
</dependency>

代码:

java 复制代码
StreamExecutionEnvironment env =
        StreamExecutionEnvironment.getExecutionEnvironment();

StreamTableEnvironment tableEnv =
        StreamTableEnvironment.create(env);

必须记住:

内容 答案
表环境类 StreamTableEnvironment
创建方法 StreamTableEnvironment.create(env)

4. 创建临时表

将 DataStream 注册为临时视图:

java 复制代码
tableEnv.createTemporaryView("wc", dataStreamSource);

作用:

text 复制代码
把 DataStream 注册成 SQL 可以直接查询的虚拟表。

后续可以写:

java 复制代码
Table table = tableEnv.sqlQuery("select * from wc");

5. DataStream 转 Table

5.1 fromDataStream()

java 复制代码
Table sensorTable =
        tableEnv.fromDataStream(sensorDS);

作用:

text 复制代码
DataStream → Table

指定字段:

java 复制代码
Table sensorTable =
        tableEnv.fromDataStream(
                sensorDS,
                $("id"),
                $("vc")
        );

字段重命名:

java 复制代码
Table sensorTable =
        tableEnv.fromDataStream(
                sensorDS,
                $("id").as("sid"),
                $("vc")
        );

5.2 createTemporaryView()

java 复制代码
tableEnv.createTemporaryView(
        "sensorTable",
        sensorDS,
        $("id"),
        $("ts"),
        $("vc")
);

作用:

text 复制代码
DataStream → 临时表 / 临时视图
→ 可以直接被 SQL 使用

6. Table 转 DataStream

6.1 toDataStream()

java 复制代码
tableEnv.toDataStream(table).print();

适合:

  • 普通追加型结果;
  • 表中每一行不会反复更新的情况。

6.2 toChangelogStream()

java 复制代码
tableEnv.toChangelogStream(table).print();

适合:

  • 聚合结果;
  • 分组统计结果;
  • 表中某一行可能被更新的情况。

例如:

sql 复制代码
SELECT id, SUM(vc)
FROM source
GROUP BY id

这里某个 idSUM(vc) 会不断变化。

因此应使用:

java 复制代码
tableEnv.toChangelogStream(table)

来得到更新日志流。

高频易错点

方法 适用场景
toDataStream() 追加型、不会更新的表
toChangelogStream() 有更新、撤回、聚合变化的表

7. Table 字段类型转换

7.1 原子类型

例如:

java 复制代码
DataStream<Long> stream = ...;

转成 Table 后只有一列。

重命名:

java 复制代码
Table table =
        tableEnv.fromDataStream(stream, $("myLong"));

7.2 Tuple 类型

Tuple 默认字段名:

text 复制代码
f0、f1、f2...

例如:

java 复制代码
DataStream<Tuple2<Long, Integer>> stream = ...;

重排字段:

java 复制代码
Table table =
        tableEnv.fromDataStream(stream, $("f1"), $("f0"));

重命名:

java 复制代码
Table table =
        tableEnv.fromDataStream(
                stream,
                $("f1").as("myInt"),
                $("f0").as("myLong")
        );

7.3 POJO 类型

POJO 转 Table 时,默认字段名使用 POJO 的属性名。

例如:

java 复制代码
Table table =
        tableEnv.fromDataStream(stream);

若 POJO 中有:

text 复制代码
user
url
timestamp

则 Table 默认列名也是这些属性名。

8. Table API 的 Kafka Connector

Kafka 依赖:

xml 复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka</artifactId>
    <version>${flink.version}</version>
</dependency>

JSON 格式依赖:

xml 复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-json</artifactId>
    <version>1.17.2</version>
</dependency>

Kafka 表的典型 DDL

sql 复制代码
CREATE TABLE table1 (
    `user_id` INT,
    `page_id` INT,
    `status` STRING
) WITH (
    'connector' = 'kafka',
    'topic' = 'topic1',
    'properties.bootstrap.servers' = 'hadoop102:9092',
    'properties.group.id' = 'g1',
    'scan.startup.mode' = 'latest-offset',
    'format' = 'json'
);

参数意义:

参数 含义
'connector' = 'kafka' 使用 Kafka 连接器
'topic' Kafka Topic
'properties.bootstrap.servers' Kafka Broker 地址
'properties.group.id' 消费者组
'scan.startup.mode' Offset 起始策略
'format' = 'json' 数据格式是 JSON

从 Kafka 读,过滤后再写 Kafka

sql 复制代码
INSERT INTO table2
SELECT *
FROM table1
WHERE status = 'success';

含义:

text 复制代码
从 table1 读取 Kafka 数据
→ 过滤 status='success'
→ 写入 table2 对应的 Kafka Topic

9. JDBC Connector

JDBC 表的典型 DDL:

sql 复制代码
CREATE TABLE table2 (
    `user_id` INT,
    `page_id` INT,
    `status` STRING
) WITH (
    'connector' = 'jdbc',
    'url' = 'jdbc:mysql://localhost:3306/flink',
    'table-name' = 't_success',
    'username' = 'root',
    'password' = 'root'
);

关键参数:

参数 含义
'connector' = 'jdbc' 使用 JDBC Connector
'url' 数据库连接地址
'table-name' 数据库表名
'username' 用户名
'password' 密码

Kafka 数据写入 MySQL:

sql 复制代码
INSERT INTO table2
SELECT *
FROM table1
WHERE status = 'success';

10. Table API 的事件时间与 Watermark

创建事件时间字段:

sql 复制代码
`event_time` TIMESTAMP(3)

创建 Watermark:

sql 复制代码
WATERMARK FOR event_time
AS event_time - INTERVAL '3' SECOND

完整结构:

sql 复制代码
CREATE TABLE table1 (
    `username` STRING,
    `price` INT,
    `event_time` TIMESTAMP(3),
    WATERMARK FOR event_time
    AS event_time - INTERVAL '3' SECOND
) WITH (
    ...
);

老师多次强调填空时重点记住:

sql 复制代码
WATERMARK FOR

如果题目只问"创建水印的关键子句",写:

sql 复制代码
WATERMARK FOR

即可。

11. Table API 滚动窗口:TUMBLE()

滚动窗口函数名:

sql 复制代码
TUMBLE()

典型写法:

sql 复制代码
SELECT
    window_start,
    window_end,
    username,
    COUNT(1) AS zongNum,
    SUM(price) AS totalMoney
FROM TABLE(
    TUMBLE(
        TABLE table1,
        DESCRIPTOR(event_time),
        INTERVAL '60' SECOND
    )
)
GROUP BY
    window_start,
    window_end,
    username;

含义:

text 复制代码
每 60 秒统计一次
每个用户名下的消费次数和消费金额

TUMBLE() 核心参数:

text 复制代码
TABLE 表名
DESCRIPTOR(时间字段)
窗口大小

12. Table API 滑动窗口:HOP()

滑动窗口函数名:

sql 复制代码
HOP()

需求:

text 复制代码
每隔 10 秒
统计最近 1 分钟
每个用户的消费金额和消费次数

典型写法:

sql 复制代码
SELECT
    window_start,
    window_end,
    username,
    COUNT(1) AS zongNum,
    SUM(price) AS totalMoney
FROM TABLE(
    HOP(
        TABLE table1,
        DESCRIPTOR(event_time),
        INTERVAL '10' SECOND,
        INTERVAL '60' SECOND
    )
)
GROUP BY
    window_start,
    window_end,
    username;

参数顺序:

text 复制代码
HOP(
    TABLE 表名,
    DESCRIPTOR(时间字段),
    滑动步长 slide,
    窗口大小 size
)

与 DataStream API 中滑动窗口不同的是:

API 参数顺序
DataStream SlidingEventTimeWindows.of() size, slide
Table API HOP() slide, size

这是非常容易写反的一点。

13. 处理时间表

处理时间字段可写为:

sql 复制代码
`event_time` AS PROCTIME()

例如:

sql 复制代码
CREATE TABLE table1 (
    `username` STRING,
    `price` INT,
    `event_time` AS PROCTIME()
) WITH (
    ...
);

含义:

不使用数据本身携带的时间戳,而使用 Flink 实际处理数据时的系统时间。


最后冲刺:高频易错点清单

  1. keyBy() 不是聚合算子。

    它是逻辑上的按键分区,结果是 KeyedStream

  2. keyBy() 也不是物理分区算子。

    物理分区算子包括 shufflerebalancerescalebroadcastglobal 等。

  3. filter() 返回 true 表示保留数据。

  4. map() 是一进一出;flatMap() 是一进零到多出。

  5. 滚动窗口一个参数,滑动窗口两个参数。

  6. DataStream 滑动窗口参数顺序:size, slide

  7. Table API 的 HOP() 参数顺序:slide, size

  8. windowAll() 是非并行操作。

  9. Watermark 不改变 Event Time。

    它只决定窗口什么时候触发。

  10. 事件时间窗口触发条件:

text 复制代码
窗口有数据
+
Watermark >= 窗口结束时间
  1. allowedLateness() 允许窗口在首次触发后继续接收一段时间的迟到数据。

  2. Side Output 处理超出 Watermark 和允许迟到时间的超级迟到数据。

  3. Operator State 按并行子任务隔离;Keyed State 按"并行子任务 + key"隔离。

  4. Checkpoint 自动触发;Savepoint 手动触发。

  5. 从状态快照恢复作业要记住 -s 参数。

  6. toDataStream() 适合追加型结果;有聚合更新时使用 toChangelogStream()

  7. Kafka Connector 关键参数:地址、Topic、消费者组、Offset 策略、反序列化或格式。

  8. JDBC 中 SQL 里的 ?PreparedStatement.setXxx(位置, 值) 必须按字段顺序对应。

  9. 三种部署模式最重要的区别:集群生命周期、资源是否共享、main() 在 Client 还是 JobManager 执行。

  10. Table API 创建表环境:

java 复制代码
StreamTableEnvironment.create(env)
  1. 注册临时视图:
java 复制代码
createTemporaryView()
  1. Table API 创建水印的关键词:
sql 复制代码
WATERMARK FOR
  1. Table API 滚动窗口函数:
sql 复制代码
TUMBLE()
  1. Table API 滑动窗口函数:
sql 复制代码
HOP()