我第一次在Windows上装canal,是被一个很现实的问题逼的:本地开发机上跑着一套业务代码,需要实时感知某个测试库的表数据变化,把变更同步到本地Redis做缓存预热。方案倒是好选,直接用canal订阅MySQL的binlog,可一查教程,几乎全是Linux下用docker或systemd启动的,Windows玩家直接被晾在一边。其实canal官方的发行包里一直带着Windows启动脚本,说明Windows部署是被支持的,只不过没人愿意把那些细节讲透。这篇文章就专门讲Windows安装canal这件事,从环境准备、MySQL配置、服务端启动,到写一个能收到binlog事件的消费端Demo,一步步来,也把Windows下最容易踩的几个坑一并说清楚。不管你是要做本地联调,还是确实只能在Windows机器上部署,照着走一遍基本就能跑通。
1. canal是做什么的,为什么Windows本地也值得装
1.1 先理解canal的定位
canal是一个针对MySQL的增量日志解析中间件,核心工作可以概括为三件事:模拟MySQL从库、拉取binlog、把binlog解析成结构化的增量事件再交给下游程序。
MySQL本身有主从复制机制,从库通过dump协议向主库要binlog。canal做的其实就是同一个动作——它伪装成一个从库,向MySQL发起复制请求,MySQL把binlog源源不断推给它;canal收到之后按行解析,把每一条增删改都还原成"哪张表、哪一行、操作类型、变更前后的字段值"这样的数据结构。我经常用一个类比:binlog就像快递包裹,canal是小区门口的快递分拣员,把包裹拆开、贴好标签,再按不同的收件人地址挨家派送。这个"分拣派送"动作,正是增量数据从MySQL流向其他系统的关键一环。
1.2 Windows本地调试的典型场景
很多人第一反应是"canal这种中间件就该跑在Linux上",这没错,但Windows也有很实际的需求。
最常见的场景就是本地开发。开发机上连着测试环境的MySQL,想验证缓存更新、索引同步或者数据异构的逻辑,这时候直接在Windows上把canal跑起来,连上同一个测试库,比自己另起一台虚拟机要省事得多。另一个场景是内网里确实只有Windows服务器,或者运维体系以Windows为主,那canal作为一个小型Java进程部署在Windows上也完全能扛住中等规模的增量订阅。还有人纯粹是先上手学习,想把canal的原理和链路摸清楚,Windows本地环境比Linux虚拟机直观,日志、断点、调试都方便。
1.3 安装包选择:deployer才是我们需要的那一个
canal项目在发布时会提供多个安装包,常见的有deployer、adapter、admin。新手第一次下载容易被名字绕晕,这里直接给结论:如果只做binlog增量订阅,用canal.deployer就够了。
deployer是canal的服务端本体,负责连接MySQL、拉取解析binlog,开放一个端口给客户端连接。adapter是官方提供的同步适配器,可以把增量数据直接同步到ES、关系型数据库、HBase等目标端,属于"别人帮你写好了消费逻辑"的组件;如果目的是自己写代码处理增量事件,deployer完全够用,adapter反而多了一层要学习的配置。admin则是一个Web管理后台,适合生产环境集中管理多个canal实例,本地调试用不上。
2. 动手前的环境准备:JDK、binlog和授权账号
2.1 装好JDK并把JAVA_HOME配明白
canal是纯Java写的,Windows上启动它需要JDK。版本方面,canal 1.1.x系列在JDK 8上运行得很稳定,如果下载的是比较新的1.1.6或更高版本,JDK 11也没问题。我自己用的是JDK 8,跑1.1.7版本没有任何压力。
装好JDK之后,很多人会忽略环境变量。双击startup.bat一闪而过,十有八九就是JAVA_HOME没配好。Windows下canal的启动脚本会直接引用JAVA_HOME去定位java.exe,只装了JDK却不配置环境变量等于没有。建议在系统环境变量里新增JAVA_HOME,指向JDK安装目录,同时在Path里加上%JAVA_HOME%\bin。配置完成之后重新开一个cmd窗口执行java -version,能正常输出版本号再继续下一步。
2.2 给MySQL开启ROW格式的binlog
canal的数据来源是binlog,所以MySQL这边必须先开启binlog,而且格式必须设为ROW。这一步是整条链路的基础,也是很多人装了canal却收不到数据的最主要原因。
修改方式:找到MySQL的配置文件my.ini(Windows服务版一般放在安装根目录),在[mysqld]区域加三行:
server-id=1 log-bin=mysql-bin binlog_format=ROW binlog_row_image=FULL- server-id:每个MySQL实例必须唯一。canal模拟从库时也要有自己的ID,后面配置instance的时候会用到,两边的ID不能冲突。
- log-bin:开启binlog,文件名前缀叫mysql-bin,生成的日志文件在data目录下。
- binlog_format=ROW:这是最关键的一项。binlog有三种格式:STATEMENT、ROW、MIXED。STATEMENT记录的是SQL语句本身,canal面对一条UPDATE时只能看到SQL文本,拿不到哪一行变了、旧值是什么、新值是什么;MIXED在部分场景下也会退化成STATEMENT。只有ROW格式会把每一行变更前后的完整镜像都写进binlog,canal才能还原出结构化的增量数据。所以binlog_format=ROW不是偏好,是硬性要求。
改完之后重启MySQL服务,然后在客户端里执行SHOW VARIABLES LIKE 'log_bin';,看到Value为ON就说明生效了。查看当前binlog文件位置可以用SHOW BINARY LOG STATUS;(老版本命令是SHOW MASTER STATUS;)。
2.3 建一个canal能用的MySQL账号
canal伪装成从库拉取binlog,需要一个有复制权限的专用账号。权限不用给太多,最小够用即可。
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;三个权限各有用途:SELECT用来查表结构,因为canal解析binlog时需要通过表结构把字段名和值对应起来;REPLICATION SLAVE是让canal能以从库身份拉取binlog的核心权限;REPLICATION CLIENT用来获取主库服务状态,比如当前binlog的文件名和位置。这里不建议直接给ALL权限,没必要,也不符合最小权限原则。
如果MySQL是8.x版本,还需要注意认证插件的问题。MySQL 8默认使用caching_sha2_password,canal某些版本连接时会报认证失败,最省事的解决办法是建号时直接指定mysql_native_password:
CREATE USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;这个问题如果不提前处理,后面启动canal时会在日志里看到一堆认证异常,排查起来非常费劲。
3. 下载解压canal,并改好两个配置文件
3.1 解压到D盘的纯英文目录
去canal官方release页面下载canal.deployer的tar.gz包,版本选1.1.x即可,我这边用的是1.1.7。Windows下解压tar.gz通常会解出两层:先是一层tar包,再解一层才是真正的目录结构,解压工具一般能自动处理。
解压之后,强烈建议把整个目录放到纯英文、无空格的路径下,比如D:\canal\canal.deployer-1.1.7。原因很简单:canal内部的日志输出和配置文件解析对中文路径的支持并不好,出现过因为路径带中文导致启动异常或日志乱码的情况。既然坑能提前避开,就没必要往里踩。解压后目录里主要有bin(启动脚本)、conf(全部配置)、lib(依赖jar包)、logs(运行日志)。
3.2 canal.properties,默认不动就够
conf/canal.properties是服务端的全局配置。很多教程会把这个文件从头到尾讲一遍,实际本地调试根本不必动它,默认值已经能跑起来了。真正需要留意的只有两个点。
一个是canal.port=11111,这是canal对外提供服务的端口,客户端程序之后要连的就是它。如果本机11111端口被占用,就修改成其他端口。另一个是canal.destinations=example,它声明了当前服务名下有哪些实例。一个实例对应一个数据库连接配置,多个实例用逗号分隔;后面如果要加第二个数据源,就在conf目录下复制一份example文件夹改名,并把新名字加到这个配置项里。
其余配置项,比如manager相关的连接配置、metrics上报配置,本地调试都用不上,保持默认即可。很多人一上来就打开这个文件改来改去,反而把自己绕晕了。
3.3 instance.properties,真正的连接入口
实例配置在conf/example/instance.properties里,canal要连哪个MySQL、用什么账号、订阅哪些库表,全在这个文件指定。这也是整个安装过程中最需要认真核对的地方。
canal.instance.master.address = 127.0.0.1:3306 canal.instance.mysql.slaveId = 12345 canal.instance.dbUsername = canal canal.instance.dbPassword = canal canal.instance.connectionCharset = UTF-8 canal.instance.filter.regex = test_db\\..*逐项说明:
- canal.instance.master.address:MySQL的地址和端口。本机MySQL就填127.0.0.1:3306。
- canal.instance.mysql.slaveId:canal模拟从库的ID。这里有两个容易踩的坑:一是不能和MySQL的server-id重复,比如MySQL配置里server-id=1,那canal这里就绝对不能再用1;二是同一台MySQL的多个canal实例,每个slaveId也要不一样,否则会互相冲突。默认的12345一般够用。
- dbUsername和dbPassword:对应准备阶段创建的canal账号。
- connectionCharset:强烈建议设成UTF-8。Windows环境下MySQL客户端默认字符集容易出问题,不设置的话中文表名、字段名或者中文数据会出现乱码。
- filter.regex:订阅规则,默认是
.*\\..*订阅所有库表。只想要某个库时改成test_db\\..*。注意properties文件里反斜杠本身要做转义,所以文件里写的是\\..,不是\..,这个细节很容易翻车。
如果订阅规则写错了,canal能正常启动,但消费端永远收不到那个库的事件,排查起来相当隐蔽。
4. 启动canal、确认日志,再聊聊Windows下的典型坑
4.1 startup.bat与两个日志文件
配置改好之后进入bin目录,执行startup.bat。双击启动会看到一个黑色控制台窗口一直挂着,这就是canal的宿主进程,千万不要手滑关掉,关了canal就停了。
建议先用cmd手动执行,方便看到报错信息:
cd /d D:\canal\canal.deployer-1.1.7\bin startup.bat启动完成后,日志会写到canal根目录下的logs文件夹,需要重点关注两个文件:
- logs/canal/canal.log:服务端本身的启动日志,包含launcher的启动过程。
- logs/example/example.log:example这个实例的运行日志,连接MySQL、拉取binlog、心跳报文、解析异常都会打在这里。
可以说,后面所有的排错信息,几乎都集中在example.log里。
4.2 判断启动成功的三种方式
第一种是看日志。在example.log里能看到类似"start canal successfully"、"dump address xxx success"的字样,说明实例已经和MySQL建立拉取链路,这是最可靠的判断方式。
第二种是查端口。开一个新的cmd窗口执行:
netstat -ano | findstr 11111看到LISTENING状态说明canal对外服务端口已经起来了。
第三种是直接用MySQL客户端连一下canal的端口。canal对外暴露的是MySQL兼容协议,所以可以用mysql命令做一个"握手"测试:
mysql -h127.0.0.1 -P11111 -ucanal -pcanal能进入类似MySQL的命令行界面并返回协议信息,说明服务端确实在工作。这一步不是必须,但很直观。
4.3 Windows下几个高频坑和排查顺序
我把Windows环境里常遇到的问题整理成一个表,按出现频率排序:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| startup.bat一闪而过 | JAVA_HOME没配置或配置错误 | 在cmd里执行startup.bat看真实报错,配置好环境变量后重新开cmd |
| 启动后11111端口没监听 | 端口被占用,或实例启动失败 | netstat查占用,若有别的进程占用就改canal.port;否则看example.log找原因 |
| 日志乱码 | 字符集配置不一致 | instance.properties里connectionCharset设成UTF-8;也可在启动命令里加-Dfile.encoding=UTF-8 |
| 连接MySQL报认证失败 | MySQL 8用了caching_sha2_password | 建号时指定mysql_native_password |
| 能连上但收不到事件 | binlog_format不是ROW,或filter.regex写错 | 确认binlog_format=ROW并重启MySQL;检查订阅规则转义 |
| 连接MySQL超时 | Windows防火墙拦截3306或MySQL未允许远程 | 检查防火墙放通3306,确认MySQL服务正常监听 |
排查的时候别瞎改配置,按这个顺序来:先确认startup.bat有没有真正跑起来;再确认11111端口有没有监听;然后打开logs/example/example.log看最后几十行,绝大多数问题都会直接写在日志里。我见过太多人反复改配置文件,最后发现是端口被占用,白白折腾一下午。
5. 写一个Java消费端,把binlog事件真正接回来
5.1 引入canal客户端依赖
服务端跑通只是第一步,最终目的是在自己的程序里拿到binlog事件。canal官方提供了Java客户端SDK,在Maven工程里加入依赖即可,版本和服务端保持一个大版本一致:
<dependency> <groupId>com.alibaba.otter</groupId> <artifactId>canal.client</artifactId> <version>1.1.7</version> </dependency>如果用的是1.1.6或1.1.8服务端,客户端版本跟着调整即可,不要求精确一致,但大版本别差太远。
5.2 最小可运行的消费端代码
下面是一段最小可运行的消费端Demo,不依赖Spring,不需要任何框架,直接编译运行就能收到增量事件:
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 java.net.InetSocketAddress; import java.util.List; public class CanalClientDemo { public static void main(String[] args) throws Exception { // 连接canal服务端,地址和端口对应canal.properties里的canal.port CanalConnector connector = CanalConnectors.newSingleConnector( new InetSocketAddress("127.0.0.1", 11111), "example", "", ""); int batchSize = 100; try { connector.connect(); // 订阅所有库表,和instance.properties里的filter保持一致的语法 connector.subscribe(".*\\..*"); connector.rollback(); System.out.println("connected and subscribed"); while (true) { Message message = connector.getWithoutAck(batchSize); if (message.getId() == -1 || message.getEntries().isEmpty()) { Thread.sleep(1000); continue; } for (CanalEntry.Entry entry : message.getEntries()) { if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA) { continue; } CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { System.out.printf("binlog[%s:%s] %s.%s %s%n", entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), rowChange.getEventType()); StringBuilder before = new StringBuilder("before: "); for (CanalEntry.Column col : rowData.getBeforeColumnsList()) { before.append(col.getName()).append("=").append(col.getValue()).append(" "); } StringBuilder after = new StringBuilder("after: "); for (CanalEntry.Column col : rowData.getAfterColumnsList()) { after.append(col.getName()).append("=").append(col.getValue()).append(" "); } System.out.println(before); System.out.println(after); } } connector.ack(message.getId()); } } finally { connector.disconnect(); } } }代码逻辑很直接:连接canal服务端后订阅全部库表,然后在一个循环里不断拉取消息;拿到消息后按binlog行记录解析,打印出文件名、偏移量、库名、表名、事件类型以及变更前后的字段列表。处理好一批数据之后调用ack(message.getId())确认消费成功。
这里补充一句:newSingleConnector方法最后的用户名密码参数默认留空即可。如果你在canal.properties里专门配置了canal.user和canal.passwd,那这里要对应填上,否则连接会被服务端拒绝。
5.3 验证效果与两个容易混淆的点
运行Demo,看到"connected and subscribed"之后,去MySQL里对订阅的库表随便执行一条INSERT或UPDATE,控制台很快就会出现一行带binlog文件位置、表名、事件类型以及字段值的输出。第一次看到自己的程序实时打印出数据库变更时,那种链路被打通的感觉还是挺有成就感的。
这里有两个点特别容易混淆,单独说一下。
第一个,新连接默认只消费"连接建立之后产生的新binlog",不会把历史数据补给你。canal启动时从当前binlog位点开始拉取,之前已经发生的变更不会重新推送。如果想消费历史数据,需要在instance.properties里手工指定binlog文件位置,对应canal.instance.master.journal.name和canal.instance.master.position这类参数,但本地调试基本用不到。
第二个,别忘了处理DDL事件。消费端代码里我只判断了ROWDATA,但建表、加列这类DDL操作同样会生成binlog事件,EntryType是DDL。如果下游逻辑只处理ROWDATA,遇到DDL事件直接continue掉倒是没关系,但如果你在真实项目里要把增量同步到别的存储,DDL事件往往意味着结构变更,需要单独处理,否则后面数据格式就对不上了。
6. 长期运行的维护建议与收尾经验
6.1 用服务包装工具把canal做成Windows服务
本地调试时让canal一直挂着一个黑框窗口问题不大,但如果要长期运行,我更建议把它注册成Windows服务,开机自动启动,谁也不会误关窗口。
做法是用一个开源的服务包装工具(比如winsw这类小工具),把canal的启动命令包装成一个Windows服务。配置大致长这样:
<service> <id>canal</id> <name>canal</name> <description>canal deployer service</description> <executable>D:\jdk\bin\java.exe</executable> <arguments>-Xms512m -Xmx1024m -Dfile.encoding=UTF-8 -cp "../conf;../lib/*" com.alibaba.otter.canal.deployer.CanalLauncher</arguments> <workingdirectory>D:\canal\canal.deployer-1.1.7</workingdirectory> <logpath>D:\canal\logs</logpath> </service>executable改成你自己JDK的实际路径,workingdirectory改成canal的实际解压目录。注意启动参数里的-cp引用了conf和lib目录,工作目录必须切到canal根目录,否则classpath会找错。注册成服务之后,用Windows服务管理器启动,日志依然会写到logs目录,排错方式不变。
如果不想引入额外工具,也可以用任务计划程序在开机时启动startup.bat,但黑框会一直可见,更容易被手滑关掉,稳定性不如服务方案。
6.2 日志清理、堆内存与长连接维护
Windows下没有Linux那种现成的logrotate机制,logs目录会随着运行时间慢慢膨胀。canal自带的logback配置在conf目录下,可以调整日志滚动策略和保留天数,比如单文件超过一定大小就滚动、保留最近7天的日志。这个建议早点做,等日志把C盘塞满了再处理就有点被动了。
堆内存方面,startup.bat里启动JVM时会设置默认堆大小。如果订阅的库表很多、binlog事件量大,消费速度跟不上,可以适当调大-Xmx,同时确认消费端处理逻辑足够快,避免消息堆积。这里尤其要注意消费端的ack节奏,getWithoutAck拿到的消息如果迟迟不ack,canal服务端会把客户端视为异常,后续消息推送会受影响。
还有一个Windows特有的问题:笔记本休眠、网络切换都会导致canal到MySQL的长连接断开。如果example.log里持续输出连接异常或者dump中断的日志,最简单的恢复手段就是重启服务。这个现象在Windows笔记本上做开发时特别常见,不用过度分析,优先检查网络状态和MySQL是否可达。
我个人在实际操作中的体会是,Windows上跑canal真的不比Linux复杂,核心就三件事:JDK环境配好、MySQL的binlog格式设成ROW、账号权限给对。只要这三件事没问题,配置文件就算一次写错也能靠日志快速定位。真要说有什么值得记住的经验,那就是遇到问题先看logs/example/example.log,别急着改配置——日志里的报错信息,比任何教程都诚实。