Canal入门教程:基于MySQL Binlog的实时数据同步与缓存一致性实践
说实话我第一次听说 Canal 这个名字的时候第一反应是“这又是什么中间件”。但真正花了一个下午把它跑通之后才发现这玩意儿对于搞后端、搞数据同步的人来说简直是解放生产力的利器。如果你正在做缓存和数据库的一致性同步、要把 MySQL 的数据实时同步到 Elasticsearch、或者想把数据库的每次增删改都推送给下游系统那 Canal 基本是你绕不开的一个工具。这篇东西就是给完全没用过 Canal 的兄弟准备的从零开始把它是什么、怎么跑起来、会遇到什么坑一次说清楚。1. 别被名字吓到Canal 到底是个啥1.1 从“缓存和数据库不一致”这个经典问题说起我先举个很常见的场景。你的业务里有张订单表用户下单之后订单状态不断变化从“待支付”到“已支付”再到“已发货”。前端为了查询快会把订单信息丢一份到 Redis 缓存里。但问题来了数据库的订单状态变了缓存里的那份怎么同步第一种方案是业务代码里双写先更新数据库再删缓存或者更新缓存。这么做简单但问题是代码侵入性太强。你每个写操作的地方都得写一套同步逻辑一旦漏掉一个缓存和数据库就永久不一致了。而且如果你们是微服务架构订单服务和优惠券服务都要改订单状态那双写逻辑散落在各个服务里迟早维护到崩溃。第二种方案是定时任务扫表每隔几秒查一下有没有变化的记录。这个方案也有毛病实时性差是一方面更重要的是你得给表加一个 update_time 字段靠它来捞增量。如果数据结构复杂一点或者有分库分表扫起来就很痛苦。第三种方案也是我目前见过最优雅的就是通过 Canal 监听 MySQL 的 binlog数据库里任何一行数据发生变化Canal 都能第一时间感知到然后推给你。你只需要关心“收到变更之后要干嘛”根本不用管“怎么发现变更”。说白了Canal 就是 MySQL 变更的“传声筒”和“广播站”。1.2 Canal 在中间件家族里的角色定位Canal 的全称很长我也不背了你只需要记住它诞生于阿里最开始是为了解决异地机房数据同步的问题。它的核心能力就是把自己伪装成一个 MySQL 的从库向主库请求 binlog拿到 binlog 之后解析成你能看懂的结构化数据再通过各种方式吐给你。注意它的位置——它不替代消息队列不是数据库也不是 ETL 工具。它是一个管道。它做的事情非常单一就是“监听 解析 分发”。正因为专注所以它在数据同步这条链路上非常可靠。生产环境里有人拿它同步数据到 Elasticsearch有人拿它同步到 Redis有人拿它同步到 HBase还有人拿它对接 Kafka 做数据总线。无论下游是谁Canal 的那一段逻辑都是一样的它只负责告诉你“哪张表的哪一行发生了什么操作前后的值是什么。”如果你现在看到这里还是有点抽象那你就把 Canal 想象成一个坐在数据库旁边的“记事员”。数据库每做一次增删改它就在小本本上记录下来然后按照你的要求把这条记录念给你听。你不用趴在数据库上一直盯着只需要等它主动告诉你。1.3 什么人需要认真学 Canal我总结下来下面这几类人是最该学 Canal 的后端开发尤其是做了几年业务、开始接触缓存、消息队列、搜索服务的人。数据研发和数据运维需要从业务库同步数据到数据仓库或做实时计算的人。架构师在设计数据一致性方案、异构数据同步方案时Canal 几乎是必选项。刚入行的初中级程序员如果你想在简历上写一个有含金量的亮点把 Canal 的原理吃透、亲手搭一个 Demo面试时够你和面试官聊很久了。反过来说如果你只是写简单的 CRUD不涉及跨系统数据交互那 Canal 对你暂时没什么用。但只要你负责的系统开始变复杂数据开始需要被多个系统消费你就绕不开它。2. 快速上手前必须搞懂的原理它一直在装“从库”2.1 先花五分钟回忆一下 MySQL 主从同步Canal 的原理要说透绕不开 MySQL 的主从复制机制。主从复制很简单主库把数据变更记录写到 binlog 日志文件里从库用一个 I/O 线程把主库的 binlog 拉过来写入自己的 relay log再由 SQL 线程把 relay log 里的变更重放到自己身上。这样主库和从库的数据就保持一致了。很多第一次接触 Canal 的人会问“那我是不是直接搭一个从库然后读数据就行了”理论上可以但代价太大了。为了拿到变更数据你得维护一整套从库环境存储成本、运维成本都上去了。而且从库的数据是全量的你其实只想知道“谁变了、变了什么”并不需要存一份完整副本。Canal 的精明之处就在这它把自己伪装成一个从库去主库注册主库就真的把它当成自己的小弟兢兢业业地给它推送 binlog。但 Canal 拿到 binlog 之后并不去“重放”而是直接解析把里面的事件提取出来格式化之后再交给下游。主库一根线程推送Canal 解析完之后丢了就行不需要落地存储成本极低。2.2 一个“假的从库”是怎么骗过主库的你可能会问“Canal 又不是真正的 MySQL 实例它怎么跟主库通信”答案是Canal 自己实现了一套 MySQL 主从交互协议。它跟主库建立 TCP 连接之后发送一个类似“我要当你的从库”的握手请求主库验证完账号权限就会从这个连接上源源不断地把 binlog 推给 Canal。Canal 再把字节流按照 binlog 的格式去解析还原出原始的表结构信息和数据变更内容。这里有一个特别容易踩的坑主库推送 binlog 的格式有三种STATEMENT、ROW、MIXED。Canal 强制要求 ROW 模式。为什么因为 STATEMENT 模式记录的是 SQL 语句本身比如 “UPDATE user SET name张三 WHERE id1”这种格式下 Canal 没法得知修改后的整行长什么样只有 ROW 模式会记录“哪一行、哪些列、旧值是什么、新值是什么”。所以你在配置 MySQL 的时候binlog_format 必须设成 ROW最好连带 binlog_row_image 也设成 FULL否则某些场景下拿不到完整的镜像数据。2.3 Canal 的关键组件和部署形态Canal 的部署形态有一个很清晰的层次。最底层是 server它对应一个 Canal 进程里面可以配置多个 instance。每个 instance 对应一个独立的“数据同步任务”包含一个完整的数据管道。你可以这样理解server 是车间instance 是车间里的一条条流水线。每条流水线都有自己独立的配置监听不同的 MySQL 实例互不干扰。生产环境里常见做法是一台服务器上跑一个 Canal server里面根据业务拆成多个 instance每一个 instance 负责同步某几个库表。另外Canal 还分为单机模式和集群模式。单机模式就是一个 server 负责所有 instance配置简单适合学习和中小业务。集群模式引入了 ZooKeeper由它来协调多个 Canal server实现 instance 的 failover。这里我先不多展开后面讲生产实践时再细说。新手入门阶段你只需要先把单机模式跑通原理理解了往集群扩展就是水到渠成的事。3. 手把手环境准备从 MySQL 开启 binlog 到 Canal 跑起来3.1 MySQL 侧配置开启 binlog 并确认参数在你动手下载 Canal 之前先确认你的 MySQL 版本。Canal 对 MySQL 版本的兼容性整体不错我实测过 5.7 和 8.0 都能正常跑。但 8.0 以上需要注意加密认证插件的问题稍后我会在常见问题里专门说。首先编辑 MySQL 的配置文件在 [mysqld] 段下加入以下几行[mysqld] log-binmysql-bin binlog-formatROW binlog-row-imageFULL server-id100这里说明一下每个参数的意义log-bin 开启 binlog 日志文件名前缀随便起我用的是 mysql-bin。binlog-format 必须设置为 ROW这个前面已经解释过了。binlog-row-image 设置为 FULL这样 binlog 里会记录所有列的旧值和新值方便 Canal 解析。server-id 绝对不能省略主从复制协议里每个参与复制的节点都需要一个唯一的 IDCanal 伪装从库时也需要一个不同的 server-id。改完配置之后重启 MySQL 让它生效。然后登录 MySQL执行下面这条命令确认一下SHOW VARIABLES LIKE binlog_format;看到结果是 ROW就说明配置生效了。3.2 创建专用账号并授权这一步很多人会忽略图省事直接用了 root 账号。我不建议这么干因为 Canal 要的操作权限其实非常小给它一个专用账号反而更容易排查问题。执行下面的 SQLCREATE USER canal% IDENTIFIED BY canal; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO canal%; FLUSH PRIVILEGES;SELECT 权限是让 Canal 在解析 binlog 时如果需要获取表结构元数据可以查表。REPLICATION SLAVE 权限是让它能够伪装成从库请求 binlog。REPLICATION CLIENT 权限是让它能够执行 SHOW MASTER STATUS 之类的命令获取当前 binlog 的位置。如果你用的是 MySQL 8.0创建用户之后大概率还要再授权一次因为 8.0 的默认认证插件是 caching_sha2_passwordCanal 对它的支持在低版本上不够友好。最稳妥的办法是在创建用户时指定认证插件CREATE USER canal% IDENTIFIED WITH mysql_native_password BY canal;如果你是在 MySQL 8.0 里已经创建好了用户也可以修改ALTER USER canal% IDENTIFIED WITH mysql_native_password BY canal;这样能省掉一堆客户端的握手兼容问题。3.3 下载 Canal 服务端部署包环境准备完毕接下来去下载 Canal 的部署包。你不需要自己编译源码官方直接提供了编译好的 release 包。下载的时候注意版本选择目前比较主流的是 1.1.x 系列对新手来说直接挑最新的稳定版本就行。下载完解压到你喜欢的目录我习惯放在 /opt/canal 下目录结构大概是这样的bin/ # 启动/停止脚本 conf/ # 全局配置和实例配置 lib/ # Canal 依赖的 jar 包 logs/ # 日志输出目录conf 目录下有一个 canal.properties这是全局配置还有一个 example 目录这是一个叫 example 的实例模板里面放着 instance.properties这个文件决定了 Canal 到底监听哪个 MySQL。后面要新增同步任务就是复制 example 目录改名字再改配置。解压完成之后先别急着启动还有两个配置文件需要根据你的环境修改。3.4 修改 canal.properties 和 instance.properties先打开 conf/canal.properties里面有很多参数新手阶段只要关注这几个关键项。# 当前 Canal server 的 ID多实例时不能重复 canal.id1 # 是否开启内置的 admin 服务新手可关掉 canal.admin.managerfalse # 端口号Canal server 暴露给客户端的 TCP 端口默认 11111 canal.port11111 # 注册到 ZooKeeper 的配置单机模式不需要改 canal.zkServers然后是 conf/example/instance.properties 这个文件它才是真正决定连哪个 MySQL 的关键。需要改的核心项如下# MySQL 主库地址和端口 canal.instance.master.address127.0.0.1:3306 # 前面创建好的账号和密码 canal.instance.dbUsernamecanal canal.instance.dbPasswordcanal # 需要监听的库表注意它只接受正则表达式 # 下面这行表示监听所有库的所有表 canal.instance.filter.regex.*\\..*这里有个细节filter.regex 的正则分隔符是\\..就是库名加一个点加表名。比如只想监听 test 库下的 user 表就写成canal.instance.filter.regextest\\.user.*如果想监听 test 库下所有表就写成canal.instance.filter.regextest\\..*这个正则语法有点绕我在第一次配置时在这里卡了一段时间。后来索性先配了监听所有表跑通了再去收紧范围。3.5 首次启动和日志观察配置完成后就可以启动了。在 bin 目录下执行sh startup.sh启动后第一件事不是去看日志而是先确认进程起来了没有ps -ef | grep canal然后去看 logs/canal.log 和 logs/example/example.log。新手第一次跑大概率会在这两个日志里发现一些报错。常见的无非是账号密码不对、权限不足、连接超时这几类。如果看到类似start successful的字样就说明 Canal 已经成功连接上 MySQL伪装成从库开始等待 binlog 推送了。4. 一个 5 分钟跑通的最小 Demo从 MySQL 到终端4.1 目标监听一张表的变更并在终端打印环境跑通了我们来做一次完整的端到端实验。实验目标很简单对某张表执行 UPDATE 操作在 Canal 收到变更后用一段客户端程序把变更内容打印出来。在 MySQL 里先建一张简单的表CREATE DATABASE IF NOT EXISTS canal_demo DEFAULT CHARSET utf8mb4; USE canal_demo; CREATE TABLE user_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64), age INT, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );然后确认一下 instance.properties 里的过滤规则能匹配上这张表。按照前面的配置监听所有库就不用额外改了。如果你改了过滤规则记得重启 Canal。4.2 客户端环境准备Python 还是 Java 你选一个Canal 官方提供了 Java 客户端依赖这是最正统的方式。但很多非 Java 体系的人不熟悉 Maven 工程这里我推荐用 Python 的 canal 客户端包它足够轻量适合快速验证。pip install canal-python这个包的使用方式非常直观核心代码是连接 Canal server 的地址和端口订阅你想要的 instance然后循环拉取消息解析出每一条数据变更。我们写一个最简单的脚本from canal.client import CanalClient from canal.protocol import EntryType def parse(entry): # 只处理行数据变更 if entry.entry_type ! EntryType.ROWDATA: return store entry.store_value for row_change in store.row_changes: row row_change.row for column in row.after_columns: print(f列名{column.name}, 新值{column.value}, 是否主键{column.is_key}) client CanalClient(host127.0.0.1, port11111, username, password) client.subscribe(client_id1001, destinationexample, filter.*\\..*) client.start() while True: message client.get(1000) if message is None: continue entries message.entries for entry in entries: parse(entry)这段脚本的逻辑很直白。Canal 服务端把变更事件封装成 Entry 对象我们取出来之后遍历里面每一行变更再把变更后的字段打印出来。4.3 在 MySQL 里执行一次 UPDATE 并观察结果脚本跑起来之后回到 MySQL 执行INSERT INTO user_info (name, age) VALUES (张三, 25); UPDATE user_info SET age 26 WHERE name 张三;再去终端看你应该能看到类似这样的输出列名id, 新值1, 是否主键True 列名name, 新值张三, 是否主键False 列名age, 新值26, 是否主键False 列名update_time, 新值2025-01-01 12:00:00, 是否主键False到这里你的 Canal 已经成功把 MySQL 的变更实时拿到手里了。整个链路是MySQL 产生的 binlog 被 Canal 伪装从库接收解析成结构化字段再通过 TCP 推送到客户端程序。4.4 数据变更的完整形态before 和 after这里我要强调一个知识点。上面打印的只是变更后的值在真实的 Canal 数据里每一条行变更都包含 before 和 after 两组字段。before 是这行数据变更前的样子after 是变更后的样子。如果你拿 Canal 做数据同步这个 before 和 after 的对比非常关键。举个例子如果你要把订单状态变更推送给短信服务你需要先判断 after.state 是不是“已发货”然后才能决定要不要发短信。如果你要把变更同步到 Elasticsearch绝大多数情况下只需要 after 这一组数据直接覆盖写文档即可。看 canal-python 这个包的源码你会发现它每个 row 下面有 before_columns 和 after_columns 两个列表。如果你只需要 after就跟我上面写的那样遍历 after_columns 就对了。有些同事会疑惑“我明明更新了一行为什么收到的消息里没有旧值”这就要回去检查 binlog_row_image 配置了如果设置成了 MINIMALbinlog 里只记录被修改的列和主键列旧值是拿不到的。所以生产环境同步场景强烈建议 FULL。5. 我踩过的坑和排查思路建议先收藏5.1 binlog 格式没生效数据一直收不到这是新手最容易翻车的问题。MySQL 的配置文件改了但你在配置文件的写法有误或者改错了实例导致实际运行的 MySQL 并没有加载到 ROW 格式。你可以在 MySQL 里执行SHOW VARIABLES LIKE binlog_format;如果结果是 STATEMENT 而不是 ROW别怀疑就是配置没生效。检查一下配置文件路径到底是 /etc/my.cnf、/etc/mysql/my.cnf 还是 /data/mysql/my.cnf不同安装方式路径不一样。5.2 收到消息一直报 Insert 而不是 Update有位朋友跑通之后跑来问我“我明明执行的是 UPDATE为什么 Canal 一直给我 INSERT”这个问题很有意思排查到最后发现他更新语句执行时一张表被清空了又重新插入在 binlog 里就呈现为 DELETE INSERT。Canal 是按 binlog 事件来解析的它不管你在 SQL 层面写了什么只看 binlog 里实际记录了什么。所以如果你要做精确的 CUD 分类不要看 SQL 语句要以 binlog 事件为准。5.3 客户端连接收不到数据但 Canal 日志显示已经推送了这个情况我也遇到过。Canal server 的日志显示有客户端连接成功也能正常发送数据但客户端脚本就是干等着不打印。排查方法是先检查客户端的 filter 正则是否跟服务端的配置冲突。当时我是把服务端 filter 设置了只监听某个库客户端却用.*\..*去订阅结果根本匹配不上。这个 filter 的概念很像消息队列里的 topic 订阅关系服务端和客户端两侧要一致才行。5.4 消费端太慢导致 Canal 延迟增大如果你接的下游处理比较慢而 MySQL 的变更又很频繁你可能会发现数据同步延迟越来越大。这里要明白 Canal 的机制它从 binlog 增量拉取数据之后会先放到一个内存缓冲区里客户端消费走后才会从缓冲区删除。如果客户端消费跟不上缓冲区满了Canal 就会停止拉取新数据MySQL 的 binlog 就会积压。解决办法不是去调大缓冲区而是优化消费逻辑。比如批量拉取而不是一条条处理。canal-python 的get(num)方法你可以一次性地把参数调大比如get(2000)把拉取和处理改成批量模式吞吐量会有明显提升。5.5 MySQL 主从切换后 Canal 失联一旦你的 MySQL 做了主从切换Canal 在配置里写死的 master.address 就失效了。它会疯狂报连接失败的错。处理方案有两个要么在 Canal 前面再挂一层域名或负载均衡切换时只改域名指向要么用 Canal 的集群模式配合 ZooKeeper让 Canal 自己感知主从变化。新手单机实验阶段遇到这个问题直接改配置重启就完事了。6. 从 Demo 走向生产Canal 能玩出的花活6.1 把消息扔进 Kafka 或 RocketMQ实现解耦Demo 版本是把数据直接推给客户端程序生产环境很少这么干。更合理的架构是Canal 接入消息队列业务系统从消息队列消费。举个例子你有一个搜索服务它要的数据来源于订单库。传统做法是下单服务在写完数据库之后主动调搜索服务的接口同步索引。这个方案耦合太重订单服务一旦异常搜索数据就断了。引入 Canal 之后订单服务的代码不用任何改动Canal 检测到订单表变化把变更消息丢进 Kafka搜索服务以消费者的身份读 Kafka更新自己的索引。订单服务、Canal、搜索服务三者互相不知晓对方的存在整个数据链路完全解耦。配置 Kafka 客户端其实不难Canal 的 protocol 模块提供了 CanalMQProducer在 canal.properties 里指定 producer 类型为 kafka配置好 Kafka 的地址然后在 instance 里开启 MQ 模式消息就会自动进入指定的 topic。6.2 缓存一致性用 binlog 驱动缓存刷新前面说到的缓存一致性问题Canal 提供了一个非常优雅的解法。你只需要在缓存更新的频道上做文章Canal 监听电商系统的商品表变更一旦商品价格发生变化消息消费端收到之后把 Redis 里对应的商品缓存删掉。用户下一次访问时缓存未命中再从数据库读一次并回填。注意这里用的是“删缓存”而不是“更新缓存”。为什么因为并发场景下更新缓存容易出现旧值覆盖新值的竞态问题而删缓存天然免疫这个问题读线程发现缓存没有就去数据库拿最新值然后回填所有线程拿到的都是最新数据。Canal 加缓存删除这套组合在业界已经是很成熟的做法了。6.3 异构数据同步MySQL 到 Elasticsearch 的实时管道现在很多系统的搜索功能都依赖 Elasticsearch但数据库是 MySQL。业务数据被写入 MySQL 之后必须尽快同步到 ES 里才能被搜到。如果靠业务代码里双写 ES等于给每个写接口都绑上一个重活接口响应时间直接恶化。用 Canal 之后数据同步对业务代码完全透明。ES 的索引结构跟 MySQL 的表结构可以不一样比如 MySQL 里用户表存的是 user_id、user_name、tag_idsES 里需要的是一个包含 tag_names 的扁平文档。那消息消费端就做一次数据转换把 Canal 传来的行数据改造成 ES 需要的文档再用 Bulk API 批量写入。这套方案我在模拟项目 X 里完整走过一遍实测下来几十万行数据能在秒级完成同步。6.4 进阶学习路径从会用到底层原理最后给想深入研究的人指个方向。第一步把 Canal 自身带的可观测性指标用起来比如通过 JMX 暴露的延迟指标、消费速率指标。第二步去看 Canal 的源码重点看 binlog 解析模块和分析模块理解它如何把 MySQL 的二进制协议翻译成一系列事件对象。第三步把 Canal 和 ZooKeeper 的集群模式搭一遍了解实例在节点故障时如何自动迁移。走到这一步你对分布式系统里“主从选举 故障转移”的套路也会理解得更深。另外如果你用的是阿里云等托管数据库建议先用控制台确认是否已经开放了 binlog 相关参数。某些托管数据库默认是不给开 binlog 的需要工单申请或者白名单配置。我最早差点在这上面栽了跟头还好提前确认了一下。写在最后的几句体会从配置 MySQL 到跑通第一个 Demo整个过程不会花你太多时间但后面做排查和调优时你一定会回来反复琢磨原理。我个人的感觉是Canal 属于那种“配置看起来很简单但真正理解它需要一点耐心”的工具。它不像 Web 框架那样写几行代码就能看到页面它工作在数据链路的底层一旦接通你就能真切感受到数据像水一样在整个系统里流动起来。还有一个小建议第一次实验时别一上来就监听所有库所有表。找个小库、建两张表、跑一个最小同步链路先让整条路走通再慢慢加规则。数据同步这行断断续续最坑人缓步推进才是最快的。