只需要十分钟 ,愉快入门 FlinkCDC 数据同步

3,093 阅读5分钟

一. 前言

Flink CDC(Change Data Capture)是 Apache Flink 提供的一个功能强大的组件,用于实时捕获和处理数据库中的数据变更。

官方文档 @ Apache Flink CDC |Apache Flink CDC

  • Flink CDC 不止支持一种数据库 ,包括 MySQL , PostgreSQL , MongoDB 等
  • Flink CDC 通常是基于 BinLog 来实现的
  • Flink CDC 使用时 ,还是要导入对应的包

宏观概念入门 :

  • CDC 是一种技术,用来捕获数据变更
  • CDC 的运用场景主要包括 : 数据同步 , 数据分发 ,数据集成
  • CDC 的实现方式主要包括 : 基于查询(自行去数据源查询) , 基于日志(Binlog)

简单聊一聊趋势 :

常见的方式包括通过 DataX 实现全量同步后 ,再通过 Canal 实现对应的增量同步。这种方式并没有太大的问题 ,性能方面和操作方式都不难,最大的问题可能就是 : 全量和增量需要分开处理

而 Flink 我认为它的最大好处在于可以同时集成增量和全量,且适配的上下游比较多(生态好,功能多).

而在我个人的设计中 ,轻量级的 MySQL 数据同步用 Canal 足够了。 而实时计算要求更高的复杂营销,统计我会考虑 Flink。

生态快速一览(数据同步类型) :

  • 支持包括 MySQL / Oracle 等常见关系型数据库的同步
  • 支持 MongoDB / HDFS / ES / Hbase 等新数据架构的数据同步
  • 支持 集成流处理平台 Apache Pulsar / Pravega
  • 支持 Kafka, RabbitMQ 等消息推送能力
  • 支持 Java / Python / Scala 等不同的语言

当然除了数据同步 ,Flink 还支持机器学习 , CEP 等高级用法

最后的最后聊一下应用场景 :

就像上文说的 ,功能上包括 : 数据同步 , 数据分发 ,数据集成。 而在实践上面就可以分为 : 报表分析 ,实时大屏 , 数据应用 ,实时营销等一些具体的场景了

二. 基础使用流程

2.1 使用方式

  • 自行安装 Flink 运行环境 ( 参考文档
  • 准备 Flink Java 项目 ,用于实现 FlinkCDC 代码
  • 执行项目 ,同步数据

2.2 关于版本的选择

Flink CDC 最难的点不在代码上, 而在于版本如何确定

  • 冲突点一 : FlinkCDC 和 Flink 版本不匹配 ,导致 Flink 无法调用到 CDC
  • 冲突点二 : 组件驱动无法和组件匹配 ,写入数据 / 读取数据异常
  • 冲突点三 : 不同层级之间存在组件的版本冲突
// Flink CDC 参考
- Flink 1.15.x 对应 flink-connector-mysql-cdc 2.4.x
- Flink 1.14.x 对应 flink-connector-mysql-cdc 2.3.x
- Flink 1.13.x 对应 flink-connector-mysql-cdc 2.2.x
- Flink 1.12.x 对应 flink-connector-mysql-cdc 1.4.x

三. 上手流程

3.1 Maven 依赖

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.gang</groupId>
    <artifactId>com-ant-example-mysql</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <dependencies>
        <!-- Flink dependencies -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-core</artifactId>
            <version>1.14.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>1.14.2</version>
        </dependency>

        <!-- 版本冲突问题 -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-shaded-guava</artifactId>
            <version>30.1.1-jre-14.0</version>
        </dependency>

        <!-- Flink CDC 依赖 -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java_2.12</artifactId>
            <version>1.14.2</version>
        </dependency>

        <!-- MySQL connector -->
        <dependency>
            <groupId>com.ververica</groupId>
            <artifactId>flink-connector-mysql-cdc</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-jdbc_2.12</artifactId>
            <version>1.14.2</version>
        </dependency>

        <!-- 个人项目依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- Maven compiler plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <!-- Maven shade plugin for creating a fat jar -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.ant.flink.FlinkMySQLExampleMain</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

核心关注要点 :

  • 我这里使用的 Flink 版本是 1.14.2 , 所以我 Java 项目的依赖都是基于该版本的
  • flink-core / flink-java 是构建项目的基础依赖 ,按照版本进行导入就行
  • flink-shaded-guava 主要是由于 CDC 和 Flink 引用的 GUAVA 版本不一致
    • 表现为 ClassNotFoundException = org.apache.flink.shaded.guava18.....ThreadFactoryBuilder
    • 具体使用什么版本可以去 Flink 源码里面找对应的版本看 (最准)
  • flink-streaming-java 是 Flink Stream 模式的核心依赖 ,CDC 也是基于这个实现的
  • flink-connector-mysql-cdcJDBC 就是主要的业务依赖了
  • 最后 ,不要忘了把以上的依赖都打到 Jar 里面去哦

3.2 核心逻辑一览

package com.ant.flink;

import com.alibaba.fastjson.JSONObject;
import com.ververica.cdc.connectors.mysql.MySqlSource;
import com.ververica.cdc.connectors.mysql.table.StartupOptions;
import com.ververica.cdc.debezium.DebeziumDeserializationSchema;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.util.Collector;
import org.apache.kafka.connect.data.Field;
import org.apache.kafka.connect.data.Schema;
import org.apache.kafka.connect.data.Struct;
import org.apache.kafka.connect.source.SourceRecord;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class FlinkMySQLExampleMain {

    /**
     * MySQL 数据同步主方法
     */
    public static void main(String[] args) throws Exception {

        // S1 : 搭建 Flink Stream 核心容器
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // S2 : 构建 Source 连接
        SourceFunction<String> sourceFunction = MySqlSource.<String>builder()
                .hostname("123.106.145.123")
                .port(3306)
                .databaseList("ant-test")// 设置要监听的数据库
                .tableList("ant-test.ant_user") // 设置要监听的表
                .username("root")
                .password("123123132")
                .deserializer(new JsonDebeziumDeserializationSchema())// 使用 Debezium 反序列化模式
                .startupOptions(StartupOptions.initial()) // 设置启动选项,initial 表示从头开始读取
                .build();

        // S3 : 添加 source 和 Sink
        env.addSource(sourceFunction)
                .addSink(new ConsoleSink());

        // S4 : 执行 Flink 作业
        env.execute("MySQL CDC Example");
    }

    /**
     * BinLog 序列化 : 主要用于解析 BinLog
     */
    public static class JsonDebeziumDeserializationSchema implements DebeziumDeserializationSchema {
        @Override
        public void deserialize(SourceRecord sourceRecord, Collector collector) throws Exception {

            log.info("S1 : 拉取到 MySQL Binlog,数据信息:" + sourceRecord);
            Map<String, Object> exchangeMap = new HashMap<>();

            // 拆分 BinLog 数据库及表信息
            String topic = sourceRecord.topic();
            String[] split = topic.split("[.]");
            String database = split[1];
            String table = split[2];
            log.info("S2 : 拉取到 MySQL Binlog :" + database + "." + table);

            exchangeMap.put("database", database);
            exchangeMap.put("table", table);

            // 拆分 Binlog 数据信息
            Struct struct = (Struct) sourceRecord.value();
            Struct after = struct.getStruct("after");
            if (after != null) {
                Schema schema = after.schema();
                Map<String, Object> dataMap = new HashMap<>();
                for (Field field : schema.fields()) {
                    dataMap.put(field.name(), after.get(field.name()));
                }
                exchangeMap.put("data", JSONObject.toJSONString(dataMap));
                log.info("S3 : 拉取到 MySQL Binlog 数据体 {}", JSONObject.toJSONString(dataMap));
            }

            // 传递数据到下游 Sink
            collector.collect(JSONObject.toJSONString(exchangeMap));
        }

        @Override
        public TypeInformation<String> getProducedType() {
            return BasicTypeInfo.STRING_TYPE_INFO;
        }
    }

    /**
     * 写 Log 到控制台
     */
    public static class ConsoleSink extends RichSinkFunction<String> {

        public void open(Configuration parameters) throws Exception {
        }

        public void invoke(String value, Context context) throws Exception {
            log.info("S4 : 接收到 MySQL 数据信息 {}", value);
            JSONObject exchangeMap = JSONObject.parseObject(value);
            FlinkSourceDto sourceDto = exchangeMap.getObject(exchangeMap.getString("data"), FlinkSourceDto.class);

            log.info("S5 : 接收到对象 {}", sourceDto);
        }
    }
    
    /**
    * 传递 DTO
    **/
    @Data
    public static class FlinkSourceDto {

        private Long id;

        private Integer userStatus;

        private String userName;

    }
}

结果展示 :

image.png

3.3 写数据到其他的组件中

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>
/**
 * 写数据到 MySQL
 */
public static class MySQLSink extends RichSinkFunction<String> {


    private Connection connection;
    private PreparedStatement preparedStatement;

    // 数据库连接配置
    private final String jdbcUrl = "jdbc:mysql://123.106.145.123:3306/ant-test";
    private final String username = "root";
    private final String password = "123123123";
    private final String insertQuery = "INSERT INTO ant_user_image (user_name) VALUES (?)";


    /**
     * 初始化数据库连接
     */
    public void open(Configuration parameters) throws Exception {
        log.info("初始化MySQL连接信息");
        super.open(parameters);
        Class.forName("com.mysql.cj.jdbc.Driver");
        log.info("JDBC Driver 显式加载完成");
        connection = DriverManager.getConnection(jdbcUrl, username, password);
        preparedStatement = connection.prepareStatement(insertQuery);
    }

    public void invoke(String value, Context context) throws Exception {
        log.info("S4 : 接收到 MySQL 数据信息 {}", value);
        JSONObject exchangeMap = JSONObject.parseObject(value);
        FlinkSourceDto sourceDto = JSONObject.parseObject(exchangeMap.getString("data"), FlinkSourceDto.class);

        log.info("S5 : 接收到对象 {}", sourceDto);
        // 设置 SQL 语句中的参数
        preparedStatement.setString(1, sourceDto.getUserName());
        // 执行插入操作
        preparedStatement.executeUpdate();
    }

    @Override
    public void close() throws Exception {
        // 关闭资源
        if (preparedStatement != null) {
            preparedStatement.close();
        }
        if (connection != null) {
            connection.close();
        }
        super.close();
    }
}

image.png

总结

  • 版本号是最大的问题 ,要找到对应的版本号 ,其他后面东西都很简单
  • 其次就是依赖冲突 ,这里可以下源码 ,从源码里面找到对应的依赖 ,解决即可

最后的最后 ❤️❤️❤️👇👇👇