4、Flink SQL与Table API:动态表、连续查询与CDC入湖入仓

说实话,Flink 的 Table API 和 SQL 是我这几年用得最多的接口。很多人觉得「SQL 嘛,谁不会写」,但在流处理里,事情没那么简单。

你想想看,传统数据库里一张表,数据是静止的。你查一次,结果就固定了。但在 Flink 里,数据一直在流,表的内容每时每刻都在变。那 SQL 怎么查?查出来的结果又怎么更新?

这就是 Flink 动态表要解决的核心问题。我当年第一次接触这个概念时,也绕了好一阵子。今天咱们就把这块彻底讲透。

4.1 动态表:流上的「快照」

动态表,说白了就是一张会随时间变化的关系表。每条新数据进来,表的内容就更新一次。你可以把它理解成数据库里的物化视图——只不过这个视图是实时刷新的。

核心要点:动态表是流与 SQL 之间的桥梁。流转换成动态表,SQL 查询动态表,结果再转换成新的流。

举个例子。假设你有一个订单流:

// 订单流样例
{user: "Alice", product: "手机", amount: 5000, time: "2024-01-01 10:00"}
{user: "Bob",   product: "电脑", amount: 8000, time: "2024-01-01 10:01"}
{user: "Alice", product: "耳机", amount: 200,  time: "2024-01-01 10:02"}

这个流转换成动态表后,每来一条记录,表里就多一行。但注意——动态表不存储所有历史数据,它只保留当前「可见」的数据窗口。

我个人习惯把动态表想象成「河流上的浮标」。河水在流,浮标的位置一直在变。你每次看,浮标都在新位置。动态表也一样,每次查询,结果都可能不同。

4.2 连续查询:一直在跑的 SQL

普通 SQL 查一次就结束了。连续查询不一样——它一直在跑,输入数据一变,输出结果就跟着变。

我举个例子你就明白了:

-- 连续查询示例:统计每个用户的订单总额
SELECT 
  user, 
  SUM(amount) AS total_amount
FROM orders
GROUP BY user;

这条 SQL 在传统数据库里跑一次,给你一个结果。但在 Flink 里,它会一直运行。Alice 每下一单,她的 total_amount 就更新一次。

这里有个坑,我曾经踩过——连续查询的结果也是动态表。也就是说,查询结果本身也在不断变化。那怎么把结果输出到外部系统?

Flink 提供了三种更新模式:

更新模式 说明 适用场景
Append 模式 只追加,不更新已有行 日志、事件流
Retract 模式 先发送一条删除消息,再发送一条插入消息 聚合查询、去重
Upsert 模式 根据 key 直接更新或插入 维表关联、CDC 输出

注意:不是所有 SQL 都支持所有模式。比如 GROUP BY 产生的更新,只能用 Retract 或 Upsert 模式。如果你用 Append 模式去接聚合结果,Flink 会直接报错。

4.3 维表 Join:流处理中的「查字典」

流处理里最头疼的问题之一,就是维表 Join。你想想看,订单流里只有 user_id,没有用户姓名、地址这些信息。你得去 MySQL 或者 Redis 里查。

但问题来了——流是无限的,维表数据是有限的。而且维表数据还会变。怎么保证 Join 的结果是对的?

Flink 提供了几种维表 Join 的方式:

  • 预加载维表:启动时一次性加载到内存。适合小维表,比如几百条配置数据。
  • 热缓存维表:用 Guava Cache 或 Caffeine,设置过期时间。适合中等规模维表。
  • 异步 IO 查询:每条数据都去查外部存储。适合大维表,但延迟较高。

我项目中用得最多的是热缓存维表。给你看一段代码:

-- 维表定义:从 MySQL 读取用户信息
CREATE TEMPORARY TABLE user_dim (
  user_id   STRING,
  user_name STRING,
  city      STRING,
  PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
  'connector' = 'jdbc',
  'url'      = 'jdbc:mysql://localhost:3306/db',
  'table-name' = 'users',
  'lookup.cache' = 'PARTIAL',   -- 热缓存模式
  'lookup.cache.max-rows' = '10000',
  'lookup.cache.ttl' = '10 minute'
);

-- 流表与维表 Join
SELECT 
  o.order_id,
  o.amount,
  u.user_name,
  u.city
FROM orders AS o
JOIN user_dim FOR SYSTEM_TIME AS OF o.proc_time AS u
ON o.user_id = u.user_id;

这里有个关键点——FOR SYSTEM_TIME AS OF。它告诉 Flink:在数据到达的那一刻,去查维表当时的值。这样即使维表数据变了,每条数据也能拿到它「应该」看到的版本。

我的经验:维表缓存时间别设太长。我曾经设了 30 分钟,结果用户改了地址,订单还是发到老地址去了。一般 5-10 分钟比较合理。如果对实时性要求极高,就用异步 IO 直查。

4.4 CDC 数据入湖入仓:从 Binlog 到数仓

CDC(Change Data Capture)是这几年最火的数据同步方式。说白了,就是监听数据库的 Binlog,把增删改操作实时同步到数仓或数据湖。

Flink 对 CDC 的支持非常成熟。你只需要一个 Flink CDC connector,就能把 MySQL、PostgreSQL、MongoDB 的数据实时同步到 Hive、Iceberg、Paimon 或者 Kafka。

我画了一张图,帮你理解 CDC 入湖入仓的整体流程:

CDC 数据入湖入仓流程 MySQL Binlog 增删改操作 PostgreSQL WAL 日志 Flink CDC 解析 Binlog 转换为 Changelog Exactly-Once 语义 数据湖 Iceberg / Paimon 数据仓库 Hive / ClickHouse 实时 报表 分析 关键说明: 1. Flink CDC 通过 Debezium 解析 Binlog,生成 INSERT / UPDATE / DELETE 事件流 2. 数据湖(Iceberg/Paimon)支持 ACID 事务,适合存储全量历史数据 3. 数据仓库(Hive/ClickHouse)适合 OLAP 分析,查询性能更优 4. 两者可以同时写入,实现「湖仓一体」架构

你看,Flink CDC 在中间扮演了「翻译官」的角色。它把数据库的 Binlog 翻译成 Flink 能理解的 Changelog 流,然后你可以用 SQL 随意处理——过滤、聚合、Join,最后写到任何地方。

给你看一个完整的 CDC 入湖示例:

-- 1. 定义 CDC 源表(监听 MySQL 的 orders 表)
CREATE TABLE cdc_orders (
  order_id   INT,
  user_id    INT,
  amount     DECIMAL(10,2),
  order_time TIMESTAMP(3),
  PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
  'connector' = 'mysql-cdc',
  'hostname'  = 'localhost',
  'port'      = '3306',
  'username'  = 'root',
  'password'  = '123456',
  'database-name' = 'mydb',
  'table-name'    = 'orders',
  'scan.startup.mode' = 'latest-offset'  -- 只从当前开始同步
);

-- 2. 定义 Iceberg 目标表(数据湖)
CREATE TABLE iceberg_orders (
  order_id   INT,
  user_id    INT,
  amount     DECIMAL(10,2),
  order_time TIMESTAMP(3),
  PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
  'connector' = 'iceberg',
  'catalog-database' = 'default',
  'catalog-table'    = 'orders',
  'format-version'   = '2',
  'write.upsert.enabled' = 'true'
);

-- 3. 启动同步作业
INSERT INTO iceberg_orders
SELECT * FROM cdc_orders;

避坑指南:我曾经在生产环境遇到过一个问题——CDC 作业重启后,数据重复了。原因是 scan.startup.mode 设成了 initial,每次重启都从 Binlog 开头读。后来改成 latest-offset 配合 checkpoint,才解决了。记住:CDC 作业一定要开启 checkpoint,否则 Exactly-Once 就是空话。

4.5 动态表与 CDC 的「化学反应」

动态表和 CDC 结合起来,能实现一个非常优雅的架构——实时数仓。

你想想看:

  • CDC 把数据库的变更实时同步到 Kafka
  • Flink SQL 消费 Kafka,用动态表做实时 ETL
  • 连续查询的结果写入数据湖或数据仓库

整个过程全是 SQL,不需要写一行 Java 代码。我去年帮一个电商客户做实时数仓,就是用这套方案。原来他们用 Spark 批处理,每天凌晨跑一次,数据延迟 12 小时。换成 Flink SQL 后,延迟降到 5 秒以内。

嗯,这里要注意——不是所有场景都适合 CDC。如果你的源表有大量的 DDL 操作(比如加列、改类型),CDC 作业可能会挂。Flink CDC 2.4 之后支持了 schema evolution,但还不够成熟。我建议你在生产环境先做充分的压力测试。

我的建议:如果你刚开始接触 Flink SQL,先从简单的 Append 流开始。比如把 Kafka 数据写到 HDFS,不要一上来就搞 CDC + 维表 Join + 聚合。等把动态表的概念吃透了,再逐步加复杂度。

动态表、连续查询、维表 Join、CDC 入湖入仓——这四个概念是 Flink SQL 的基石。理解了它们,你就能用 SQL 写出以前需要几百行 Java 代码才能实现的流处理逻辑。说白了,Flink 把流处理的复杂度藏在了 SQL 引擎里,让你用最熟悉的方式解决最复杂的问题。