Flink SQL 流批一体:动态表、连续查询与流表对偶性
这一章,我们来聊聊 Flink SQL 流批一体的核心理论。说实话,很多初学者刚接触动态表、连续查询这些概念时,会觉得有点绕。我当年第一次看官方文档,也愣了半天。
但这些东西,恰恰是理解 Flink SQL 为什么能同时处理流和批的关键。你搞懂了它们,后面写 SQL 时心里就有底了。
从传统 SQL 到流式 SQL 的思维转变
传统数据库里,表是静态的。你查它,数据就在那,不会变。但流数据是源源不断进来的,你没法等它“结束”。
那怎么办?Flink 的思路很巧妙——把流当成一张不断追加数据的表。这就是动态表的由来。
核心思想:流是表的无限版本,表是流的某个时刻快照。
动态表(Dynamic Table)
动态表,说白了就是一张会随时间变化的关系表。每条新数据进来,表的内容就更新一次。
举个例子。假设你有一个电商订单流:
-- 订单数据流(JSON 格式)
{"order_id": 1001, "user_id": "u01", "amount": 50.0, "ts": "2024-01-01 10:00:00"}
{"order_id": 1002, "user_id": "u02", "amount": 30.0, "ts": "2024-01-01 10:01:00"}
{"order_id": 1003, "user_id": "u01", "amount": 20.0, "ts": "2024-01-01 10:02:00"}
在 Flink SQL 里,你可以直接把它注册成一张动态表:
CREATE TABLE orders (
order_id BIGINT,
user_id STRING,
amount DOUBLE,
ts TIMESTAMP(3),
WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'orders',
'format' = 'json'
);
这张表,每来一条 Kafka 消息,就多一行数据。它没有终点。
我的习惯:定义动态表时,一定要加上 WATERMARK。否则乱序数据会让你查出来的结果忽大忽小。我曾经在生产环境吃过这个亏,后来每条流表都强制加水位线。
连续查询(Continuous Query)
动态表上跑的 SQL,就是连续查询。它不会终止,会一直输出结果。
看一个简单的分组聚合:
SELECT
user_id,
COUNT(*) AS order_cnt,
SUM(amount) AS total_amount
FROM orders
GROUP BY user_id;
这个查询,每来一条新订单,结果就会更新一次。你想想看,传统 SQL 里 GROUP BY 是一次性算完,但这里它是持续计算的。
连续查询有两种输出方式:
- 更新(Update)模式:结果表会不断更新已有行。比如上面那个分组查询,u01 的订单数从 1 变成 2,结果表里 u01 那行就更新一次。
- 追加(Append)模式:结果表只增不减。比如纯过滤查询
SELECT * FROM orders WHERE amount > 100,每条符合条件的记录直接追加到结果表。
避坑指南:我曾经遇到一个案例,同事用 GROUP BY 做实时大屏,结果下游的 MySQL 表主键没设对,导致更新冲突。连续查询的更新模式,要求下游存储支持 UPSERT。Kafka 这种只追加的存储,就别指望它能处理更新了。
流表对偶性(Stream-Table Duality)
这个概念,是 Flink SQL 流批一体的理论基石。它讲的是:
- 流可以看作表:把每条流记录当成表的一行 INSERT。
- 表可以看作流:表的每次变更(INSERT、UPDATE、DELETE)都可以转成一条变更日志流(Changelog Stream)。
说白了,流和表是同一枚硬币的两面。你从流的角度看,数据是动态的;从表的角度看,数据是静态的。
Flink 内部用 Changelog 机制 来统一这两者。每条数据都带一个标记:
| RowKind | 含义 | 对应 SQL 操作 |
|---|---|---|
| INSERT | 新增一行 | INSERT |
| UPDATE_BEFORE | 更新前的旧值 | UPDATE |
| UPDATE_AFTER | 更新后的新值 | UPDATE |
| DELETE | 删除一行 | DELETE |
你看,批处理里的 UPDATE 和 DELETE,在流处理里就变成了带 RowKind 的变更事件。这就是流表对偶性的具体实现。
核心逻辑:一张图看懂
下面这张图,把动态表、连续查询、流表对偶性的关系串起来了。我建议你多看几遍。
流批一体的 SQL 写法
有了上面的理论基础,流批一体的 SQL 写法就顺理成章了。同一套 SQL,既能跑在流上,也能跑在批上。
看一个实际例子。统计每个用户的订单总额:
-- 流模式:实时计算
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id;
-- 批模式:回溯历史
-- 只需要把 orders 表换成批数据源
SELECT user_id, SUM(amount) AS total
FROM orders_batch
GROUP BY user_id;
两段 SQL 几乎一样。区别只在于数据源是流还是批。Flink 在运行时,会根据数据源自动选择执行模式。
我建议:开发阶段先用批模式调试 SQL 逻辑,确认无误后再切到流模式上线。这样能省掉很多实时调试的麻烦。我在好几个项目里都是这么干的,效率提升很明显。
状态管理与容错
连续查询是有状态的。比如上面的 SUM,它得记住每个用户当前的累计值。这个状态,Flink 用 状态后端 来管理。
常见的状态后端:
- HashMapStateBackend:存在 JVM 堆内存,适合小状态、高吞吐场景。
- RocksDBStateBackend:存在本地磁盘,适合大状态、需要容错的场景。
容错靠的是 Checkpoint。Flink 定期把状态快照保存到持久化存储(比如 HDFS)。一旦作业失败,就从最近的 Checkpoint 恢复。
我曾经踩过的坑:有一次状态后端配了 HashMap,结果状态涨到几十 GB,直接 OOM 了。后来换成 RocksDB,配合增量 Checkpoint,才稳住。大状态场景,别省那点配置时间。
总结一下
这一章的核心,就三句话:
- 动态表把流包装成表,让 SQL 能处理无限数据。
- 连续查询让 SQL 持续计算,结果实时更新。
- 流表对偶性用 Changelog 统一了流和批的底层表示。
你理解了这三句话,Flink SQL 流批一体的设计哲学就抓住了。后面讲具体算子、优化策略时,你会发现自己上手特别快。