在分布式系统和微服务架构日益普及的今天,如何保证多系统间的数据一致性,以及如何将数据库的变更实时同步到缓存、搜索引擎或数据仓库中,是许多开发者面临的共同挑战。阿里巴巴开源的 Canal,正是解决这一痛点的高效利器。
1. Canal概述
1.1. 什么是Canal
Canal(发音 /kəˈnæl/,意为“管道/沟渠”)是阿里巴巴 B2B 公司,因为业务的特性,卖家主要集中在国内,买家主要集中在国外,所以衍生出了同步杭州和美国异地机房的需求,从 2010 年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。
Canal 是用 Java 开发的基于数据库增量日志解析,提供增量数据订阅&消费的中间件。
目前,Canal 主要支持了 MySQL 的 Binlog 解析,解析完成后才利用 Canal Client 来处理获得的相关数据。
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主从复制过程
- Master 主库将改变记录,写到二进制日志(Binary Log)中。
- Slave 从库向 MySQL Master 发送 dump 协议,将 Master 主库的 binary log events 拷贝到它的中继日志(relay log)。
- Slave 从库读取并重做中继日志中的事件,将改变的数据同步到自己的数据。
1.3.2. Canal工作原理
简单的说,就是把自己伪装成 MySQL 的 Slave,假装从 Master 复制数据。
1.4. 使用场景
更新缓存:
- 使用 Canal 实时同步更新业务数据库中的数据到缓存服务器。
- 业务在读取某些数据时,可以优先从缓存服务器中读取,未命中,再查询数据库。
抓取业务表的新增变化数据,用于制作实时统计。
2. Canal安装部署
2.1. MySQL准备
需要提前部署好 MySQL,并且做以下修改。
- 创建一个数据库和表,用于测试。
# 创建数据库
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='书籍信息表';
- 修改 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
- 重启 MySQL 使配置生效。
sudo systemctl restart mysqld
- 到
/var/lib/mysql/目录下,查看初始文件大小,默认应该为157。
-rw-r-----. 1 mysql mysql 157 9月 17 20:44 mysql-bin.000001
- 测试 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
- 连接数据库,查看 binlog_format 是否修改。
SHOW VARIABLES LIKE 'binlog_format' ;
查询结果:
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
- 创建用户名,并赋权限,提供给 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。
- 上传 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/
- 修改
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模式实现”配置文件。
- 修改
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_.*
- 启动 Canal-deployer。
./bin/startup.sh
- 查看日志,看 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部署
- 上传安装包,并解压到指定位置。
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/
- 连接数据库后,执行数据库脚本,脚本存放目录
conf/canal_manager.sql。
# 进入到MySQL中
source /opt/module/canal-admin-1.1.8/conf/canal_manager.sql
- 为数据库
canal用户赋值操作权限。
GRANT ALL PRIVILEGES ON canal_manager.* TO 'canal'@'%';
FLUSH PRIVILEGES;
- 修改
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
- 启动 canal-admin。
./bin/startup.sh
启动成功后,访问 Web 管理界面:http://192.168.8.102:8089,默认账号密码:admin / 123456。
可以在“Server 管理”中,创建 canal-server,并且,管理该 Canal Server。
3. TCP模式实现
3.1. 创建项目
- 创建 Maven 项目
CanalDemo。
<groupId>org.example</groupId>
<artifactId>CanalDemo</artifactId>
- 添加依赖。
<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 封装的数据结构。
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. 修改配置文件
- 修改配置文件
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
- 修改
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 带来的内存压力等。如果你在搭建或使用的过程中遇到了什么问题,或者有更好的优化方案,欢迎在评论区留言,我们一起交流探讨!如果觉得这篇文章对你有帮助,别忘了点赞收藏,我们下期见!