一、概述
动态表(Dynamic Table)和连续查询(Continuous Query)是 Flink Table API / SQL 实现流批统一 与标准关系代数语义的两大核心理论基础。
其核心思想:将无限、无界的流式数据,映射为一张随时间不断变化的逻辑表,让用户可以直接使用标准 SQL 对流数据进行查询、聚合、关联等操作,完美对齐批处理的 SQL 使用习惯。
整套机制分为三层核心能力:
-
动态输入表技术:将实时输入数据流,映射为 SQL 可识别的动态输入表;
-
连续查询技术:在动态表上执行持续计算,映射标准 SQL 运算语义;
-
动态输出表技术:将计算后的动态结果表,反向转换为可输出的数据流。
二、动态表(Dynamic Table)
2.1 产生背景
传统大数据计算存在明显的流批割裂认知:
-
批处理 :操作静态有限表,数据集固定,查询一次性执行完成;
-
流处理 :处理无界事件流,数据逐条持续到达,无固定数据集。
Flink 打破流批边界,提出核心理论:流 = 动态表的 Changelog(变更日志)
-
流是动态表的实时变更记录;
-
动态表是流数据的高层逻辑抽象。
双视角对照理解:
| 视角 | 数据表现形式 |
|---|---|
| 流视角 | (Alice, +1), (Bob, +1), (Alice, +1)... 逐条变更数据流 |
| 表视角 | 一张不断更新、追加数据的动态数据表,可随时查询快照 |
基于该设计,同一条 SQL 语句可同时适配批处理(静态表)和流处理(动态表),真正实现 Flink 流批一体。
2.2 动态表详解
动态表是 Flink 对流式无界数据的逻辑表抽象,核心特性如下:
-
随时间持续变化,支持行的插入、更新、删除操作;
-
任意时间点都可像静态批表一样执行 SQL 查询;
-
表初始为空,新流事件到达即触发表数据变更;
-
所有表的变更,最终以 Changelog 流 的形式对外输出。
三、连续查询(Continuous Query)
3.1 定义
连续查询是作用于动态表 的流式 SQL 查询,区别于批处理的一次性查询,它是永不停止的增量计算任务(除非手动停止作业)。
核心链路:输入动态表 → 连续查询计算 → 输出动态表(Changelog 流)
3.2 核心特性
-
增量计算:不重复计算全量数据,仅根据新输入数据增量更新状态和结果,每一次输出都是最新的中间结果;
-
状态驱动 :聚合、分组、连接等算子会维护状态,例如
GROUP BY会为每个 Key 单独维护聚合结果; -
完善的时间语义:原生支持事件时间、处理时间,支持滚动、滑动、会话等多种窗口类型。
3.3 实战案例:小时级用户点击统计
业务场景
实时统计每小时每个用户的页面点击次数,基于用户点击流数据计算。
SQL 语句
plsql
SELECT
user_id,
COUNT(*) AS click_cnt,
TUMBLE_START(ts, INTERVAL '1' HOUR) AS w_start
FROM clicks
GROUP BY user_id, TUMBLE(ts, INTERVAL '1' HOUR);
执行过程
-
输入层 :
clicks动态表持续接收用户点击流,不断追加新数据; -
计算层 :连续查询按
user_id+ 1 小时滚动窗口分组,为每个(user_id, window)组合维护 count 聚合状态; -
输出层:窗口水位线超过窗口结束时间后,触发窗口计算,输出最终结果。
输出结果示例
latex
+I (Alice, 5, 2024-06-01 10:00) -- 窗口 [10:00, 11:00) 最终结果插入
+I (Bob, 3, 2024-06-01 10:00)
该结果可直接写入 Kafka、Paimon、Hudi 等存储,供下游实时消费。若开启窗口早期触发,会产生 -U/+U 更新消息。
3.4 动态表两大更新模式
Flink 根据 SQL 查询是否产生更新、删除操作,将动态表输出流分为两类:
| 类型 | 名称 | 消息类型 | 触发条件 |
|---|---|---|---|
| Append-only Stream | 仅追加流 | 只有 +I 插入消息 |
无 GROUP BY、无 JOIN、无 DISTINCT、无窗口,仅数据追加 |
| Changelog Stream | 更新流 | 包含 +I/-U/+U/-D 全量变更消息 |
包含聚合、连接、去重、窗口等会更新历史结果的操作 |
四、Changelog 变更日志机制
Changelog 是 Flink Table/SQL 流处理的核心底层机制,所有算子之间的数据流转,本质都是传递 Changelog 变更日志,是动态表和连续查询得以实现的基础。
4.1 定义
Changelog 类似于 MySQL Binlog,是一套描述动态表数据变更的流式数据模型 ,每条消息对应表的一次变更操作。Flink 内部通过 RowKind 枚举定义四种变更类型:
| Changelog 类型 | 枚举值 | 含义 | 使用场景 |
|---|---|---|---|
+I |
INSERT | 插入新行 | 新数据首次写入结果表 |
-U |
UPDATE_BEFORE | 更新前旧值 | 数据更新时,标记需要替换的旧数据(可优化省略) |
+U |
UPDATE_AFTER | 更新后新值 | 数据更新后的最新结果 |
-D |
DELETE | 删除行 | 历史数据需要删除、撤回 |
4.2 引入 Changelog 的必要性
传统批表是静态快照,而 Flink 动态表是持续变化的,无法直接传递全量快照。因此 Flink 引入 Changelog 机制:
-
流转表、表转流的核心桥梁;
-
算子之间仅传递增量变更,而非全量数据,保证流式计算高效性;
-
所有算子消费 Changelog、产出新 Changelog,形成完整流式计算链路。
4.3 Changelog 流转原理
Flink Table 层所有算子(聚合、JOIN、窗口、去重)的底层数据结构为 Row + RowKind:
java
// 代码层面构建带变更类型的数据
Row row = Row.withKind(RowKind.INSERT, 1001, "Jack");
// 控制台输出:+I[1001, "Jack"]
数据传输时可序列化为 JSON、Avro 等格式,内存计算阶段无需序列化,性能优异。Flink WebUI 中 DAG 算子之间的链路,本质就是 Changelog 流传输通道。
4.4 Changelog 三大编码方式
核心概念区分:
-
Changelog 语义:描述表发生了什么变化(插入/更新/删除);
-
编码方式:Flink 用何种消息组合,物理实现这种变更语义。
Flink 提供三种标准化编码方式,适配不同业务场景,性能和规则差异显著:
| 编码方式 | 编码规则 | 核心特点 | 是否需要主键 | 状态开销 |
|---|---|---|---|---|
| Append-only | 仅使用 +I,所有数据均为插入 |
最简单、零开销、最高效,无更新删除操作 | 否 | 无 |
| Retract(撤回流) | 更新 = -D 删旧值 + +I 插新值,不使用 -U/+U |
通用性最强,无需主键;更新需两条消息,网络开销翻倍 | 否 | 全量缓存状态 |
| Upsert(更新插入流) | 首次写入 +I,更新直接 +U,删除 -D,省略 -U |
更新仅一条消息,高效;依赖主键覆盖旧数据 | 是 | 主键索引状态 |
生产选择建议
-
有明确主键、需要更新结果:优先 Upsert(高效、适配主流存储);
-
无主键、不确定数据规则:使用 Retract(通用兼容);
-
纯追加数据、无更新删除:使用 Append-only(性能最优)。
补充:Flink 默认优化省略 -U,仅审计、精准溯源场景可强制开启全量 Changelog:
java
tableEnv.toChangelogStream(table, ChangelogMode.all()).print();
4.5 特殊 Changelog 变体场景
4.5.1 Full Changelog(完整变更日志)
-
特点:完整输出
+I/-U/+U/-D四种消息; -
触发场景:复杂多层查询、自定义 UDF、手动强制开启;
-
用途:数据审计、精准溯源、问题调试。
4.5.2 Windowed Changelog(窗口变更日志)
-
特点:窗口支持早期触发时,会产生多次中间更新;
-
消息规则:仅窗口结束触发 →
+I;开启早期触发 → 先-U/+U迭代更新,最终输出+I; -
本质:Upsert/Retract 模式在窗口语义下的特殊表现。
4.5.3 Temporal Join Changelog(时态连接变更日志)
-
特点:维表数据更新时,会撤回旧 JOIN 结果、插入新结果;
-
消息模式:固定为
-D + +I,属于 Retract 流场景; -
原因:维表更新会导致整条关联结果失效,无法通过主键 Upsert 实现。
4.6 Retract vs Upsert 核心对比
两者最大差异是 UPDATE 操作的编码方式,直接决定作业性能与 Sink 适配性:
-
Retract:一次更新 = 2 条消息(删旧+插新),网络、存储、序列化开销翻倍;
-
Upsert :一次更新 = 1 条
+U消息,性能翻倍,生产首选。
Upsert 完美适配主流更新型存储:
-
MySQL/PostgreSQL:对应
INSERT ... ON DUPLICATE KEY UPDATE; -
Redis/HBase:主键 PUT 覆盖;
-
Upsert-Kafka:日志压缩保留 Key 最新值;
-
ClickHouse:主键更新语义。
五、Changelog 与 Sink 适配
Sink 必须精准识别上游 Changelog 语义,否则会出现数据重复、丢失、不一致问题。不同 Sink 对变更消息的支持能力差异极大。
5.1 主流 Sink 能力对比
| Sink 类型 | 是否支持完整 Changelog | 核心适用场景 | 精准一次支持 |
|---|---|---|---|
| Upsert-Kafka | ✅ 完全支持 | 实时聚合结果、维度表、实时大屏 | ✅ 事务开启即可 |
| 普通 Kafka | ✅ 原样输出 | 调试、Flink 作业间数据中转 | ✅ 支持 |
| Hudi | ✅ 支持(删除需配置) | 实时数据湖、CDC 入湖 | ✅ 完全支持 |
| JDBC/File/Hive | ❌ 不支持更新删除语义 | 静态数据初始化、日志归档 | ⚠️ 需自定义实现 |
| Print/Blackhole | ✅ 支持调试输出 | 开发测试、日志打印 | ❌ 不支持 |
5.2 核心 Sink 实战案例
5.2.1 Upsert-Kafka(生产首选)
核心要求:必须定义主键,自动根据 Key 覆盖旧数据,忽略无用 -U 消息。
plsql
CREATE TABLE user_clicks_sink (
user_id STRING,
total_clicks BIGINT,
PRIMARY KEY (user_id) NOT ENFORCED -- 必须声明主键,触发Upsert模式
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'user-clicks-result',
'properties.bootstrap.servers' = 'kafka:9092',
'key.format' = 'json',
'value.format' = 'json'
);
-- 写入聚合结果,自动处理更新覆盖
INSERT INTO user_clicks_sink
SELECT user_id, COUNT(*) FROM clicks GROUP BY user_id;
5.2.2 普通 Kafka(仅调试/中转)
原样输出完整 Changelog,保留 rowkind 字段,下游需自行解析变更语义。
plsql
CREATE TABLE debug_sink (
user_id STRING,
cnt BIGINT
) WITH (
'connector' = 'kafka',
'topic' = 'debug-changelog',
'format' = 'json' -- 输出包含rowkind的完整变更数据
);
INSERT INTO debug_sink
SELECT user_id, COUNT(*) FROM clicks GROUP BY user_id;
输出 JSON 示例:
json
{"rowkind":"+I","fields":["Alice",1]}
{"rowkind":"-U","fields":["Alice",1]}
{"rowkind":"+U","fields":["Alice",2]}
5.3 生产环境最佳实践
-
聚合、窗口、去重结果,优先使用 upsert-kafka / Hudi,规避复杂 Changelog 解析;
-
禁止将带更新删除的 Changelog 写入普通 Kafka、HDFS 等不支持更新的系统;
-
Upsert 类 Sink 必须显式定义
PRIMARY KEY; -
开发调试使用
toChangelogStream().print()观察真实变更类型; -
需要精准一次语义时,开启
'sink.semantic' = 'EXACTLY_ONCE'。
六、FlinkSQL 完整处理流程
一条流式 FlinkSQL 的完整执行链路分为三步,完美串联流、动态表、连续查询、Changelog 四大核心能力:
6.1 第一步:输入流 → 动态表
将无界输入流映射为逻辑动态表,流中每条数据默认是 +I 追加操作,构建 Append-only 初始动态表。该表为逻辑抽象,无物化存储。
6.2 第二步:动态表 → 连续查询计算
在动态表上执行 SQL 连续查询,基于状态增量计算,生成新的动态结果表。根据 SQL 逻辑不同,产生 Append-only 或 Update 类型 Changelog。
6.3 第三步:结果动态表 → 输出流
将计算后的动态结果表,通过三种编码方式(Append/Retract/Upsert)转换为可输出的 Changelog 数据流,写入外部 Sink。
七、全文总结
-
流批一体核心:流是动态表的 Changelog,动态表是流的逻辑抽象,实现流批 SQL 统一语义;
-
连续查询核心:增量计算、状态驱动、持续运行,输出动态变更结果;
-
Changelog 核心:四种 RowKind 定义表变更,三种编码方式适配不同场景;
-
生产最优解:无更新用 Append,有主键更新用 Upsert,无主键更新用 Retract;
-
Sink 适配核心:聚合结果优先 Upsert-Kafka/Hudi,杜绝 Changelog 与 Sink 语义不匹配。