4. Flink SQL 开发:动态表、连续查询与实战模式
Flink SQL 的开发,说白了就是把批处理和流处理统一到一张表上。很多刚接触的同学会问:「SQL 不是静态的吗?流数据一直在变,怎么用 SQL 查?」
嗯,这里的关键就是 动态表 和 连续查询 这两个概念。我最早接触时也觉得抽象,后来在做一个实时大屏项目时,才真正体会到它们的威力。
4.1 动态表:流上的关系视图
传统数据库的表是静态的——你查的时候数据就在那里。Flink 的动态表不一样,它是一条不断追加、更新数据的「活」表。
你可以把动态表想象成一个实时更新的 Excel 表格:新数据来了,行就增加;旧数据变了,行就修改。Flink 在背后维护这个表的完整变更日志(Changelog)。
核心要点:动态表 = 流数据 + 关系代数。每条流入的数据都会触发一次表的更新,而你的 SQL 查询就在这个不断变化的表上持续运行。
我在项目中遇到过一种常见误解:有人以为动态表会把所有历史数据都存下来。其实不是,Flink 只保留状态中需要的数据,超出窗口或 key 范围的数据会被自动清理。
4.2 连续查询:一次提交,持续计算
你写一条 SQL,提交到 Flink 集群后,它不会像传统数据库那样查完就结束。它会一直运行,每来一条新数据就重新计算一次结果。这就是连续查询。
举个例子:
-- 统计每个商品的实时销售额
SELECT
product_id,
SUM(amount) AS total_sales
FROM orders
GROUP BY product_id;
这条 SQL 在 Flink 中会变成一个长期运行的流任务。每来一条订单,对应商品的累计销售额就更新一次。结果表也是动态的,你可以把它实时写到数据库或推送到前端。
我的经验:连续查询的难点在于状态管理。如果 key 的基数很大(比如几亿个商品 ID),状态会膨胀。我建议在开发初期就估算好状态大小,必要时开启 RocksDB 状态后端。
4.3 维表 Join:实时关联外部数据
流处理中经常需要把实时流和静态(或缓慢变化)的维度数据关联起来。比如订单流关联商品信息、用户信息。
Flink SQL 支持三种维表 Join 模式:
| 模式 | 适用场景 | 我踩过的坑 |
|---|---|---|
| Lookup Join | 实时查询外部存储(Redis、MySQL、HBase) | 缓存过期时间设太短,导致频繁查库,QPS 打满 |
| Temporal Join | 关联版本化数据(如汇率变化表) | 忘记指定事件时间属性,结果全是乱序 |
| Regular Join | 双流 Join,两边都是动态表 | 状态不清理,内存 OOM |
我最常用的是 Lookup Join。举个例子:
CREATE TABLE orders (
order_id BIGINT,
product_id BIGINT,
amount DECIMAL(10,2),
proctime AS PROCTIME()
) WITH (
'connector' = 'kafka',
'topic' = 'orders',
'format' = 'json'
);
CREATE TABLE products (
product_id BIGINT,
product_name STRING,
price DECIMAL(10,2),
PRIMARY KEY (product_id) NOT ENFORCED
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://localhost:3306/mydb',
'table-name' = 'products'
);
-- Lookup Join 查询
SELECT
o.order_id,
p.product_name,
o.amount * p.price AS total
FROM orders AS o
JOIN products FOR SYSTEM_TIME AS OF o.proctime AS p
ON o.product_id = p.product_id;
避坑指南:我曾经在生产环境中把维表 Join 的缓存设成「永不超时」,结果商品信息更新后,实时订单还在用旧数据算价格,导致财务对账差了十几万。后来我改成 TTL = 5 分钟 + 主动刷新,才解决问题。
4.4 窗口聚合:时间维度的计算利器
流处理中大部分聚合都是基于时间窗口的。Flink SQL 支持三种窗口类型:
- 滚动窗口(Tumble):固定大小,不重叠。比如每 5 分钟统计一次。
- 滑动窗口(Hop):固定大小,可重叠。比如每 1 分钟统计过去 5 分钟的数据。
- 会话窗口(Session):基于不活动间隔划分。比如用户连续操作,超过 10 分钟没动作就切一个窗口。
写个滚动窗口的例子:
SELECT
TUMBLE_START(event_time, INTERVAL '5' MINUTE) AS window_start,
TUMBLE_END(event_time, INTERVAL '5' MINUTE) AS window_end,
product_id,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY
TUMBLE(event_time, INTERVAL '5' MINUTE),
product_id;
你想想看,如果没有窗口,流数据怎么聚合?数据源源不断,你永远等不到「结束」。窗口给了流一个「边界」,让聚合变得可能。
我的习惯:窗口大小不要设得太小(比如 1 秒),否则频繁触发计算,压力很大。也不要用太大(比如 24 小时),状态会撑爆。一般 1 分钟到 1 小时是比较合理的范围。
4.5 CDC 数据同步:实时捕获数据库变更
CDC(Change Data Capture)是流批一体中非常实用的能力。你可以把 MySQL、PostgreSQL 的 binlog 实时同步到 Flink,然后做 ETL、入湖、入仓。
Flink SQL 通过 cdc-connector 实现:
CREATE TABLE mysql_orders (
order_id BIGINT,
product_id BIGINT,
amount DECIMAL(10,2),
order_time TIMESTAMP(3),
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'localhost',
'port' = '3306',
'username' = 'flink',
'password' = 'flink',
'database-name' = 'mydb',
'table-name' = 'orders'
);
-- 直接查询 CDC 表,相当于实时读取 MySQL 变更
SELECT * FROM mysql_orders;
CDC 的核心优势是:全量 + 增量一体化。启动任务时先做一次全量快照,然后自动切换到增量 binlog 监听。整个过程对业务无侵入。
我曾经踩过的坑:CDC 任务重启后,binlog 位置丢失导致数据重复或丢失。后来我强制开启了 checkpoint,并设置 scan.startup.mode = 'latest-offset' 或 'initial',才稳定下来。
4.6 知识体系总览
下面这张图把本章的核心概念串起来了。你可以看到动态表、连续查询、维表 Join、窗口聚合和 CDC 之间的关系。
这张图你看懂了吗?数据从 Kafka 或 MySQL 流入,变成动态表,然后连续查询在上面运行。根据业务需求,你可以选择维表 Join 做实时关联、窗口聚合做时间维度的统计、或者 CDC 做数据库变更的实时同步。
这三个分支不是互斥的,实际项目中经常组合使用。比如:CDC 同步订单数据 → 维表 Join 关联商品信息 → 窗口聚合统计每小时销售额。一条 SQL 链路搞定。
总结一下:动态表是 Flink SQL 的基石,连续查询是运行机制,维表 Join、窗口聚合、CDC 是三大实战模式。掌握了这些,你就能应对 80% 的流批一体开发场景。