Canal + RabbitMQ 数据同步实战:从填坑到打通全链路
一次完整的 MySQL 实时数据同步实践,记录从零搭建、版本踩坑到成功打通 Canal + RabbitMQ 全链路的完整过程
一、背景与目标
在微服务架构中,经常需要将 MySQL 的数据变更实时同步到其他系统(如缓存、搜索引擎、数据仓库等)。Canal 作为阿里巴巴开源的 MySQL Binlog 解析组件,是实现这一目标的常用工具。
本文目标是:
- 在 macOS 本地完成 Canal 的安装与配置
- 将 Canal 捕获的 MySQL 变更数据投递到 RabbitMQ
- 为下游消费者提供标准化的 JSON 数据
环境信息:
- 操作系统:macOS (Apple Silicon)
- MySQL:8.0.45(官方
.dmg安装) - Canal:1.1.7(TCP 模式)
- RabbitMQ:4.2.5
- Java:JDK 17
- Spring Boot:3.3.4
二、MySQL 准备:开启 Binlog
2.1 创建配置文件
Canal 基于 MySQL 的主从复制协议工作,因此必须开启 binlog。
编辑 /etc/my.cnf:
ini
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW
2.2 重启 MySQL
由于是官方 .dmg 安装,通过系统设置中的 MySQL 面板重启即可。
2.3 验证 Binlog 是否开启
sql
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'server_id';
2.4 创建 Canal 专用账号
sql
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
三、Canal 安装与版本踩坑
3.1 安装 Canal
从 GitHub Releases 下载 canal.deployer-1.1.7.tar.gz:
bash
tar -zxvf canal.deployer-1.1.7.tar.gz -C ~/canal-1.1.7
cd ~/canal-1.1.7
3.2 配置 Canal
conf/canal.properties
properties
# 工作模式:先使用 TCP 模式验证核心功能
canal.serverMode = tcp
conf/example/instance.properties
properties
# MySQL 连接信息
canal.instance.master.address = 127.0.0.1:3306
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal
# 监听所有表
canal.instance.filter.regex = .*\\..*
3.3 启动 Canal
bash
sh bin/startup.sh
tail -f logs/example/example.log
3.4 ⚠️ 版本踩坑:RabbitMQ 直连模式 Bug
在尝试使用 Canal 的 RabbitMQ 直连模式时,遇到了反复出现的错误:
PRECONDITION_FAILED - unknown exchange type '1'
这个问题在 Canal 1.1.7 和 1.1.8 版本中都存在。无论如何在 canal.properties 中配置 rabbitmq.exchange.type = direct,Canal 内部都固执地使用了无效的整数值(1 或 2),导致连接失败。
结论:Canal 1.1.x 系列的 RabbitMQ 直连模式存在无法绕过的缺陷。
四、解决方案:TCP 模式 + Java 客户端中转
4.1 架构调整
放弃 Canal 的 RabbitMQ 直连模式,改用 TCP 模式 + Spring Boot 客户端 方案:
MySQL 变更 → Canal (TCP 模式) → Spring Boot 客户端 → RabbitMQ
这种方案的优势:
- Canal 的 TCP 模式是其最成熟、最稳定的工作方式
- 客户端可以对数据进行精细化处理
- 完全绕开 Canal 的 MQ 连接 Bug
- 获得对数据流的绝对控制权
4.2 客户端项目结构
Canal-Java-Client/
├── pom.xml
└── src/main/java/com/dp/canaljavaclient/
├── CanalJavaClientApplication.java
├── config/
│ ├── CanalConfig.java
│ └── RabbitMQConfig.java
├── service/
│ └── CanalConsumerService.java
└── resources/
└── application.yml
4.3 关键依赖
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.53</version>
</dependency>
</dependencies>
4.4 核心服务实现
Canal 配置类
java
@Data
@Component
@ConfigurationProperties(prefix = "canal")
public class CanalConfig {
private String host = "127.0.0.1";
private int port = 11111;
private String destination = "example";
private String username = "";
private String password = "";
private int batchSize = 100;
private String filterRegex = ".*\\..*";
private long idleSleepMs = 1000;
}
Canal 消费者服务(核心)
java
@Slf4j
@Service
@RequiredArgsConstructor
public class CanalConsumerService {
private final CanalConfig canalConfig;
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
startConsumer();
}
private void startConsumer() {
Thread consumerThread = new Thread(() -> {
try {
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress(canalConfig.getHost(), canalConfig.getPort()),
canalConfig.getDestination(),
canalConfig.getUsername(),
canalConfig.getPassword()
);
connector.connect();
connector.subscribe(canalConfig.getFilterRegex());
connector.rollback();
while (running) {
Message message = connector.getWithoutAck(canalConfig.getBatchSize());
if (message.getId() == -1) {
Thread.sleep(canalConfig.getIdleSleepMs());
continue;
}
handleMessage(message);
connector.ack(message.getId());
}
} catch (Exception e) {
log.error("Canal 消费者异常", e);
}
});
consumerThread.setDaemon(true);
consumerThread.start();
}
private void handleMessage(Message message) {
for (CanalEntry.Entry entry : message.getEntries()) {
if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA) continue;
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
if (rowChange.getIsDdl()) continue;
String tableName = entry.getHeader().getTableName();
String schemaName = entry.getHeader().getSchemaName();
String eventType = rowChange.getEventType().toString();
// 提取列数据,避免序列化整个 RowChange 对象
List<Map<String, Object>> rowDataList = new ArrayList<>();
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
Map<String, Object> rowMap = new LinkedHashMap<>();
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
rowMap.put(column.getName(), column.getValue());
}
rowDataList.add(rowMap);
}
// 构造简洁的消息体
Map<String, Object> simpleMessage = new LinkedHashMap<>();
simpleMessage.put("schema", schemaName);
simpleMessage.put("table", tableName);
simpleMessage.put("event", eventType);
simpleMessage.put("timestamp", System.currentTimeMillis());
simpleMessage.put("data", rowDataList);
String jsonMessage = JSON.toJSONString(simpleMessage);
rabbitTemplate.convertAndSend("canal.exchange", "canal.routingkey", jsonMessage);
log.info("✅ 已转发: {}.{} -> {}, 数据行数: {}", schemaName, tableName, eventType, rowDataList.size());
}
}
}
RabbitMQ 配置
java
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "canal.exchange";
public static final String QUEUE_NAME = "canal.queue";
public static final String ROUTING_KEY = "canal.routingkey";
@Bean
public TopicExchange canalExchange() {
return new TopicExchange(EXCHANGE_NAME, true, false);
}
@Bean
public Queue canalQueue() {
return new Queue(QUEUE_NAME, true);
}
@Bean
public Binding canalBinding() {
return BindingBuilder.bind(canalQueue())
.to(canalExchange())
.with(ROUTING_KEY);
}
}
4.5 ⚠️ 踩坑:Fastjson2 序列化错误
在处理数据时遇到了 level too large: 2048 的序列化错误,原因是 CanalEntry.RowChange 对象内部存在深层嵌套结构。
解决方案 :不直接序列化整个 RowChange 对象,而是手动提取需要的 afterColumns 字段,构造简洁的 JSON 消息。
4.6 配置文件
yaml
spring:
application:
name: Canal-Java-Client
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
Canal:
host: 127.0.0.1
port: 11111
destination: example
batchSize: 100
filterRegex: ".*\\..*"
idleSleepMs: 1000
logging:
level:
com.dp.canaljavaclient: DEBUG
五、验证与测试
5.1 启动流程
bash
# 1. 启动 Canal(TCP 模式)
cd ~/canal-1.1.7
sh bin/startup.sh
# 2. 启动 Spring Boot 客户端
java -jar target/canal-java-client-0.0.1-SNAPSHOT.jar
# 3. 在 MySQL 中执行变更
INSERT INTO feng.user (name, age, email) VALUES ('test', 25, 'test@example.com');
5.2 查看结果
客户端日志:
✅ 已转发: feng.user -> INSERT, 数据行数: 1
RabbitMQ 管理界面(http://localhost:15672):
队列: canal.queue
Ready: 1
消息内容:
json
{
"schema": "feng",
"table": "user",
"event": "INSERT",
"timestamp": 1782295977691,
"data": [
{
"id": "4",
"name": "test",
"age": "25",
"email": "test@example.com"
}
]
}
六、成功经验总结
6.1 核心方法论:区分"捷径"与"正道"
| 方案 | 结果 | 教训 |
|---|---|---|
| Canal RabbitMQ 直连模式 | ❌ 失败 | 存在无法绕过的 Bug,果断放弃 |
| Canal TCP 模式 + 客户端 | ✅ 成功 | 官方最稳定的工作方式 |
启示:当组件的外围功能存在长期 Bug 时,果断使用其核心功能 + 自己编写适配层,往往能获得更强的稳定性和控制权。
6.2 关键决策
- 先验证核心链路:第一时间用 TCP 模式验证 Canal 的 binlog 解析功能是否正常
- 自主构建客户端:编写轻量级 Spring Boot 客户端作为 Canal 和 RabbitMQ 之间的桥梁
- 精细化数据处理:手动提取字段,避免序列化深层嵌套对象
- 使用固定的 Routing Key:简化绑定关系,确保消息正确路由
6.3 最终架构
┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ ┌─────────────┐
│ MySQL │────▶│ Canal │────▶│ Spring Boot 3 │────▶│ RabbitMQ │
│ Binlog │ │ TCP 模式 │ │ + JDK 17 客户端 │ │ Queue │
│ feng.user │ │ 11111端口 │ │ 拉取 → 转换 → 转发 │ │ Ready: 1 │
└─────────────┘ └─────────────┘ └─────────────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ 下游消费者 │
│ (待扩展) │
└─────────────┘
七、扩展建议
7.1 多表路由
java
// 按表名路由到不同队列
if ("user".equals(tableName)) {
rabbitTemplate.convertAndSend("canal.exchange", "user.queue", jsonMessage);
} else if ("order".equals(tableName)) {
rabbitTemplate.convertAndSend("canal.exchange", "order.queue", jsonMessage);
}
7.2 下游消费者
java
@Component
@Slf4j
public class CanalMessageConsumer {
@RabbitListener(queues = "canal.queue")
public void handle(String message) {
log.info("收到消息: {}", message);
// 更新缓存、同步 ES、触发通知...
}
}
测试:
http://localhost:15672/#/queues/%2F/canal.queue

八、结语
本文完整记录了从零搭建 Canal + RabbitMQ 数据同步管道的过程,重点分享了:
- 版本踩坑经验:Canal 1.1.x 的 RabbitMQ 直连模式存在 bug
- 稳健的替代方案:TCP 模式 + Spring Boot 客户端
- 实际问题解决:序列化异常、消息路由、配置优化
这套方案已在本地环境完全验证通过,可作为微服务架构中数据同步的基础设施。
项目源码地址:https://github.com/lvan-jone/canal-java-client.git
欢迎交流讨论! 😊