基于Kafka+Flink+ClickHouse打造轻量级点击流实时数仓

一、前言

Flink 和 ClickHouse 分别是实时计算和 OLAP 领域的翘楚,也是近些年非常火爆的开源框架,很多大厂都在将两者结合使用来构建各种用途的实时平台,效果很好。关于两者的优点就不再赘述,本文来简单介绍笔者团队在点击流实时数仓方面的一点实践经验。

1. 点击流及其维度建模

所谓点击流(click stream),就是指用户访问网站、App 等 Web 前端时在后端留下的轨迹数据,也是流量分析(traffic analysis)和用户行为分析(user behavior analysis)的基础。点击流数据一般以访问日志和埋点日志的形式存储,其特点是量大、维度丰富。以我们一个中等体量的普通电商平台为例,每天产生 200+GB、十亿条左右的原始日志,埋点事件 100+个,涉及 50+个维度。按照 Kimball 的维度建模理论,点击流数仓遵循典型的星形模型,简图如下。

2. 点击流数仓分层设计

点击流实时数仓的分层设计仍然可以借鉴传统数仓的方案,以扁平为上策,尽量减少数据传输中途的延迟。简图如下。

  • DIM 层:维度层,MySQL 镜像库,存储所有维度数据。
  • ODS 层:贴源层,原始数据由 Flume 直接进入 Kafka 的对应 topic。
  • DWD 层:明细层,通过 Flink 将 Kafka 中数据进行必要的 ETL 与实时维度 join 操作,形成规范的明细数据,并写回 Kafka 以便下游与其他业务使用。再通过 Flink 将明细数据分别写入 ClickHouse 和 Hive 打成大宽表,前者作为查询与分析的核心,后者作为备份和数据质量保证(对数、补数等)。
  • DWS 层:服务层,部分指标通过 Flink 实时汇总至 Redis,供大屏类业务使用。更多的指标则通过 ClickHouse 物化视图等机制周期性汇总,形成报表与页面热力图。特别地,部分明细数据也在此层开放,方便高级 BI 人员进行漏斗、留存、用户路径等灵活的 ad-hoc 查询,这些也是 ClickHouse 远超过其他 OLAP 引擎的强大之处。

二、要点与注意事项

1. Flink 实时维度关联

Flink 框架的异步 I/O 机制为用户在流式作业中访问外部存储提供了很大的便利。针对我们的情况,有以下三点需要注意:

  • 使用异步 MySQL 客户端,如 Vert.x MySQL Client。
  • AsyncFunction 内添加内存缓存(如 Guava Cache、Caffeine 等),并设定合理的缓存驱逐机制,避免频繁请求 MySQL 库。
  • 实时维度关联仅适用于缓慢变化维度,如地理位置信息、商品及分类信息等。快速变化维度(如用户信息)则不太适合打进宽表,我们采用 MySQL 表引擎将快变维度表直接映射到 ClickHouse 中,而 ClickHouse 支持异构查询,也能够支撑规模较小的维表 join 场景。未来则考虑使用 MaterializedMySQL 引擎将部分维度表通过 binlog 镜像到 ClickHouse。
2. Flink-ClickHouse Sink 设计

可以通过 JDBC(flink-connector-jdbc)方式来直接写入 ClickHouse,但灵活性欠佳。好在 clickhouse-jdbc 项目提供了适配 ClickHouse 集群的 BalancedClickhouseDataSource 组件,我们基于它设计了 Flink-ClickHouse Sink,要点有三:

  • 写入本地表,而非分布式表,老生常谈了。
  • 按数据批次大小以及批次间隔两个条件控制写入频率,在 part merge 压力和数据实时性两方面取得平衡。目前我们采用 10000 条的批次大小与 15 秒的间隔,只要满足其一则触发写入。
  • BalancedClickhouseDataSource 通过随机路由保证了各 ClickHouse 实例的负载均衡,但是只是通过周期性 ping 来探活,并屏蔽掉当前不能访问的实例,而没有故障转移——亦即一旦试图写入已经失败的节点,就会丢失数据。为此我们设计了重试机制,重试次数和间隔均可配置,如果当重试机会耗尽后仍然无法成功写入,就将该批次数据转存至配置好的路径下,并报警要求及时检查与回填。

当前我们仅实现了 DataStream API 风格的 Flink-ClickHouse Sink,随着 Flink 作业 SQL 化的大潮,在未来还计划实现 SQL 风格的 ClickHouse Sink,打磨健壮后会适时回馈给社区。另外,除了随机路由,我们也计划加入轮询和 sharding key hash 等更灵活的路由方式。

还有一点就是,ClickHouse 并不支持事务,所以也不必费心考虑 2PC Sink 等保证 exactly once 语义的操作。如果 Flink 到 ClickHouse 的链路出现问题导致作业重启,作业会直接从最新的位点(即 Kafka 的 latest offset)开始消费,丢失的数据再经由 Hive 进行回填即可。

3. ClickHouse 数据重平衡

ClickHouse 集群扩容之后,数据的重平衡(reshard)是一件麻烦事,因为不存在类似 HDFS Balancer 这种开箱即用的工具。一种比较简单粗暴的思路是修改 ClickHouse 配置文件中的 shard weight,使新加入的 shard 多写入数据,直到所有节点近似平衡之后再调整回来。但是这会造成明显的热点问题,并且仅对直接写入分布式表才有效,并不可取。

因此,我们采用了一种比较曲折的方法:将原表重命名,在所有节点上建立与原表 schema 相同的新表,将实时数据写入新表,同时用 clickhouse-copier 工具将历史数据整体迁移到新表上来,再删除原表。当然在迁移期间,被重平衡的表是无法提供服务的,仍然不那么优雅。

安装过程

1、docker-compose up -d 安装

version: '3.8'

services:
  zookeeper:
    image: bitnami/zookeeper:latest
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes
  kafka:
    image: wurstmeister/kafka:2.12-2.2.1
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    depends_on:
      - zookeeper
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock


  flink-jobmanager:
    image: flink:1.17.2-scala_2.12
    container_name: flink-jobmanager
    ports:
      - "9081:9081"  # Flink Web UI
    command: jobmanager            # 必须指定启动模式
    environment:
      - JOB_MANAGER_RPC_ADDRESS=flink-jobmanager
  flink-taskmanager:
    image: flink:1.17.2-scala_2.12
    container_name: flink-taskmanager
    depends_on:
      - flink-jobmanager
    command: taskmanager           # 必须指定启动模式
    environment:
      - JOB_MANAGER_RPC_ADDRESS=flink-jobmanager

  clickhouse-server:
    image: yandex/clickhouse-server:latest
    container_name: clickhouse-server
    ports:
      - "8123:8123"
      - "9000:9000"
    volumes:
      - clickhouse_data:/var/lib/clickhouse
    ulimits:
      nofile:
        soft: 262144
        hard: 262144

volumes:
  clickhouse_data:


2、进入kafka创建Topic
 

docker exec -it kafka /bin/bash

kafka-topics.sh --create --topic user-events --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1

查看已创建的Topic

kafka-topics.sh --list --bootstrap-server localhost:9092

3、进Clickhouse创建数据库和表

#进入clickhouose容器
docker exec -it clickhouse-server /bin/bash

#命令行进入clickhouse管理,类似mysql的mysql命令

clickhouse-client

#创建数据库

CREATE DATABASE IF NOT EXISTS testdb;

#切换数据库,和mysql一样

use testdb;

#创建表用户行为表(追加)

CREATE TABLE employee_behavior (
  event_time DateTime DEFAULT now(),     -- 行为发生时间
  employee_id UInt32,                    -- 员工ID
  team_id UInt32,
  behavior_type Enum('forward'=1, 'new_fan'=2, 'view'=3, 'deal'=4, 'refund'=5),
  amount Float64 DEFAULT 0               -- 金额(成交/退款)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (event_time, employee_id);


4、数据采集及查询

行为类型及值:viewforwardnew_fan 等行为的 amount 就固定为 1,虽然它们本身没“金额”,但你可以这样理解 👇:


✅ 推荐字段含义约定

行为类型(behavior_type含义是否用 amount建议处理方式
view浏览一次✔️ 是(值为 1写入 amount = 1
forward转发一次✔️ 是(值为 1写入 amount = 1
new_fan新增粉丝✔️ 是(值为 1写入 amount = 1
deal成交✔️ 是(金额)写入 amount = 实收金额
refund退款✔️ 是(退款金额)写入 amount = 退款金额

🔁 这样你可以用统一 SQL 聚合 amount

比如这个查询同时展示各行为数量:

SELECT
  employee_id,
  countIf(behavior_type = 'view') AS views,
  countIf(behavior_type = 'forward') AS forwards,
  countIf(behavior_type = 'new_fan') AS new_fans,
  countIf(behavior_type = 'deal') AS deal_count,
  sumIf(amount, behavior_type = 'deal') AS revenue,
  sumIf(amount, behavior_type = 'refund') AS refund_amount
FROM employee_behavior
WHERE event_time >= today()
GROUP BY employee_id
ORDER BY revenue DESC;

✅ 前三类 count 类行为通过 countIf
✅ 金额类行为用 sumIf


✅ 写入时的代码建议(Java 示例)

// 伪代码,行为类型 + 金额
public void reportBehavior(String type, int employeeId, double money) {
    double amount = 0;

    if (type.equals("deal") || type.equals("refund")) {
        amount = money; // 金额行为
    } else {
        amount = 1;      // 计数行为
    }

    String sql = "INSERT INTO employee_behavior (employee_id, behavior_type, amount) VALUES (?, ?, ?)";
    jdbc.update(sql, employeeId, type, amount);
}

🧠 总结

行为amount 建议值
浏览/转发/新粉1
成交/退款实际金额

前端直接上传来的用户行为数据,并且希望实现“实时排行榜”展示,采用以下这个高效稳定的架构方案👇:


✅ 总体架构:前端行为实时记录 + ClickHouse 实时聚合查询

[前端用户操作]
      ↓
[后端接口接收行为数据]
      ↓(写入)
[ClickHouse(行为日志表)]
      ↓(聚合查询)
[排行榜页面实时刷新展示]

🧱 Step 1:ClickHouse 表结构(行为日志追加表)

CREATE TABLE employee_behavior (
  event_time DateTime DEFAULT now(),     -- 行为发生时间
  employee_id UInt32,                    -- 员工ID
  team_id UInt32,
  behavior_type Enum('forward'=1, 'new_fan'=2, 'view'=3, 'deal'=4, 'refund'=5),
  amount Float64 DEFAULT 0               -- 金额(成交/退款)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (event_time, employee_id);

✅ 每次行为(转发、新粉、浏览、成交、退款)就写入一条记录,数据是只追加,不更新的,这正是 ClickHouse 最擅长的场景。


🔁 Step 2:前端上传行为数据格式

前端每次发生行为,比如转发、成交、浏览,都调用一个接口:

POST /api/report-behavior
{
  "employee_id": 1001,
  "team_id": 1,
  "behavior_type": "deal",
  "amount": 89.0
}

或者转发时:

{
  "employee_id": 1001,
  "team_id": 1,
  "behavior_type": "forward"
}

🖥 Step 3:后端接收接口写入 ClickHouse(Java 示例)

@PostMapping("/api/report-behavior")
public void reportBehavior(@RequestBody BehaviorDTO dto) {
    String sql = "INSERT INTO employee_behavior (employee_id, team_id, behavior_type, amount) VALUES (?, ?, ?, ?)";
    clickHouseJdbcTemplate.update(sql, dto.employeeId, dto.teamId, dto.behaviorType, dto.amount == null ? 0 : dto.amount);
}

也可以批量写入、异步缓冲优化。


📊 Step 4:排行榜查询 SQL 示例

今日成交排行榜(实时)

SELECT
  employee_id,
  countIf(behavior_type = 'deal') AS deals,
  sumIf(amount, behavior_type = 'deal') AS revenue
FROM employee_behavior
WHERE event_time >= today()
GROUP BY employee_id
ORDER BY deals DESC
LIMIT 10;

昨日浏览排行榜

SELECT
  employee_id,
  countIf(behavior_type = 'view') AS views
FROM employee_behavior
WHERE event_time >= today() - INTERVAL 1 DAY AND event_time < today()
GROUP BY employee_id
ORDER BY views DESC
LIMIT 10;

实时5分钟内排行榜(适合直播刷榜)

SELECT
  employee_id,
  countIf(behavior_type = 'deal') AS deals,
  sumIf(amount, behavior_type = 'deal') AS revenue
FROM employee_behavior
WHERE event_time >= now() - INTERVAL 5 MINUTE
GROUP BY employee_id
ORDER BY deals DESC
LIMIT 10;

🚀 Step 5:前端展示页面定时轮询刷新

// uniapp / vue 示例,每10秒请求排行榜
setInterval(() => {
  uni.request({
    url: '/api/rankings?type=today',
    success: (res) => {
      this.rankingList = res.data;
    }
  });
}, 10000);

如果你想统计 “实际成交额排行” = 成交金额 - 退款金额,ClickHouse 非常适合通过 sumIf() 实现这个聚合公式,不用多表 JOIN,也不用临时表,一个 SQL 就能搞定 ✅。


✅ 示例 SQL:统计今日“实收金额排行”

SELECT
  employee_id,
  sumIf(amount, behavior_type = 'deal') AS total_deal_amount,
  sumIf(amount, behavior_type = 'refund') AS total_refund_amount,
  sumIf(amount, behavior_type = 'deal') - sumIf(amount, behavior_type = 'refund') AS net_income
FROM employee_behavior
WHERE event_time >= today()
GROUP BY employee_id
ORDER BY net_income DESC
LIMIT 10;

📌 字段解释:

字段名含义
total_deal_amount今日总成交金额
total_refund_amount今日退款金额
net_income实际收入 = 成交 - 退款
employee_id员工 ID

🧠 扩展用法(带员工名、更多行为)

SELECT
  employee_id,
  any(employee_name) AS name,
  sumIf(amount, behavior_type = 'deal') AS total_deal,
  sumIf(amount, behavior_type = 'refund') AS total_refund,
  countIf(behavior_type = 'deal') AS deal_count,
  countIf(behavior_type = 'view') AS views,
  total_deal - total_refund AS net_income
FROM employee_behavior
WHERE event_time >= today()
GROUP BY employee_id
ORDER BY net_income DESC
LIMIT 10;

🚨 如果你没有 employee_name 字段,也可以从别的维表关联进来,或者提前冗余进 ClickHouse。

如果你想支持:

  • 查询“昨日实收排行”

  • 查询“最近1小时实收排行”

  • 查询“上周实收前10名”

也只需改一下 WHERE event_time 的时间条件即可。

都是ClickHouse 行为统计排行榜系统设计中的经典需求,逐一讲解并附上完整 SQL + 技术建议 ✅


✅ 1. 如何查「团队总成交额排行」?

按团队聚合就可以,核心是 GROUP BY team_id

🔍 SQL 示例(今日团队成交排行):

SELECT
  team_id,
  sumIf(amount, behavior_type = 'deal') AS total_deal,
  sumIf(amount, behavior_type = 'refund') AS total_refund,
  total_deal - total_refund AS net_income
FROM employee_behavior
WHERE event_time >= today()
GROUP BY team_id
ORDER BY net_income DESC
LIMIT 10;

如果你还有 team_name 字段,也可以加上 any(team_name) 一起返回。


✅ 2. 如何查「前日 / 昨日 / 今日 / 总榜」一起返回?

建议拆成四个子查询再 UNION ALL 或前端并行请求(ClickHouse 不支持多个 SELECT 同时返回),下面是服务端方式:

🔄 SQL 示例(4段时间)

SELECT 'today' AS tag, employee_id, sumIf(amount, behavior_type = 'deal') - sumIf(amount, behavior_type = 'refund') AS net_income
FROM employee_behavior
WHERE event_time >= today()
GROUP BY employee_id
ORDER BY net_income DESC
LIMIT 10

UNION ALL

SELECT 'yesterday', employee_id, sumIf(amount, behavior_type = 'deal') - sumIf(amount, behavior_type = 'refund') AS net_income
FROM employee_behavior
WHERE event_time >= today() - INTERVAL 1 DAY AND event_time < today()
GROUP BY employee_id
ORDER BY net_income DESC
LIMIT 10

UNION ALL

SELECT 'day_before', employee_id, sumIf(amount, behavior_type = 'deal') - sumIf(amount, behavior_type = 'refund') AS net_income
FROM employee_behavior
WHERE event_time >= today() - INTERVAL 2 DAY AND event_time < today() - INTERVAL 1 DAY
GROUP BY employee_id
ORDER BY net_income DESC
LIMIT 10

UNION ALL

SELECT 'total', employee_id, sumIf(amount, behavior_type = 'deal') - sumIf(amount, behavior_type = 'refund') AS net_income
FROM employee_behavior
GROUP BY employee_id
ORDER BY net_income DESC
LIMIT 10

🧠 建议:

  • 返回字段中用 tag 区分是哪一段;

  • 或者后端拆成 4 个 SQL,让前端接收结构更清晰;

  • 或者前端用 tab 切换,每次查询不同时间段。


✅ 3. 如何查「我的排名 + 前10名」?

这个是排行榜系统中非常重要的功能

➕ 查看前10名
➕ 查看自己当前排第几(哪怕你不在前10)

ClickHouse 从 23.3 起原生支持 rank()row_number() 窗口函数!你可以这样:


💡 SQL 示例:查询前10名 + 我的排名(假设用户ID = 12345)

WITH leaderboard AS (
  SELECT
    employee_id,
    sumIf(amount, behavior_type = 'deal') - sumIf(amount, behavior_type = 'refund') AS net_income,
    rank() OVER (ORDER BY sumIf(amount, behavior_type = 'deal') - sumIf(amount, behavior_type = 'refund') DESC) AS rk
  FROM employee_behavior
  WHERE event_time >= today()
  GROUP BY employee_id
)
SELECT *
FROM leaderboard
WHERE rk <= 10 OR employee_id = 12345
ORDER BY rk

这样你会获得:

employee_idnet_incomerk
100013421.001
........
12345340.0048

✅ 不仅展示前10,也展示你自己第几名,哪怕你在第1000名。


🔧 小结:你现在的系统可以支持👇

功能方法
实时团队成交排行GROUP BY team_id 聚合
查询不同时间段排行榜多个 SELECT 查询组合
展示我的名次窗口函数 rank() over(...)
自定义榜单粒度WHERE 条件灵活控制

⚙️ Bonus:接口结构建议(REST API)

你可以设计一个统一接口,如:

GET /api/rankings?scope=today&type=employee&me=12345
参数含义
`scope=todayyesterday
`type=employeeteam`
me=12345是否返回自己的名次(选填)

✅ 最终优势总结

项目ClickHouse 方案表现
实时查询排行榜✅ 毫无压力
每秒上千条写入✅ OK(可分区/批量写)
聚合性能✅ 强大(适合复杂排名)
前端实时体验✅ 每5~10秒刷新即算“准实时”体验
高并发扩展✅ 支持分布式部署

三、编写Flink程序

package com.aa.flinkkafkach;

import com.aa.flinkkafkach.model.EmployeeBehavior;
import com.aa.flinkkafkach.sink.ClickHouseSink;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.connector.kafka.source.reader.deserializer.KafkaRecordDeserializationSchema;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import java.util.Map;
import java.util.Properties;
import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import org.apache.kafka.common.serialization.StringDeserializer;

public class FlinkKafkaToClickHouseJob {
    public static void main(String[] args) {
       try {
            // 从 application.yml 读取配置
            Yaml yaml = new Yaml();
            Map<String, Object> yamlMap;
            try (InputStream input = FlinkKafkaToClickHouseJob.class.getClassLoader().getResourceAsStream("application.yml")) {
                yamlMap = yaml.load(input);
            }

            Map<String, Object> flinkMap = (Map<String, Object>) yamlMap.get("flink");
            Map<String, Object> kafkaMap = (Map<String, Object>) flinkMap.get("kafka");
            Map<String, Object> clickhouseMap = (Map<String, Object>) flinkMap.get("clickhouse");

            String kafkaBrokers = kafkaMap.get("brokers").toString();
            String kafkaTopic = kafkaMap.get("topic").toString();
            String clickhouseUrl = clickhouseMap.get("url").toString();

            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            env.getConfig().disableClosureCleaner();

            Properties kafkaProps = new Properties();
            kafkaProps.setProperty("bootstrap.servers", kafkaBrokers);
            kafkaProps.setProperty("group.id", "flink-clickhouse-group");

            KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
                    .setBootstrapServers(kafkaBrokers)
                    .setTopics(kafkaTopic)
                    .setGroupId("flink-clickhouse-group")
                    .setDeserializer(KafkaRecordDeserializationSchema.valueOnly(StringDeserializer.class))
                    .setStartingOffsets(OffsetsInitializer.latest())
                    .build();

            env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "Kafka Source")
            .map(json -> {
                ObjectMapper mapper = new ObjectMapper();
                return mapper.readValue(json, EmployeeBehavior.class);
            })
            .addSink(new ClickHouseSink(clickhouseUrl));

            env.execute("Kafka to ClickHouse Job");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.aaa.flinkkafkach.sink;

import com.aaa.flinkkafkach.model.EmployeeBehavior;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

public class ClickHouseSink implements SinkFunction<EmployeeBehavior> {
    private transient Connection conn;
    private transient PreparedStatement ps;
    private final String jdbcUrl;

    public ClickHouseSink(String jdbcUrl) {
        this.jdbcUrl = jdbcUrl;
    }

    @Override
    public void invoke(EmployeeBehavior value, Context context) throws Exception {
        if (conn == null) {
            conn = DriverManager.getConnection(jdbcUrl);
            ps = conn.prepareStatement(
                    "INSERT INTO employee_behavior (event_time, employee_id, team_id, behavior_type, amount) VALUES (?, ?, ?, ?, ?)"
            );
        }

        ps.setString(1, value.eventTime);
        ps.setInt(2, value.employeeId);
        ps.setInt(3, value.teamId);
        ps.setString(4, value.behaviorType);
        ps.setDouble(5, value.amount);
        ps.executeUpdate();
    }

    public void close() throws Exception {
        if (ps != null) ps.close();
        if (conn != null) conn.close();
    }
}
package com.aaa.flinkkafkach.model;

public class EmployeeBehavior {
    public String eventTime;
    public int employeeId;
    public int teamId;
    public String behaviorType;
    public double amount;

    public EmployeeBehavior() {}

    // 可选 getter/setter
}
flink:
  kafka:
    brokers: localhost:9092
    topic: user-events
  clickhouse:
    url: jdbc:clickhouse://localhost:8123/default
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>dateflink</groupId>
    <artifactId>dateflink</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>dateflink</name>
    <description>dateflink</description>

    <properties>
        <java.version>17</java.version>
        <flink.version>1.17.2</flink.version>
        <spring-boot.version>2.7.18</spring-boot.version>

    </properties>

    <dependencies>
        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
                <version>2.7.18</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-autoconfigure</artifactId>
                <version>2.7.18</version>
            </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <!-- <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka</artifactId>
            <version>${flink.version}</version>
        </dependency> -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>com.clickhouse</groupId>
            <artifactId>clickhouse-jdbc</artifactId>
            <version>0.4.6</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.4</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.33</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

四、验证消息生产消费到ClickHouse全流程

下面是完整的 Kafka → Flink → ClickHouse 测试验证流程,确保你能从前端发送数据,最后在 ClickHouse 查到结果。


✅ 一、确认前提组件都已运行

确保你通过 Docker 启动了:

  • Kafka(带 Zookeeper)

  • ClickHouse

  • Flink(JobManager & TaskManager)

这里我们直接用docker启动。 docker-compose.yml 如下

version: '3.8'

services:
  zookeeper:
    image: bitnami/zookeeper:latest
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes
  kafka:
    image: wurstmeister/kafka:2.12-2.2.1
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    depends_on:
      - zookeeper
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock


  flink-jobmanager:
    image: flink:1.17.2-scala_2.12
    container_name: flink-jobmanager
    ports:
      - "9081:9081"  # Flink Web UI
    command: jobmanager            # 必须指定启动模式
    environment:
      - JOB_MANAGER_RPC_ADDRESS=flink-jobmanager
  flink-taskmanager:
    image: flink:1.17.2-scala_2.12
    container_name: flink-taskmanager
    depends_on:
      - flink-jobmanager
    command: taskmanager           # 必须指定启动模式
    environment:
      - JOB_MANAGER_RPC_ADDRESS=flink-jobmanager

  clickhouse-server:
    image: yandex/clickhouse-server:latest
    container_name: clickhouse-server
    ports:
      - "8123:8123"
      - "9000:9000"
    volumes:
      - clickhouse_data:/var/lib/clickhouse
    ulimits:
      nofile:
        soft: 262144
        hard: 262144

volumes:
  clickhouse_data:


✅ 二、ClickHouse 中建表

连接 ClickHouse 容器(或使用 DataGrip / DBeaver 等工具):

docker exec -it clickhouse-server clickhouse-client

执行建表语句:

CREATE TABLE employee_behavior
(
  event_time     DateTime DEFAULT now(),
  employee_id    UInt32,
  team_id        UInt32,
  behavior_type  Enum('forward' = 1, 'new_fan' = 2, 'view' = 3, 'deal' = 4, 'refund' = 5),
  amount         Float64 DEFAULT 0
)
ENGINE = MergeTree
ORDER BY (event_time, employee_id);

✅ 三、Kafka 创建 Topic

进入 Kafka 容器:

docker exec -it kafka bash

创建 topic(与 application.yml 中保持一致):

kafka-topics.sh --create --topic user-events --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1

查看 topic 是否创建成功:

kafka-topics.sh --list --bootstrap-server localhost:9092

✅ 四、运行 Flink 程序

1. 启动 main 方法

你可以直接用 VS Code 或打包运行:

mvn clean package
java -jar target/xxx.jar

或直接在 IDE 里点 FlinkKafkaToClickHouseJob.main()


✅ 五、模拟发送消息到 Kafka(测试数据)

打开终端,使用 Kafka 自带命令行工具发送 JSON:

docker exec -it kafka bash

kafka-console-producer.sh --broker-list localhost:9092 --topic user-events

输入一条 JSON 消息(按 EmployeeBehavior 格式):

{"eventTime":"2025-07-03 19:00:00","employeeId":101,"teamId":5,"behaviorType":"deal","amount":399.99}

回车即可发送。


✅ 六、验证 ClickHouse 中是否有数据

在 ClickHouse 中查询:

SELECT * FROM employee_behavior ORDER BY event_time DESC;


✅ 补充建议(可选)

你也可以使用前端或 Postman 等工具写一个接口,把消息 POST 给 Kafka:

POST http://localhost:8080/produce
{
  "eventTime": "2025-07-03T19:00:00",
  "employeeId": 101,
  "teamId": 5,
  "behaviorType": "deal",
  "amount": 399.99
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值