Canal入门

70 阅读6分钟

一、Canal介绍

Canal 组件是一个基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费,支持将增量数据投递到下游消费者(如 Kafka、RocketMQ 等)或者存储(如 Elasticsearch、HBase 等)的组件。

大白话: Canal 感知到MySQL数据变动,然后解析变动数据,将变动数据发送到MQ或者同步到其他数据库,等待进一步业务逻辑处理。

二、Canal的工作原理

2.1 MySQL主从复制原理

  • MySQL master 将数据变更写入二进制日志binary log,简称Binlog。
  • MySQL slave 将 master 的 binary log 拷贝到它的中继日志(relay log)
  • MySQL slave 重放 relay log 操作,将变更数据同步到最新。

2.2 MySQL Binlog日志

2.2.1 介绍

MySQL 的Binlog可以说 MySQL 最重要的日志,它记录了所有的 DDL 和 DML语句,以事件形式记录。

MySQL默认情况下是不开启Binlog,因为记录Binlog日志需要消耗时间,官方给出的数据是有1%的性能损耗。

具体开不开启,开发中需要根据实际情况做取舍。

一般来说,在下面两场景下会开启Binlog日志:

  • MySQL 主从集群部署时,需要将在 Master 端开启 Binlog,方便将数据同步到Slaves中。
  • 数据恢复了,通过使用 MySQL Binlog 工具来使恢复数据。

2.2.2 Binlog的分类

MySQL Binlog 的格式有三种,分别是 STATEMENT,MIXED,ROW。在配置文件中可以选择配

置 binlog_format= statement|mixed|row

分类介绍优点缺点
STATEMENT语句级别,记录每一次执行写操作的语句,相对于ROW模式节省了空间,但是可能产生数据不一致如update tt set create_date=now(),由于执行时间不同产生饿得数据就不同节省空间可能造成数据不一致
ROW行级,记录每次操作后每行记录的变化。假如一个update的sql执行结果是1万行statement只存一条,如果是row的话会把这个1万行的结果存这。持数据的绝对一致性。因为不管sql是什么,引用了什么函数,他只记录执行后的效果占用较大空间
MIXED是对statement的升级,如当函数中包含 UUID() 时,包含 AUTO_INCREMENT 字段的表被更新时,执行 INSERT DELAYED 语句时,用 UDF 时,会按照 ROW的方式进行处理节省空间,同时兼顾了一定的一致性还有些极个别情况依旧会造成不一致,另外statement和mixed对于需要对binlog的监控的情况都不方便

综合上面对比,Canal 想做监控分析,选择 row 格式比较合适。

2.3 Canal 工作原理

  • Canal 将自己伪装为 MySQL slave(从库) ,向 MySQL master (主库)发送dump 协议
  • MySQL master(主库) 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • Canal 接收并解析 Binlog 日志,得到变更的数据,执行后续逻辑

三、Canal运用场景

3.1 数据同步

Canal 可以帮助用户进行多种数据同步操作,如实时同步 MySQL 数据到 Elasticsearch、Redis 等数据存储介质中。

3.2 数据库实时监控

Canal 可以实时监控 MySQL 的更新操作,对于敏感数据的修改可以及时通知相关人员。

3.3 数据分析和挖掘

Canal 可以将 MySQL 增量数据投递到 Kafka 等消息队列中,为数据分析和挖掘提供数据来源。

3.4 数据库备份

Canal 可以将 MySQL 主库上的数据增量日志复制到备库上,实现数据库备份。

3.5 数据集成

Canal 可以将多个 MySQL 数据库中的数据进行集成,为数据处理提供更加高效可靠的解决方案。

3.6 数据库迁移

Canal 可以协助完成 MySQL 数据库的版本升级及数据迁移任务。

四、MySQL准备

4.1 创建数据库

新建库:canal-demo

4.2 创建表

用户表

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

4.3 修改配置文件开启Binlog支持

修改mysql的配置文件, 名字为: my.ini

server-id=1
log-bin=C:/ProgramData/MySQL/MySQL Server 8.0/binlogs/mysql-bin.log
binlog_format=row
binlog-do-db=canal-demo

server-id:mysql 实例id,集群时用于区分实例

lob-bin:binlog日志文件名称

binlog_format:binlog日志数据保存格式

binlog-do-db:指定开启binlog日志数据库。

注意:一般根据情况进行指定需要同步的数据库,如果不配置则表示所有数据库均开启 Binlog。

4.4 校验Binlog生效

重启MySQL服务,查看Binlog日志

show  VARIABLES like 'log_bin'

五、Canal安装与配置

5.1 下载

地址:github.com/alibaba/can…

解压即可。

5.2 配置

5.2.1修改canal.properties的配置

canal.port = 11111
# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ
canal.serverMode = tcp

canal.destinations = example

canal.port:默认端口 11111

canal.serverMode:服务模式,tcp 表示输入客户端,xxMQ输出到各类消息中间件

canal.destinations:canal能可以收集多个MySQL数据库数据,每个MySQL数据库都有独立的配置文件控制。通过canal.destinations可配置Mysql数据库信息,如果canal需要监听多台mysql,canal.destinations中变量用逗号隔开。配置后在conf/example/instance.properties文件内配置数据库信息。

5.2.2 修改MySQL实例配置文件instance.properties

canal.instance.mysql.slaveId=20

# position info
canal.instance.master.address=127.0.0.1:3306

# username/password
canal.instance.dbUsername=root
canal.instance.dbPassword=admin

canal.instance.mysql.slaveId:使用canal从节点id (只要保证每个canal从节点id不一样就行)

canal.instance.master.address:数据库ip端口

canal.instance.dbUsername:连接mysql账号

canal.instance.dbPassword:连接mysql密码

5.3 启动

双击启动startup.bat

六、SpringBoot集成

6.1 Helloworld

1>创建项目:canal-hello

2>导入相关依赖

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.0</version>
</dependency>

3>编写测试代码,当数据库表中信息发生变化后,canal会监听到,并打印到控制台。

package com.langfeiyes.hello;

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;

import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CanalDemo {

    public static void main(String[] args) throws InvalidProtocolBufferException {
        //1.获取 canal 连接对象
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("localhost", 11111), "example", "", "");
        while (true) {
            //2.获取连接
            canalConnector.connect();
            //3.指定要监控的数据库
            canalConnector.subscribe("canal-demo.*");
            //4.获取 Message
            Message message = canalConnector.get(100);
            List<CanalEntry.Entry> entries = message.getEntries();
            if (entries.size() <= 0) {
                System.out.println("没有数据,休息一会");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                for (CanalEntry.Entry entry : entries) {
                    // 获取表名
                    String tableName = entry.getHeader().getTableName();
                    //  Entry 类型
                    CanalEntry.EntryType entryType = entry.getEntryType();
                    //  判断 entryType 是否为 ROWDATA
                    if (CanalEntry.EntryType.ROWDATA.equals(entryType)) {
                        //  序列化数据
                        ByteString storeValue = entry.getStoreValue();
                        //  反序列化
                        CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
                        // 获取事件类型
                        CanalEntry.EventType eventType = rowChange.getEventType();
                        // 获取具体的数据
                        List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
                        // 遍历并打印数据
                        for (CanalEntry.RowData rowData : rowDatasList) {
                            List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
                            Map<String, Object> bMap = new HashMap<>();
                            for (CanalEntry.Column column : beforeColumnsList) {
                                bMap.put(column.getName(), column.getValue());
                            }
                            Map<String, Object> afMap = new HashMap<>();
                            List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
                            for (CanalEntry.Column column : afterColumnsList) {
                                afMap.put(column.getName(), column.getValue());
                            }
                            System.out.println("表名:" + tableName + ",操作类型:" + eventType);
                            System.out.println("改前:" + bMap );
                            System.out.println("改后:" + afMap );
                        }
                    }
                }
            }
        }
    }
}

6.2 SpringBoot集成

1>创建项目:canal-sb-demo

2>导入相关依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-parent</artifactId>
    <version>2.7.11</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>top.javatool</groupId>
        <artifactId>canal-spring-boot-starter</artifactId>
        <version>1.2.6-RELEASE</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.12</version>

    </dependency>

    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.21.4</version>
    </dependency>

</dependencies>

3>配置文件

canal:
  server: 127.0.0.1:11111 #canal 默认端口11111
  destination: example
spring:
  application:
    name: canal-sb-demo
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/canal-demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
    username: root
    password: admin

4>实体对象

package com.langfeiyes.sb.domain;

public class User {
    private Long id;
    private String name;
    private Integer age;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + ''' +
                ", age=" + age +
                '}';
    }

}

5>监控处理类

package com.langfeiyes.sb.handler;

import com.langfeiyes.sb.domain.User;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;

@Component
@CanalTable(value = "user") //监听数据库的表名
public class UserHandler implements EntryHandler<User> {
 
    @Override
    public void insert(User user) {
        System.err.println("添加:" + user);
    }
 
    @Override
    public void update(User before, User after) {
        System.err.println("改前:" + before);
        System.err.println("改后:" + after);

    }
    @Override
    public void delete(User user) {
        System.err.println("删除:" + user);
    }
}

6>启动类

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

7>测试

七、canal-adapter适配器

除了通过编写java项目来消费canal数据外,还可通过canal提供的canal-adapter 通过配置来实现消费数据并将数据同步到其他中间件。

canal-adapter启动器本质上也是一个SpringBoot项目。