主题
Canal 数据同步
一句话:Canal 把自己伪装成 MySQL 从库,拉取 Binlog 并解析成结构化变更事件投给下游,换来业务零侵入的近实时同步,代价是位点、幂等和表结构变更都要自己兜。
适用版本:Canal 1.1.7 / 1.1.8(1.1.8 于 2025-01 发布,原生支持 MySQL 8 的
caching_sha2_password);MySQL 8.0。更早的 Canal 版本连接 MySQL 8 需要额外认证配置。版本细节以官方 Release Notes 为准。
为什么需要
业务只写 MySQL,但缓存、搜索、数仓、对账系统都需要同一份数据。常见做法各有问题:
| 方式 | 问题 |
|---|---|
| 定时扫表 | 延迟大、扫全表压力大、感知不到 DELETE |
| 业务双写 | 分布式事务难、代码耦合、容易漏写 |
| 触发器 | 侵入业务表、维护困难、性能差 |
| 订阅 Binlog | 业务零侵入、近实时、能看到完整前后镜像 |
Canal 就是最后一种:它复用 MySQL 的主从协议,对 Master 来说只是多了一个从库。Binlog 本身的分层与两阶段提交见 04-Redo 与 Binlog。
核心机制
伪装成从库
Canal 对 Master 来说只是一个多了 server_id 的从库:走 dump 协议拉 Binlog,解析出 ROW 事件后一路投给下游。
- Master 必须有
log_bin=ON、binlog_format=ROW、全局唯一server_id。 - Canal 用的账号需要
REPLICATION SLAVE、REPLICATION CLIENT,以及读取表的SELECT。 - Canal 解析的是 ROW 事件,所以能拿到变更前后的列值;STATEMENT 格式拿不到完整行镜像。
事件形态
json
{"type":"UPDATE","database":"order_db","table":"t_order",
"before":{"id":1001,"status":0},"after":{"id":1001,"status":1},
"ts":1718800001000}INSERT 只有 after,DELETE 只有 before,UPDATE 两者都有。下游按主键做 upsert/delete 即可,不需要关心 SQL 长什么样。
位点与投递语义
Canal Server 记录消费到的 Binlog 位点(file + position 或 GTID),Client 确认后推进。默认是 at-least-once:网络重试、Client 重启都可能重复投递,下游必须幂等。
幂等的常见做法:
- 按主键 upsert(
INSERT ... ON DUPLICATE KEY UPDATE),重复事件覆盖同一行。 - 用
(binlog_file, binlog_position)或业务版本号去重。 - 删除操作用幂等删除,不要假设「一定存在」。
MySQL 8 的认证坑
MySQL 8.0 默认认证插件是 caching_sha2_password。Canal 1.1.7 及更早版本对它支持不完整,常见报错是 Public Key Retrieval is not allowed 或认证失败。两条路:
- 升级到 Canal 1.1.8+:原生支持
caching_sha2_password,推荐。 - 临时兼容:把 Canal 账号改成
mysql_native_password,或在 JDBC 参数里加allowPublicKeyRetrieval=true&useSSL=false。
注意 MySQL 8.4 默认不再启用 mysql_native_password,所以长期方案只能是升级 Canal,而不是一直用旧插件。
最小上手
MySQL 侧(8.0):
sql
-- my.cnf: log_bin=mysql-bin, binlog_format=ROW, server_id=1
CREATE USER 'canal'@'%' IDENTIFIED WITH caching_sha2_password BY 'ChangeMe_123';
-- 8.0.22+ 推荐 REPLICATION REPLICA(REPLICATION SLAVE 已弃用);8.4 以官方文档为准
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;Canal 侧 instance.properties:
properties
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=ChangeMe_123
canal.instance.filter.regex=order_db\\..*
canal.instance.connectionCharset=UTF-8
# 1.1.7 及更早连接 MySQL 8,可显式指定 JDBC URL:
# canal.instance.master.jdbc.url=jdbc:mysql://127.0.0.1:3306/order_db?useSSL=false&allowPublicKeyRetrieval=true启动后看 logs/canal/canal.log 与 logs/order_db/order_db.log,出现 start successful 且位点开始推进即跑通。
生产动作
- 位点持久化:默认存本地文件;多实例/容器化要配 ZooKeeper 或外部存储,否则重建实例会从旧位点重放或丢位点。
- HA:Canal 1.1.x 用 ZooKeeper 做多 Server 选主,只有主实例拉 Binlog;别让两个实例用同一个
server_id同时订阅。 - Binlog 保留期:
binlog_expire_logs_seconds(8.0 默认 30 天)必须大于「Canal 最长中断时间 + 排障时间」;否则位点对应的 Binlog 已被清理,只能重新全量。 - 延迟监控:看 Canal 当前位点与 Master 最新位点的差值,以及下游消费 lag;延迟先查大事务和下游写入慢。
- 大事务:一个事务更新百万行会生成大量事件,Canal 解析和下游消费都会积压;业务侧拆小事务。
- DDL:Canal 能解析 DDL,但下游表结构不会自动跟着变;上线 DDL 要同步通知下游并做兼容。
- 下游幂等:至少一次投递 + 重试,必须按主键 upsert/delete;不要用「事件只来一次」的假设写代码。
边界
- 只支持 MySQL 系:PostgreSQL、MongoDB 要用 Debezium、Flink CDC 等方案。
- 不是全量迁移工具:历史数据仍要全量导出 + 增量追平;Canal 只负责增量。
- 不是 exactly-once:投递语义是至少一次,幂等必须下游自己做。
- 强依赖 ROW 格式:STATEMENT 格式解析不出完整行镜像;改格式前先确认 Canal 链路。
- 表结构变更仍需治理:加列、改类型、改分片键都可能让下游映射失败。
讲清楚
Canal 的价值是把「业务双写」换成「订阅 Binlog」:它伪装成 MySQL 从库,用 dump 协议拉 ROW 事件,解析成前后镜像投给下游,业务代码只写 MySQL。代价有三块:位点要持久化,否则重启会重放或丢数据;投递是至少一次,下游必须按主键幂等;表结构和 Binlog 保留期要一起治理,否则位点过期或字段对不上。MySQL 8 还要注意认证插件,1.1.7 及更早版本连 caching_sha2_password 会有坑,1.1.8 起原生支持,长期不要靠 mysql_native_password 兼容。