Flink 系列第18篇:Flink 动态表、连续查询与 Changelog 机制

一、概述

动态表(Dynamic Table)和连续查询(Continuous Query)是 Flink Table API / SQL 实现流批统一标准关系代数语义的两大核心理论基础。

其核心思想:将无限、无界的流式数据,映射为一张随时间不断变化的逻辑表,让用户可以直接使用标准 SQL 对流数据进行查询、聚合、关联等操作,完美对齐批处理的 SQL 使用习惯。

整套机制分为三层核心能力:

  1. 动态输入表技术:将实时输入数据流,映射为 SQL 可识别的动态输入表;

  2. 连续查询技术:在动态表上执行持续计算,映射标准 SQL 运算语义;

  3. 动态输出表技术:将计算后的动态结果表,反向转换为可输出的数据流。

二、动态表(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 核心特性

  1. 增量计算:不重复计算全量数据,仅根据新输入数据增量更新状态和结果,每一次输出都是最新的中间结果;

  2. 状态驱动 :聚合、分组、连接等算子会维护状态,例如 GROUP BY 会为每个 Key 单独维护聚合结果;

  3. 完善的时间语义:原生支持事件时间、处理时间,支持滚动、滑动、会话等多种窗口类型。

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);

执行过程

  1. 输入层clicks 动态表持续接收用户点击流,不断追加新数据;

  2. 计算层 :连续查询按 user_id + 1 小时滚动窗口分组,为每个 (user_id, window) 组合维护 count 聚合状态;

  3. 输出层:窗口水位线超过窗口结束时间后,触发窗口计算,输出最终结果。

输出结果示例

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 生产环境最佳实践

  1. 聚合、窗口、去重结果,优先使用 upsert-kafka / Hudi,规避复杂 Changelog 解析;

  2. 禁止将带更新删除的 Changelog 写入普通 Kafka、HDFS 等不支持更新的系统;

  3. Upsert 类 Sink 必须显式定义 PRIMARY KEY

  4. 开发调试使用 toChangelogStream().print() 观察真实变更类型;

  5. 需要精准一次语义时,开启 '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。

七、全文总结

  1. 流批一体核心:流是动态表的 Changelog,动态表是流的逻辑抽象,实现流批 SQL 统一语义;

  2. 连续查询核心:增量计算、状态驱动、持续运行,输出动态变更结果;

  3. Changelog 核心:四种 RowKind 定义表变更,三种编码方式适配不同场景;

  4. 生产最优解:无更新用 Append,有主键更新用 Upsert,无主键更新用 Retract;

  5. Sink 适配核心:聚合结果优先 Upsert-Kafka/Hudi,杜绝 Changelog 与 Sink 语义不匹配。

相关推荐
aXin_ya2 小时前
微服务(高级) 8
java·数据库·微服务
绿草在线2 小时前
03.JakartaEE11+Thymeleaf实现图书列表功能
java
逻辑驱动的ken2 小时前
Java高频面试考点场景题15
java·开发语言·深度学习·面试·职场和发展·高效学习
juniperhan2 小时前
Flink 系列第19篇:深入理解 Flink SQL 的时间语义与时区处理:从原理到实战
java·大数据·数据仓库·分布式·sql·flink
是有头发的程序猿2 小时前
AI agent电商运营成本管控:1688运费核算及自动下单付款Python实操教程
大数据·开发语言
这是程序猿2 小时前
MySQL 索引一篇讲透:原理、分类、优化与面试总结
java·前端·mysql
珠海西格电力2 小时前
零碳园区管理系统“云-边-端”架构协同的核心价值
大数据·人工智能·分布式·微服务·架构·能源
无籽西瓜a2 小时前
MD5算法原理、适用场景
java·后端·算法·哈希算法·md5
帅次2 小时前
Android 高级工程师面试速记版
android·java·面试·kotlin·binder·zygote·android runtime