Canal实战:从原理到部署,轻松搞定MySQL数据实时同步

64 阅读11分钟

在分布式系统和微服务架构日益普及的今天,如何保证多系统间的数据一致性,以及如何将数据库的变更实时同步到缓存、搜索引擎或数据仓库中,是许多开发者面临的共同挑战。阿里巴巴开源的 Canal,正是解决这一痛点的高效利器。

1. Canal概述

1.1. 什么是Canal

Canal(发音 /kəˈnæl/,意为“管道/沟渠”)是阿里巴巴 B2B 公司,因为业务的特性,卖家主要集中在国内,买家主要集中在国外,所以衍生出了同步杭州和美国异地机房的需求,从 2010 年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。

Canal 是用 Java 开发的基于数据库增量日志解析,提供增量数据订阅&消费的中间件。

目前,Canal 主要支持了 MySQL 的 Binlog 解析,解析完成后才利用 Canal Client 来处理获得的相关数据。

Canal架构

1.2. MySQL的Binlog

1.2.1. 什么是Binlog

MySQL 的二进制日志可以说 MySQL 最重要的日志了,它记录了所有的 DDL 和 DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的。

1.2.2. Binlog分类

MySQL 的 Binlog 主要有三种格式,Canal 的解析精度很大程度上依赖于这些格式:

  • STATEMENT 格式:语句级,记录的是原始的 SQL 语句。优点是日志体积小,但如果 SQL 中包含 NOW()、UUID() 等非确定性函数,极易导致主从数据不一致。
  • ROW 格式(强烈推荐):行级,记录的是每一行数据的实际变更(变更前后的具体值)。优点是绝对不会出现主从不一致,解析精度 100%,是 Canal 生产环境的首选格式;缺点是批量更新时日志体积较大。
  • MIXED 格式:混合了上述两种模式,由 MySQL 自动判断使用哪种格式记录。

1.3. Canal工作原理

1.3.1. MySQL主从复制过程

  1. Master 主库将改变记录,写到二进制日志(Binary Log)中。
  2. Slave 从库向 MySQL Master 发送 dump 协议,将 Master 主库的 binary log events 拷贝到它的中继日志(relay log)。
  3. Slave 从库读取并重做中继日志中的事件,将改变的数据同步到自己的数据。

1.3.2. Canal工作原理

简单的说,就是把自己伪装成 MySQL 的 Slave,假装从 Master 复制数据。

Canal工作原理

1.4. 使用场景

更新缓存:

  1. 使用 Canal 实时同步更新业务数据库中的数据到缓存服务器。
  2. 业务在读取某些数据时,可以优先从缓存服务器中读取,未命中,再查询数据库。

抓取业务表的新增变化数据,用于制作实时统计。

2. Canal安装部署

2.1. MySQL准备

需要提前部署好 MySQL,并且做以下修改。

  1. 创建一个数据库和表,用于测试。
# 创建数据库
create database test_db;

# 创建表
CREATE TABLE `books` (
  `id` VARCHAR(64) NOT NULL COMMENT '主键ID',
  `name` VARCHAR(255) NOT NULL COMMENT '书名',
  `author` VARCHAR(255) DEFAULT NULL COMMENT '作者',
  `score` int DEFAULT NULL COMMENT '评分',
  `comment` VARCHAR(2000) DEFAULT NULL COMMENT '摘要',
  `publishTime` DATETIME DEFAULT NULL COMMENT '发布时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='书籍信息表';
  1. 修改 MySQL 的配置文件/etc/my.cnf。
service-id=1
log-bin=mysql-bin
binlog_format=row
binlog-do-db=test
# 如果开启多个数据库时,可以配置多个 binlog-do-db 即可
# 如果不配置,则表示所有数据库均开启 binlog
binlog-do-db=test_db
  1. 重启 MySQL 使配置生效。
sudo systemctl restart mysqld
  1. 到/var/lib/mysql/目录下,查看初始文件大小,默认应该为157。
-rw-r-----. 1 mysql mysql      157 9月  17 20:44 mysql-bin.000001
  1. 测试 binlog 是否成功开启。
INSERT INTO books values ('1001', '测试1', 'zhangsan', 9, '这是一本书', '2026-05-05 09:00:00');

再次查看/var/lib/mysql/目录下文本大小,正常情况下,该文件应该会增大。

-rw-r-----. 1 mysql mysql      463 5月  10 20:44 mysql-bin.000001
  1. 连接数据库,查看 binlog_format 是否修改。
SHOW VARIABLES LIKE 'binlog_format' ;

查询结果:

+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
  1. 创建用户名,并赋权限,提供给 Canal 访问 MySQL 使用
CREATE USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'Canal@123.';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';

2.2. Canal-deploye部署

Canal 下载地址:github.com/alibaba/can…。

当前使用 Canal 版本:canal-1.1.8。

  1. 上传 Canal 安装包,并解压到指定目录。
mkdir /opt/module/canal-deployer-1.1.8
tar -zxvf canal.deployer-1.1.8.tar.gz -C /opt/module/canal-deployer-1.1.8/
  1. 修改conf/canal.properties配置文件。
# tcp bind ip
canal.ip = 192.168.8.102
# register ip to zookeeper
canal.register.ip = 192.168.8.102
canal.port = 11111
canal.metrics.pull.port = 11112

# canal server 模式,这里选择 tcp
# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ
canal.serverMode = tcp

# canal 实例存放目录
canal.destinations = example
# conf root dir
canal.conf.dir = /opt/module/canal-deployer-1.1.8/conf

注意:这里canal.serverMode = tcp,就是下一章节“TCP模式实现”配置文件。

  1. 修改conf/example/instance.properties实例配置文件。
#canal 作为 mysql 从机,slaveId 配置只要与 mysql 的 service-id 不同即可
## mysql serverId , v1.0.26+ will autoGen
canal.instance.mysql.slaveId=10

# 这个 tsdb 暂时用不到,可以设置为 false
# table meta tsdb info
canal.instance.tsdb.enable=false

# 访问 mysql 数据库的用户名/密码
# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=Canal@123.
canal.instance.connectionCharset = UTF-8

# 过滤表的正则表达式
# table regex
canal.instance.filter.regex=.*\..*
# table black regex
canal.instance.filter.black.regex=mysql\.slave_.*
  1. 启动 Canal-deployer。
./bin/startup.sh
  1. 查看日志,看 Canal 是否启动成功。
cat ./logs/canal/canal.log

可以看到:

2026-05-10 20:27:25.516 [main] INFO  com.alibaba.otter.canal.deployer.CanalStarter - ## start the canal server.
2026-05-10 20:27:25.559 [main] INFO  com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[192.168.8.102(192.168.8.102):11111]
2026-05-10 20:27:26.312 [main] INFO  com.alibaba.otter.canal.deployer.CanalStarter - ## the canal server is running now ......

2.3. Canal-admin部署

  1. 上传安装包,并解压到指定位置。
mkdir /opt/module/canal-admin-1.1.8
tar -zxvf canal.admin-1.1.8.tar.gz -C /opt/module/canal-admin-1.1.8/
  1. 连接数据库后,执行数据库脚本,脚本存放目录conf/canal_manager.sql。
# 进入到MySQL中
source /opt/module/canal-admin-1.1.8/conf/canal_manager.sql
  1. 为数据库canal用户赋值操作权限。
GRANT ALL PRIVILEGES ON canal_manager.* TO 'canal'@'%';
FLUSH PRIVILEGES;
  1. 修改conf/application.yaml配置文件。
spring.datasource:
  address: 192.168.8.102:3306
  database: canal_manager
  username: canal
  password: Canal@123.


canal:
  adminUser: admin
  # 注意,这里密码是需要给一个默认值的,否则报错
  # 但经过测试后发现,这里随便配置即可,web端账号密码都是 admin/123456
  adminPasswd: 123456
  1. 启动 canal-admin。
./bin/startup.sh

启动成功后,访问 Web 管理界面:http://192.168.8.102:8089,默认账号密码:admin / 123456。

Canal-admin Web界面

可以在“Server 管理”中,创建 canal-server,并且,管理该 Canal Server。

新建Server

3. TCP模式实现

3.1. 创建项目

  1. 创建 Maven 项目CanalDemo。
<groupId>org.example</groupId>
<artifactId>CanalDemo</artifactId>
  1. 添加依赖。
<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.7</version>
</dependency>

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.protocol</artifactId>
    <version>1.1.7</version>
</dependency>

3.2. Canal封装数据结构

在开始开发之前,这里说明一下 Canal 封装的数据结构。

Canal数据结构

3.3. 编写Canal客户端代码

package com.example.canal.demo;

import com.alibaba.fastjson2.JSONObject;
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.List;

/**
 * @Description TODO
 */
public class SimpleCanalClient {

    public static void main(String[] args) throws InterruptedException, InvalidProtocolBufferException {

        //获取canal连接对象
        CanalConnector connector = CanalConnectors.newSingleConnector(
            new InetSocketAddress("192.168.8.102", 11111), "example", "", "");

        while (true) {
            //获取连接
            connector.connect();
            //指定监测数据库
            connector.subscribe(".*\..*");
            //获取message数据,包含多个entry
            Message message = connector.get(100);
            //获取entry集合
            List<CanalEntry.Entry> entries = message.getEntries();
            //判断集合是否为空,如果非空开始解析数据,如果为空,等待一会然后再进行拉取
            if (entries.size() > 0) {
                for (CanalEntry.Entry entry : entries) {
                    //获取表名
                    String tableName = entry.getHeader().getTableName();
                    //获取类型
                    CanalEntry.EntryType entryType = entry.getEntryType();
                    //获取序列化后的数据
                    ByteString storeValue = entry.getStoreValue();
                    //判断当前entryType类型是否为ROWDATA
                    if (CanalEntry.EntryType.ROWDATA.equals(entryType)) {
                        System.out.println("当前 EntryType 为 " + entryType);
                        //反序列化数据
                        CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
                        //获取当前事件的操作类型
                        CanalEntry.EventType eventType = rowChange.getEventType();
                        //获取数据集
                        List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
                        //遍历数据集,并打印数据集
                        for (CanalEntry.RowData rowData : rowDatasList) {
                            //获取变化前数据
                            JSONObject beforeData = new JSONObject();
                            List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
                            for (CanalEntry.Column column : beforeColumnsList) {
                                beforeData.put(column.getName(), column.getValue());
                            }

                            //获取变化前数据
                            JSONObject afterData = new JSONObject();
                            List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
                            for (CanalEntry.Column column : afterColumnsList) {
                                afterData.put(column.getName(), column.getValue());
                            }

                            //TODO 业务处理
                            //打印数据
                            System.out.println("TableName: " + tableName +
                                    ",EventType: " + eventType +
                                    ",Before: " + beforeData +
                                    ",After: " + afterData);

                        }
                    } else {
                        System.out.println("当前 EntryType 为 " + entryType + ", 非 ROWDATA 类型");
                    }
                }
            } else {
                System.out.println("等待1秒... ...");
                Thread.sleep(1000);
            }

        }
    }
}

3.4. 启动测试

启动后,会进入循环等待状态,当有数据修改时,就会对应输出相关的信息。

当数据新增时,会输出以下信息:

TableName: books
    , EventType: INSERT
    , Before: {}
    , After: {"id":"1002","name":"测试2","author":"zhangsan","score":"7","comment":"这是一本书2222","publishTime":"2026-05-05 09:00:00"}

当数据更新时,会输出以下信息:

TableName: books
    , EventType: UPDATE
    , Before: {"id":"1001","name":"测试1","author":"zhangsan","score":"9","comment":"这是一本书","publishTime":"2026-05-05 09:00:00"}
    , After: {"id":"1001","name":"test-book111","author":"zhangsan","score":"9","comment":"这是一本书","publishTime":"2026-05-05 09:00:00"}

当数据删除时,会输出以下信息:

TableName: books
    , EventType: DELETE
    , Before: {"id":"1001","name":"test-book111","author":"zhangsan","score":"9","comment":"这是一本书","publishTime":"2026-05-05 09:00:00"}
    , After: {}

在真实的场景中,我们就可以在输出位置写入符合业务场景的相关代码,即可实现数据的实时增量同步。

4. Kafka模式实现

4.1. 修改配置文件

  1. 修改配置文件conf/canal.properties配置文件。
# tcp bind ip
canal.ip = 192.168.8.102
# register ip to zookeeper
canal.register.ip = 192.168.8.102
canal.port = 11111
canal.metrics.pull.port = 11112

# 修改为 kafka 模式
# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ
canal.serverMode = kafka

# canal 实例存放目录
canal.destinations = example
# conf root dir
canal.conf.dir = /opt/module/canal-deployer-1.1.8/conf


# 配置 kafka 地址
kafka.bootstrap.servers = 192.168.8.102:9092:192.168.8.104:9092:192.168.8.104:9092
  1. 修改conf/example.instance.properties实例配置文件。
#canal 作为 mysql 从机,slaveId 配置只要与 mysql 的 service-id 不同即可
## mysql serverId , v1.0.26+ will autoGen
canal.instance.mysql.slaveId=10

# 这个 tsdb 暂时用不到,可以设置为 false
# table meta tsdb info
canal.instance.tsdb.enable=false

# 访问 mysql 数据库的用户名/密码
# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=Canal@123.
canal.instance.connectionCharset = UTF-8

# 过滤表的正则表达式
# table regex
canal.instance.filter.regex=.*\..*
# table black regex
canal.instance.filter.black.regex=mysql\.slave_.*

# 配置topic
# mq config
canal.mq.topic=canal_test
# dynamic topic route by schema or table regex
#canal.mq.dynamicTopic=mytest1.user,topic2:mytest2\..*,.*\..*
# 分区
canal.mq.partition=0
# hash partition config
#canal.mq.enableDynamicQueuePartition=false
# 总分区数
#canal.mq.partitionsNum=3
#canal.mq.dynamicTopicPartitionNum=test.*:4,mycanal:6
#canal.mq.partitionHash=test.table:id^name,.*\..*

4.2. 启动消费者

Canal 获取到数据后,直接发送到 Kafka,所以需要先启动一个 Kafka 的消费者。

./bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic canal_test

4.3. 重启Canal

./bin/startup.sh

查看日志,确保 Canal 成功启动:

cat ./logs/canal/canal.log

2026-05-10 21:21:09.649 [main] INFO  com.alibaba.otter.canal.deployer.CanalStarter - ## start the canal server.
2026-05-10 21:21:09.743 [main] INFO  com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[192.168.8.102(192.168.8.102):11111]
2026-05-10 21:21:11.400 [main] INFO  com.alibaba.otter.canal.deployer.CanalStarter - ## the canal server is running now ......

4.4. 修改数据

修改 Canal 连接数据库数据,看 Kafka 消费端能否消费到对应数据。

# 新增
{"data":[{"id":"1001","name":"测试1","author":"zhangsan","score":"7","comment":"这是一本书1","publishTime":"2026-05-05 09:00:00"}],"database":"test_db","es":1778752646000,"gtid":"","id":2,"isDdl":false,"mysqlType":{"id":"varchar(64)","name":"varchar(255)","author":"varchar(255)","score":"int","comment":"varchar(2000)","publishTime":"datetime"},"old":null,"pkNames":["id"],"sql":"","sqlType":{"id":12,"name":12,"author":12,"score":4,"comment":12,"publishTime":93},"table":"books","ts":1778752646568,"type":"INSERT"}

# 修改
{"data":[{"id":"1002","name":"test-book222","author":"zhangsan","score":"7","comment":"这是一本书2222","publishTime":"2026-05-05 09:00:00"}],"database":"test_db","es":1778752665000,"gtid":"","id":3,"isDdl":false,"mysqlType":{"id":"varchar(64)","name":"varchar(255)","author":"varchar(255)","score":"int","comment":"varchar(2000)","publishTime":"datetime"},"old":[{"name":"测试2"}],"pkNames":["id"],"sql":"","sqlType":{"id":12,"name":12,"author":12,"score":4,"comment":12,"publishTime":93},"table":"books","ts":1778752665227,"type":"UPDATE"}

# 删除
{"data":[{"id":"1002","name":"test-book222","author":"zhangsan","score":"7","comment":"这是一本书2222","publishTime":"2026-05-05 09:00:00"}],"database":"test_db","es":1778752672000,"gtid":"","id":4,"isDdl":false,"mysqlType":{"id":"varchar(64)","name":"varchar(255)","author":"varchar(255)","score":"int","comment":"varchar(2000)","publishTime":"datetime"},"old":null,"pkNames":["id"],"sql":"","sqlType":{"id":12,"name":12,"author":12,"score":4,"comment":12,"publishTime":93},"table":"books","ts":1778752672770,"type":"DELETE"}

5. 写在最后

到这里,我们就完成了 Canal 从原理认知、实战部署到两种核心模式实战的全过程。Canal 作为一个强大的中间件,在实际落地时往往还会遇到各种各样的“坑”,比如 MySQL 8.0 的密码兼容问题、海量 Binlog 带来的内存压力等。如果你在搭建或使用的过程中遇到了什么问题,或者有更好的优化方案,欢迎在评论区留言,我们一起交流探讨!如果觉得这篇文章对你有帮助,别忘了点赞收藏,我们下期见!