初识canal——增量数据同步

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

工作中需要同步FM数据库全量数据到自己系统,对方数据是实时更新,对方甩手掌柜,只提供了同步接口,所有的同步任务都需要项目组自己进行,对于增量数据一直没找到合适的方法,当看到canal时眼前一亮。那么什么是canal?

一、canal简介

关键字:增量日志,增量数据订阅和消费

canal [kə'næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

先来看一张图:

canal.png

mysql的主从复制原理:

canal3.png

  • MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)

  • MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)

  • MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

canal的工作原理

canal的工作原理就是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。

二、canal能做什么

基于日志增量订阅和消费的业务包括

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

三、canal如何使用

1、mysql配置

开启MySQL的 binary log 日志

mysql我使用的是8.x版本,需要在my.ini添加如下配置:

[mysqld]
# 打开binlog
log-bin=mysql-bin
# 选择ROW(行)模式
binlog-format=ROW
# 配置MySQL replaction需要定义,不要和canal的slaveId重复
server_id=1
复制代码

ps:有的服务器mysql版本配置文件应该是my.cnf

创建canal用户并授权

-- 使用命令登录:mysql -u root -p
-- 创建用户 用户名:canal 密码:Canal@123456
create user 'canal'@'%' identified by 'Canal';
-- 授权 *.*表示所有库
grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%' identified by 'Canal';
复制代码

ps:8.x版本授权应该分开写: grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on . to 'canal'@'%' with grant option;

flush privileges;

然后net restart mysql看看是否打开binlog:

show variables like 'log_bin';

2、canal安装

下载地址:官网下载页面:github.com/alibaba/can…

下载完成解压到目录,conf/example/instance.properties修改配置文件

## mysql serverId , v1.0.26+ will autoGen
## v1.0.26版本后会自动生成slaveId,所以可以不用配置
# canal.instance.mysql.slaveId=0

# 数据库地址
canal.instance.master.address=127.0.0.1:3306
# binlog日志名称
canal.instance.master.journal.name=mysql-bin.000001
# mysql主库链接时起始的binlog偏移量
canal.instance.master.position=154

# username/password
# 在MySQL服务器授权的账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=Canal@123456
# 字符集
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false

# table regex .*\..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\..*
复制代码

然后执行start.bat脚本,启动成功。

3、Java引入

pom添加依赖:

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.4</version>
</dependency>
复制代码

创建一个springboot工程,直接新建CanalClient类,创建连接。

public static void main(String[] args) {
    //1.创建连接
    CanalConnector connect = CanalConnectors.newSingleConnector(
            new InetSocketAddress("127.0.0.1", 11111),
            "example",
            "",
            ""
    );
    //指定一次性读取的条数
    int bachChSize = 1000;
    // 设置转态
    boolean running = true;
    while (running) {
        //2.建立连接
        connect.connect();
        //回滚上次请求的信息放置防止数据丢失
        connect.rollback();
        // 订阅匹配日志
        connect.subscribe();
        while (running) {
            Message message = connect.getWithoutAck(bachChSize);
            // 获取batchId
            long batchId = message.getId();
            // 获取binlog数据的条数
            int size = message.getEntries().size();
            if (batchId == -1 || size == 0) {} else {
                printSummary(message);
            }
            // 确认指定的batchId已经消费成功
            connect.ack(batchId);
        }
    }
}

private static void printSummary(Message message) {
    // 遍历整个batch中的每个binlog实体
    for (CanalEntry.Entry entry : message.getEntries()) {
        // 事务开始
        if (
                entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                        entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND
        ) {
            continue;
        }
        // 获取binlog文件名
        String logfileName = entry.getHeader().getLogfileName();
        // 获取logfile的偏移量
        long logfileOffset = entry.getHeader().getLogfileOffset();
        // 获取sql语句执行时间戳
        long executeTime = entry.getHeader().getExecuteTime();
        // 获取数据库名
        String schemaName = entry.getHeader().getSchemaName();
        // 获取表名
        String tableName = entry.getHeader().getTableName();
        // 获取事件类型 insert/update/delete
        String eventTypeName = entry
                .getHeader()
                .getEventType()
                .toString()
                .toLowerCase();
        System.out.println("logfileName" + ":" + logfileName);
        System.out.println("logfileOffset" + ":" + logfileOffset);
        System.out.println("executeTime" + ":" + executeTime);
        System.out.println("schemaName" + ":" + schemaName);
        System.out.println("tableName" + ":" + tableName);
        System.out.println("eventTypeName" + ":" + eventTypeName);
        CanalEntry.RowChange rowChange = null;
        try {
            // 获取存储数据,并将二进制字节数据解析为RowChange实体
            rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        } // 迭代每一条变更数据
        for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
            // 判断是否为删除事件
            if (entry.getHeader().getEventType() == CanalEntry.EventType.DELETE) {
                System.out.println("---delete---");
                printColumnList(rowData.getBeforeColumnsList());
                System.out.println("---");
            }
            // 判断是否为更新事件
            else if (
                    entry.getHeader().getEventType() == CanalEntry.EventType.UPDATE
            ) {
                System.out.println("---update---");
                printColumnList(rowData.getBeforeColumnsList());
                System.out.println("---");
                printColumnList(rowData.getAfterColumnsList());
            }
            // 判断是否为插入事件
            else if (
                    entry.getHeader().getEventType() == CanalEntry.EventType.INSERT
            ) {
                System.out.println("---insert---");
                printColumnList(rowData.getAfterColumnsList());
                System.out.println("---");
            }
        }
    }
}

// 打印所有列名和列值
private static void printColumnList(List<CanalEntry.Column> columnList) {
    for (CanalEntry.Column column : columnList) {
        System.out.println(column.getName() + "\t" + column.getValue());
    }
}
复制代码

直接运行

515.png

以上就完成了Java客户端的代码,这里不做具体的处理,仅仅是打印,先有个直观的感受。

最后我们开始测试,启动MySQL、Canal Server,还有Spring Boot项目。然后创建表,插入数据,会有对应的binlog日志输出。

canal的好处在于对业务代码没有侵入,因为是基于监听binlog日志去进行同步数据的。实时性也能做到准实时,其实是很多企业一种比较常见的数据同步的方案。

此外,canal还支持集群配置,有控制台,实际项目我们是配置MQ模式,配合RocketMQ或者Kafka/redis,canal会把数据发送到MQ的topic中,然后通过消息队列的消费者进行处理,将在后面继续学习。

猜你喜欢

转载自juejin.im/post/7031864862816862238