实用指南:Flink SQL + Kafka 实时统计部门人数

一、场景设定

我们设计一个非常经典的小案例:

  • 输入: 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 显式加载)。

然后先启动:

# 启动 Flink 集群
./bin/start-cluster.sh
# 另开一个终端:启动 SQL Client
./bin/sql-client.sh

Kafka 那边你可以先创建两个 Topic(示例命令):

# 员工信息 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 作为消息格式,示例消息长这样:

{"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 映射成一张动态表:

-- 源表:员工信息(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'
);

验证一下表能不能读到数据,可以先手动塞几条:

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 中跑:

SELECT * FROM employee_information;

如果有数据流出来,就 OK 了。

四、定义结果表(Sink):部门人数统计(Upsert-Kafka)

部门人数是一个「按部门去重聚合」的结果,每个 dept_id 对应一个最新的 emp_count,非常适合用 Upsert-Kafka

-- 结果表:部门人数统计(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 表调试:

CREATE TABLE department_counts_print (
dept_id   INT,
emp_count BIGINT
) WITH (
'connector' = 'print'
);

五、实时统计 SQL:按部门人数聚合

核心逻辑其实就一条 SQL:

INSERT INTO department_counts
SELECT
dept_id,
COUNT(*) AS emp_count
FROM employee_information
GROUP BY dept_id;

语义上是:

  • 随着 employee_information 不断有新员工数据写入;
  • Flink 实时维护每个 dept_idCOUNT(*)
  • 每次计数变化就以 Upsert 形式写入 department_counts Kafka Topic。

如果你也建了 print 表,可以临时这样跑来对比:

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

kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic department_counts \
--from-beginning \
--property print.key=true \
--property key.separator=:

当你继续往 employee_information 写入数据,比如:

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_countsdept_id=1dept_id=2emp_count 不断被更新。

6.2 用 print Sink 做本地调试

如果用的是 department_counts_print,SQL Client 会直接在控制台打印结果,类似:

+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 的完整脚本,你可以按顺序执行:

-- =========================
-- 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;
posted on 2026-01-22 16:23  ljbguanli  阅读(4)  评论(0)    收藏  举报