一、整体架构与数据流
先看一下整个数据流的"文字架构图":
-
订单系统
把用户下单事件写入 Kafka Topic:
orders,消息格式使用 JSON。 -
Flink SQL 作业(本篇主角)
-
在 SQL Client 中用 DDL 定义:
orders_kafka:Kafka 源表;order_minute_stats:MySQL 汇总表;
-
写一条 INSERT INTO ... SELECT ... GROUP BY 的连续查询:
- 以事件时间为准做滚动窗口(每分钟);
- 统计每个商品的下单数、GMV 等指标;
- 实时 Upsert 到 MySQL。
-
-
MySQL / BI 报表
直接查询
order_minute_stats,即可看到实时统计结果。
二、环境准备
这里假设你本地已经装好了:
- JDK 8+
- Apache Flink(如 1.15+,更高也 OK)
- Kafka(2.x / 3.x)
- MySQL(或别的 OLAP,也可以换成 Doris / ClickHouse 等)
2.1 启动 Flink 本地集群
在 Flink 安装目录执行:
bash
./bin/start-cluster.sh
浏览器访问:http://localhost:8081,可以看到 Flink Web UI。
2.2 启动 SQL Client
同样在 Flink 安装目录:
bash
./bin/sql-client.sh
进入后会看到一个 Flink SQL> 的交互式提示符,后续所有 SQL 都在这里敲。
三、准备 Kafka 订单 Topic 与示例数据
先创建一个 Topic:
bash
kafka-topics.sh --create \
--bootstrap-server localhost:9092 \
--topic orders \
--partitions 3 \
--replication-factor 1
假设订单 JSON 格式如下(你可以按自己业务改):
json
{
"order_id": "O100001",
"user_id": 123,
"item_id": "I_001",
"item_name": "耳机",
"price": 199.0,
"quantity": 2,
"order_time": "2025-12-03 10:01:23"
}
用控制台 producer 写点测试数据(示例):
bash
kafka-console-producer.sh \
--broker-list localhost:9092 \
--topic orders
然后粘几条 JSON:
text
{"order_id":"O1","user_id":1,"item_id":"I_001","item_name":"耳机","price":199.0,"quantity":1,"order_time":"2025-12-03 10:01:00"}
{"order_id":"O2","user_id":2,"item_id":"I_001","item_name":"耳机","price":199.0,"quantity":2,"order_time":"2025-12-03 10:01:30"}
{"order_id":"O3","user_id":3,"item_id":"I_002","item_name":"键盘","price":299.0,"quantity":1,"order_time":"2025-12-03 10:02:10"}
四、在 Flink SQL 中定义 Kafka 源表
回到 SQL Client,先选一个 Catalog / Database(默认就行),然后建表:
sql
CREATE TABLE orders_kafka (
order_id STRING,
user_id BIGINT,
item_id STRING,
item_name STRING,
price DOUBLE,
quantity INT,
order_time TIMESTAMP(3),
-- 事件时间字段 + watermark 策略(延迟 5 秒)
WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'orders',
'properties.bootstrap.servers' = 'localhost:9092',
'properties.group.id' = 'flink-sql-orders',
'scan.startup.mode' = 'latest-offset', -- demo 用 latest,线上可使用 group-offset / timestamp
'format' = 'json',
'json.ignore-parse-errors' = 'true'
);
要点说明:
- 使用
order_time TIMESTAMP(3)+WATERMARK来声明 事件时间; scan.startup.mode='latest-offset'表示从最新位置开始消费,适合测试;json.ignore-parse-errors='true'避免脏数据直接干掉作业。
可以先试一把:
sql
SELECT * FROM orders_kafka;
如果你已经往 Kafka 写过数据,SQL Client 会实时打印出来。
五、在 MySQL 中定义汇总结果表
我们打算按"分钟 + 商品"维度做聚合,先在 MySQL 建一张表:
sql
CREATE DATABASE IF NOT EXISTS realtime_demo CHARACTER SET utf8mb4;
USE realtime_demo;
CREATE TABLE IF NOT EXISTS order_minute_stats (
window_start DATETIME NOT NULL,
window_end DATETIME NOT NULL,
item_id VARCHAR(64) NOT NULL,
item_name VARCHAR(255) NOT NULL,
order_cnt BIGINT NOT NULL,
total_amount DOUBLE NOT NULL,
primary key (window_start, item_id) -- 用于 upsert
);
注意:
这里我们用
(window_start, item_id)做主键,便于 Flink 通过 JDBC Upsert 覆盖同一个窗口+商品的统计值。
六、在 Flink SQL 中定义 MySQL Sink 表
回到 Flink SQL Client,用 JDBC connector 建立一张 sink 表,字段与 MySQL 一致:
sql
CREATE TABLE order_minute_stats (
window_start TIMESTAMP(3),
window_end TIMESTAMP(3),
item_id STRING,
item_name STRING,
order_cnt BIGINT,
total_amount DOUBLE,
PRIMARY KEY (window_start, item_id) NOT ENFORCED
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://localhost:3306/realtime_demo?useSSL=false&serverTimezone=UTC',
'table-name' = 'order_minute_stats',
'username' = 'root',
'password' = 'your_password'
);
要点说明:
- Flink 里用
PRIMARY KEY ... NOT ENFORCED标记主键(语义层,Flink 不做约束检查); - JDBC connector 会根据主键做 Upsert(INSERT/UPDATE);
- 确保 MySQL 连接参数正确。
七、核心 SQL:按分钟 + 商品实时聚合
现在我们来写最关键的一条 SQL,将 Kafka 源表聚合后写入 MySQL:
sql
INSERT INTO order_minute_stats
SELECT
window_start,
window_end,
item_id,
-- 这里用 max(item_name) 是为了聚合时保留商品名(假设同一 item_id 名称一致)
MAX(item_name) AS item_name,
COUNT(*) AS order_cnt,
SUM(price * quantity) AS total_amount
FROM TABLE(
TUMBLE(TABLE orders_kafka, DESCRIPTOR(order_time), INTERVAL '1' MINUTE)
)
GROUP BY window_start, window_end, item_id;
解释一下这条语句里用到的几个关键点:
-
TUMBLE Window TVF
TABLE(TUMBLE(TABLE orders_kafka, DESCRIPTOR(order_time), INTERVAL '1' MINUTE))- 表示按
order_time做 1 分钟滚动窗口; - 返回的一张虚拟表里带
window_start/window_end这两个字段。
-
GROUP BY window_start, window_end, item_id
- 对每个窗口 + 商品聚合;
- 统计订单数:
COUNT(*); - 统计 GMV:
SUM(price * quantity)。
-
连续查询
- 这条
INSERT提交后会开启一个 永不结束的流作业; - 每分钟窗口计算完就 Upsert 一条结果到 MySQL 的
order_minute_stats表中。
- 这条
你在 SQL Client 输入这条语句后,会看到类似:
text
[INFO] Submitting SQL update statement to the cluster...
[INFO] Job ID: xxxxxxxx
这个作业是"后台运行"的,你可以去 Flink Web UI(localhost:8081)里查看任务的状态 / 水位线等信息。
八、验证结果:查看 MySQL 实时统计
此时向 Kafka 持续写订单数据,例如(时间间隔不同):
text
{"order_id":"O1","user_id":1,"item_id":"I_001","item_name":"耳机","price":199.0,"quantity":1,"order_time":"2025-12-03 10:01:00"}
{"order_id":"O2","user_id":2,"item_id":"I_001","item_name":"耳机","price":199.0,"quantity":2,"order_time":"2025-12-03 10:01:30"}
{"order_id":"O3","user_id":3,"item_id":"I_002","item_name":"键盘","price":299.0,"quantity":1,"order_time":"2025-12-03 10:02:10"}
{"order_id":"O4","user_id":4,"item_id":"I_001","item_name":"耳机","price":199.0,"quantity":3,"order_time":"2025-12-03 10:02:40"}
等到每个 1 分钟窗口结束(考虑 watermark 延迟),Flink 会把统计结果写入 MySQL。
在 MySQL 里查询:
sql
SELECT * FROM order_minute_stats ORDER BY window_start, item_id;
你会看到类似结果(具体时间按你数据为准):
text
+---------------------+---------------------+---------+----------+-----------+--------------+
| window_start | window_end | item_id | item_name| order_cnt | total_amount |
+---------------------+---------------------+---------+----------+-----------+--------------+
| 2025-12-03 10:01:00 | 2025-12-03 10:02:00 | I_001 | 耳机 | 2 | 597.0 |
| 2025-12-03 10:02:00 | 2025-12-03 10:03:00 | I_001 | 耳机 | 1 | 597.0 |
| 2025-12-03 10:02:00 | 2025-12-03 10:03:00 | I_002 | 键盘 | 1 | 299.0 |
+---------------------+---------------------+---------+----------+-----------+--------------+
注意:
具体数值取决于你 price×quantity 的计算以及数据分布,以上只是示意。
到这里,一个完整的 Kafka → Flink SQL → MySQL 实时统计应用 就跑起来了。
九、补充:生产环境需要注意的几点
上面的例子更偏"入门 demo",真上生产还需要考虑几件事:
9.1 Checkpoint & 容错语义
-
在
conf/flink-conf.yaml中开启 checkpoint,例如:yamlexecution.checkpointing.interval: 1min execution.checkpointing.mode: EXACTLY_ONCE -
Kafka Source 推荐使用
scan.startup.mode = 'group-offsets',配合 checkpoint,保证 端到端 Exactly-once 或 At-least-once 语义; -
JDBC Sink 如需严格 Exactly-once,一般要配合幂等主键 / 去重策略 或者换成更适合流写的 OLAP(比如 Doris / StarRocks)。
9.2 维度信息 & 维表 Join
实际业务中,订单里可能只有 item_id,但报表希望看到更多维度(类目、品牌等),可以通过 Temporal Table Join 联一张 MySQL 维表,例如:
- 建一张
dim_item表,存放商品维度; - 在 Flink SQL 里通过 Lookup Join 把维度信息"富化"到统计结果里。
这部分你可以结合前面 "Temporal Table / Versioned Table" 那几篇再写一篇专门的博客。
9.3 多 Sink / 多指标
-
同一条 Kafka 源,可以分成多条 INSERT:
- 一条按分钟聚合写 MySQL;
- 一条明细或者更细粒度的统计写到 Doris/ClickHouse;
- 另一条写到 Kafka 做下游实时推荐 / 风控。
十、小结
回顾一下本文我们做了什么:
-
用 Kafka 承接订单事件,定义了 JSON 格式;
-
在 Flink SQL Client 里:
- 用 DDL 定义了
orders_kafka源表(带事件时间和 Watermark); - 在 MySQL 定义
order_minute_stats结果表,并通过 JDBC connector 建立 Sink 表;
- 用 DDL 定义了
-
写了一条 INSERT INTO ... SELECT ... 连续查询:
- 基于
TUMBLE滚动窗口; - 按 "窗口 + 商品" 聚合下单数和 GMV;
- 实时 Upsert 到 MySQL。
- 基于
-
在 MySQL 中验证了实时统计结果。
这一套走通之后,你基本就具备了:
- 用 纯 SQL 写出一个完整流式应用的能力;
- 把 Flink SQL 当作"流式数仓 / 实时 ETL 引擎"来用的基础。