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 之间的关系。

Flink SQL 开发核心知识体系 数据源(Kafka / MySQL) 动态表(Dynamic Table) 连续查询(Continuous Query) 维表 Join 窗口聚合 CDC 数据同步 三者共享 Flink 的状态后端与 Checkpoint 机制

这张图你看懂了吗?数据从 Kafka 或 MySQL 流入,变成动态表,然后连续查询在上面运行。根据业务需求,你可以选择维表 Join 做实时关联、窗口聚合做时间维度的统计、或者 CDC 做数据库变更的实时同步。

这三个分支不是互斥的,实际项目中经常组合使用。比如:CDC 同步订单数据 → 维表 Join 关联商品信息 → 窗口聚合统计每小时销售额。一条 SQL 链路搞定。

总结一下:动态表是 Flink SQL 的基石,连续查询是运行机制,维表 Join、窗口聚合、CDC 是三大实战模式。掌握了这些,你就能应对 80% 的流批一体开发场景。