一、场景设定
我们设计一个非常经典的小案例:
-
输入: Kafka Topic
employee_information,格式 JSON -
字段:
emp_id:员工 ID(INT)name:员工姓名(STRING)dept_id:部门 ID(INT)
-
需求:
- 实时统计各个部门当前有多少员工;
- 结果写回到另一个 Kafka Topic
department_counts(Upsert 流),方便下游报表 / 大屏消费。
整体结构就是:
Kafka → Flink SQL → Kafka
二、环境假设
先假定你本地已经有:
- Flink 1.13+(推荐 1.15+ 甚至 1.18+)
- Kafka 单机(本地 9092)
- Flink 已经把
flink-sql-connector-kafka-*.jar放到lib/目录(或 SQL Client 通过-j显式加载)。
然后先启动:
bash
# 启动 Flink 集群
./bin/start-cluster.sh
# 另开一个终端:启动 SQL Client
./bin/sql-client.sh
Kafka 那边你可以先创建两个 Topic(示例命令):
bash
# 员工信息 Topic
kafka-topics.sh --bootstrap-server localhost:9092 \
--create --topic employee_information --partitions 1 --replication-factor 1
# 部门统计 Topic
kafka-topics.sh --bootstrap-server localhost:9092 \
--create --topic department_counts --partitions 1 --replication-factor 1
三、定义源表:Kafka → Flink 源表
我们用 JSON 作为消息格式,示例消息长这样:
json
{"emp_id": 1, "name": "Alice", "dept_id": 1}
{"emp_id": 2, "name": "Bob", "dept_id": 1}
{"emp_id": 3, "name": "Cindy", "dept_id": 2}
在 SQL Client 里执行下面 DDL,把这个 Topic 映射成一张动态表:
sql
-- 源表:员工信息(Kafka JSON)
CREATE TABLE employee_information (
emp_id INT,
name STRING,
dept_id INT,
-- 可以顺带声明一个处理时间,后面如果要做窗口统计会用到
proc_time AS PROCTIME()
) WITH (
'connector' = 'kafka',
'topic' = 'employee_information',
'properties.bootstrap.servers' = 'localhost:9092',
'properties.group.id' = 'flink-sql-employee-group',
'scan.startup.mode' = 'earliest-offset',
'format' = 'json',
'json.ignore-parse-errors' = 'true'
);
验证一下表能不能读到数据,可以先手动塞几条:
bash
kafka-console-producer.sh --broker-list localhost:9092 \
--topic employee_information
# 然后输入几行 JSON,每行一条
{"emp_id":1,"name":"Alice","dept_id":1}
{"emp_id":2,"name":"Bob","dept_id":1}
{"emp_id":3,"name":"Cindy","dept_id":2}
SQL Client 中跑:
sql
SELECT * FROM employee_information;
如果有数据流出来,就 OK 了。
四、定义结果表(Sink):部门人数统计(Upsert-Kafka)
部门人数是一个「按部门去重聚合」的结果,每个 dept_id 对应一个最新的 emp_count ,非常适合用 Upsert-Kafka:
sql
-- 结果表:部门人数统计(Upsert-Kafka JSON)
CREATE TABLE department_counts (
dept_id INT,
emp_count BIGINT,
PRIMARY KEY (dept_id) NOT ENFORCED
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'department_counts',
'properties.bootstrap.servers' = 'localhost:9092',
'key.format' = 'json',
'key.json.ignore-parse-errors' = 'true',
'value.format' = 'json',
'value.json.ignore-parse-errors' = 'true'
);
这里
PRIMARY KEY (dept_id) NOT ENFORCED的含义是:
- Flink 认为
dept_id是唯一键,用它来生成 Upsert 流;- 但不会对数据做强约束检查(NOT ENFORCED),性能更好。
如果你只是想先本地看结果,也可以额外建一个 print 表调试:
sql
CREATE TABLE department_counts_print (
dept_id INT,
emp_count BIGINT
) WITH (
'connector' = 'print'
);
五、实时统计 SQL:按部门人数聚合
核心逻辑其实就一条 SQL:
sql
INSERT INTO department_counts
SELECT
dept_id,
COUNT(*) AS emp_count
FROM employee_information
GROUP BY dept_id;
语义上是:
- 随着
employee_information不断有新员工数据写入; - Flink 实时维护每个
dept_id的COUNT(*); - 每次计数变化就以 Upsert 形式写入
department_countsKafka Topic。
如果你也建了 print 表,可以临时这样跑来对比:
sql
INSERT INTO department_counts_print
SELECT
dept_id,
COUNT(*) AS emp_count
FROM employee_information
GROUP BY dept_id;
六、下游消费:实时查看聚合结果
6.1 用 Kafka 控制台消费 Upsert 流
Upsert-Kafka 的 key 是 dept_id,value 里只有 emp_count:
bash
kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic department_counts \
--from-beginning \
--property print.key=true \
--property key.separator=:
当你继续往 employee_information 写入数据,比如:
bash
kafka-console-producer.sh --broker-list localhost:9092 \
--topic employee_information
{"emp_id":4,"name":"David","dept_id":1}
{"emp_id":5,"name":"Eric","dept_id":2}
{"emp_id":6,"name":"Frank","dept_id":2}
你会看到 department_counts 里 dept_id=1 和 dept_id=2 的 emp_count 不断被更新。
6.2 用 print Sink 做本地调试
如果用的是 department_counts_print,SQL Client 会直接在控制台打印结果,类似:
text
+I(1,2)
+I(2,1)
-U(1,2)
+U(1,3)
...
其中:
+I表示 insert;-U / +U表示 update 前 / update 后(这是 Flink 内部 changelog 语义)。
七、完整 SQL 脚本(可直接复制到 SQL Client)
下面是一个可以直接丢进 SQL Client 的完整脚本,你可以按顺序执行:
sql
-- =========================
-- 1. 源表:Kafka 员工信息
-- =========================
CREATE TABLE employee_information (
emp_id INT,
name STRING,
dept_id INT,
proc_time AS PROCTIME()
) WITH (
'connector' = 'kafka',
'topic' = 'employee_information',
'properties.bootstrap.servers' = 'localhost:9092',
'properties.group.id' = 'flink-sql-employee-group',
'scan.startup.mode' = 'earliest-offset',
'format' = 'json',
'json.ignore-parse-errors' = 'true'
);
-- =========================
-- 2. 结果表:Upsert-Kafka 部门人数统计
-- =========================
CREATE TABLE department_counts (
dept_id INT,
emp_count BIGINT,
PRIMARY KEY (dept_id) NOT ENFORCED
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'department_counts',
'properties.bootstrap.servers' = 'localhost:9092',
'key.format' = 'json',
'key.json.ignore-parse-errors' = 'true',
'value.format' = 'json',
'value.json.ignore-parse-errors' = 'true'
);
-- (可选)调试用的 print Sink
CREATE TABLE department_counts_print (
dept_id INT,
emp_count BIGINT
) WITH (
'connector' = 'print'
);
-- =========================
-- 3. 实时部门人数统计作业:写入 Upsert-Kafka
-- =========================
INSERT INTO department_counts
SELECT
dept_id,
COUNT(*) AS emp_count
FROM employee_information
GROUP BY dept_id;
-- (可选)同时把结果打印出来看看
-- INSERT INTO department_counts_print
-- SELECT
-- dept_id,
-- COUNT(*) AS emp_count
-- FROM employee_information
-- GROUP BY dept_id;