用 Flink SQL 搭建一个实时统计应用Kafka → Flink → MySQL 实战

一、整体架构与数据流

先看一下整个数据流的"文字架构图":

  1. 订单系统

    把用户下单事件写入 Kafka Topic:orders,消息格式使用 JSON。

  2. Flink SQL 作业(本篇主角)

    • 在 SQL Client 中用 DDL 定义:

      • orders_kafka:Kafka 源表;
      • order_minute_stats:MySQL 汇总表;
    • 写一条 INSERT INTO ... SELECT ... GROUP BY 的连续查询:

      • 以事件时间为准做滚动窗口(每分钟);
      • 统计每个商品的下单数、GMV 等指标;
      • 实时 Upsert 到 MySQL。
  3. MySQL / BI 报表

    直接查询 order_minute_stats,即可看到实时统计结果。

二、环境准备

这里假设你本地已经装好了:

  • JDK 8+
  • Apache Flink(如 1.15+,更高也 OK)
  • Kafka(2.x / 3.x)
  • MySQL(或别的 OLAP,也可以换成 Doris / ClickHouse 等)

在 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"}

回到 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 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;

解释一下这条语句里用到的几个关键点:

  1. TUMBLE Window TVF

    • TABLE(TUMBLE(TABLE orders_kafka, DESCRIPTOR(order_time), INTERVAL '1' MINUTE))
    • 表示按 order_time 做 1 分钟滚动窗口;
    • 返回的一张虚拟表里带 window_start / window_end 这两个字段。
  2. GROUP BY window_start, window_end, item_id

    • 对每个窗口 + 商品聚合;
    • 统计订单数:COUNT(*)
    • 统计 GMV:SUM(price * quantity)
  3. 连续查询

    • 这条 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,例如:

    yaml 复制代码
    execution.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 做下游实时推荐 / 风控。

十、小结

回顾一下本文我们做了什么:

  1. 用 Kafka 承接订单事件,定义了 JSON 格式;

  2. 在 Flink SQL Client 里:

    • 用 DDL 定义了 orders_kafka 源表(带事件时间和 Watermark);
    • 在 MySQL 定义 order_minute_stats 结果表,并通过 JDBC connector 建立 Sink 表;
  3. 写了一条 INSERT INTO ... SELECT ... 连续查询:

    • 基于 TUMBLE 滚动窗口;
    • 按 "窗口 + 商品" 聚合下单数和 GMV;
    • 实时 Upsert 到 MySQL。
  4. 在 MySQL 中验证了实时统计结果。

这一套走通之后,你基本就具备了:

  • 纯 SQL 写出一个完整流式应用的能力;
  • 把 Flink SQL 当作"流式数仓 / 实时 ETL 引擎"来用的基础。
相关推荐
路边草随风1 小时前
java 实现 flink 读 kafka 写 delta
java·大数据·flink·kafka
zzhongcy1 小时前
RocketMQ、Kafka 和 RabbitMQ 等中间件对比
kafka·rabbitmq·rocketmq
写bug的小屁孩1 小时前
2.Kafka-命令行操作、两种消息模型
分布式·kafka
路边草随风1 小时前
java 实现 flink 读 kafka 写 paimon
java·大数据·flink·kafka
Hello.Reader2 小时前
Flink SQL 查询(Queries)从 sqlQuery 到 executeSql
sql·flink·linq
Han.miracle2 小时前
数据库圣经--Alter & 视图
数据库·sql·视图
bing.shao2 小时前
Golang 链接kafka 设置SASL_PLAINTEXT安全协议
分布式·安全·kafka
路边草随风2 小时前
java 实现 flink 读 kafka 写 iceberg
java·flink·kafka
路边草随风2 小时前
java 实现 flink cdc 读 mysql binlog 按表写入kafka不同topic
java·大数据·mysql·flink