Alibaba Canal:MySQL 的“复读机”,实时监听数据“悄悄话”👂🗣️
兄弟们,想象一下这个场景:你的老板想知道“每分钟有多少订单成交”,产品经理想知道“哪些商品被加购但没付款”,风控想知道“这个用户怎么刚注册就下10单”。你怎么办?在代码里到处埋点?写个定时任务扫描数据库?😫
No!No!No!这就像为了知道家里进小偷,每隔5分钟爬起来检查一次——累死不说,还可能错过关键瞬间!今天的主角 Alibaba Canal 就是个“智能监控摄像头”,7x24小时盯着数据库的“一举一动”,实时告诉你:“嘿,有人下单了!” “注意,库存被改了!” 🎥
一、Canal 是啥?—— MySQL 的“随身翻译官”👨💼
先看官方定义:Canal 是阿里巴巴开源的一款基于 MySQL 数据库增量日志解析的数据同步工具。
说人话:Canal 就是个“复读机”,它实时监听 MySQL 说的每一句“悄悄话”(数据变更),然后翻译成我们能懂的消息,广播给其他系统。
工作模式对比:
传统做法(定时轮询):
// 程序员:我太难了!
while(true) {
// 每隔5秒查一次数据库
String sql = "SELECT * FROM orders WHERE update_time > '上次时间'";
List<Order> newOrders = query(sql);
if(!newOrders.isEmpty()) {
notifyOtherSystems(newOrders); // 通知其他系统
}
Thread.sleep(5000); // 睡5秒
// 问题:延迟5秒,还可能漏数据!
}
Canal 做法:
MySQL: "我刚刚插入了一条订单,id=1001"
Canal: 👂 "听到啦!" → 📢 "大家注意,有新订单啦,id=1001!"
下游系统:🙉 "收到!"(更新缓存/发消息/同步搜索)
核心原理:Canal 把自己伪装成 MySQL 的从库(Slave),然后“偷听”主库的二进制日志(binlog),再解析成我们能理解的事件。
二、为什么需要 Canal?—— 当“轮询”变成“自残”🤕
场景1:电商订单状态同步
用户下单 → 订单服务写 MySQL → 物流系统、库存系统、客服系统都要知道
传统方案:
- 写订单时发 MQ 消息(代码侵入性强)
- 定时扫描订单表(延迟高,压力大)
- 每个系统自己查(重复造轮子)
Canal 方案:
-- MySQL 自动记录(binlog)
-- Canal 自动解析
-- 下游系统自动接收
-- 程序员:我只需喝咖啡☕
场景2:缓存与数据库一致性
// 经典难题:先更新数据库,还是先删缓存?
// 用 Canal 后:
MySQL更新数据 → Canal监听到 → 自动删缓存
// 代码干净得像刚打扫过的房间!🧹
场景3:实时数仓
数据分析师:“我要实时看到用户行为!”
你(用 Canal 前):“等明天跑完 ETL 任务...”
你(用 Canal 后):“现在就能看,实时同步!”
三、Canal 工作原理:MySQL 主从复制的“内鬼”🕵️
要理解 Canal,得先懂 MySQL 主从复制:
MySQL 主从复制流程:
主库 (Master)
↓ 写操作被记录到 binary log
↓
从库 (Slave) 连接主库,拉取 binlog
↓
从库重放 binlog,实现数据同步
Canal 的“骚操作”:
MySQL 主库
↓ 写 binlog
Canal(伪装成从库!)
↓ 拉取 binlog
↓ 解析 binlog(不执行,只解析!)
↓ 转换成结构化的消息
下游系统(Redis/ES/Kafka...)
关键点:Canal 只“偷听”不“执行”,所以对主库性能影响极小,就像你偷听别人说话,但自己不开口。
四、Canal 架构:三个“葫芦娃”各显神通🎭
1. Canal Server - “耳朵”
负责连接 MySQL,拉取和解析 binlog
# canal.properties 配置
canal.instance.master.address = 127.0.0.1:3306
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal
canal.instance.filter.regex = .*\..* # 监听所有表
2. Canal Client - “嘴巴”
从 Server 获取解析后的数据,发送给下游
// 简单示例
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("127.0.0.1", 11111),
"example", "", ""
);
connector.connect();
connector.subscribe(".*\..*"); // 订阅所有表
3. Canal Admin - “大脑”(可选)
Web 管理界面,管理多个 Canal 实例
五、Canal 能“偷听”到什么?👂
1. 数据变更事件
CanalEntry.Entry entry = ...;
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
// 1. INSERT 事件
if (rowChange.getEventType() == CanalEntry.EventType.INSERT) {
List<CanalEntry.Column> afterColumns = rowData.getAfterColumnsList();
// afterColumns 包含插入后的数据
}
// 2. UPDATE 事件
else if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {
List<CanalEntry.Column> beforeColumns = rowData.getBeforeColumnsList(); // 修改前
List<CanalEntry.Column> afterColumns = rowData.getAfterColumnsList(); // 修改后
}
// 3. DELETE 事件
else if (rowChange.getEventType() == CanalEntry.EventType.DELETE) {
List<CanalEntry.Column> beforeColumns = rowData.getBeforeColumnsList(); // 删除前的数据
}
}
}
2. DDL 语句(表结构变更)
-- MySQL执行
ALTER TABLE users ADD COLUMN age INT;
-- Canal能监听到这个DDL事件
3. 实际数据示例
{
"type": "INSERT",
"database": "test",
"table": "user",
"data": [
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
}
],
"es": 1631234567000, // 事件时间
"ts": 1631234567890 // 处理时间
}
六、Canal 实战:手把手搭建“监听系统”🔧
步骤1:MySQL 准备
-- 1. 开启 binlog(必须!)
# my.cnf
[mysqld]
log-bin=mysql-bin # 开启二进制日志
binlog-format=ROW # 必须用 ROW 模式
server_id=1
-- 2. 创建 Canal 用户
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
-- 3. 查看 binlog 状态
SHOW MASTER STATUS;
-- 输出:
-- File: mysql-bin.000001
-- Position: 154
步骤2:部署 Canal Server
# 1. 下载 Canal
wget https://github.com/alibaba/canal/releases/download/canal-1.1.7/canal.deployer-1.1.7.tar.gz
tar -zxvf canal.deployer-1.1.7.tar.gz
cd canal.deployer-1.1.7
# 2. 修改配置
vi conf/example/instance.properties
# 关键配置:
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.filter.regex=.*\..* # 监听所有库所有表
# 或只监听特定表:test.user,test.order
# 3. 启动
sh bin/startup.sh
# 查看日志
tail -f logs/example/example.log
步骤3:编写 Java 客户端
<!-- pom.xml 依赖 -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.7</version>
</dependency>
public class SimpleCanalClient {
public static void main(String[] args) {
// 1. 连接 Canal Server
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("127.0.0.1", 11111),
"example", // 对应 instance 名称
"",
""
);
connector.connect();
connector.subscribe(".*\..*"); // 订阅所有
connector.rollback(); // 回滚到上次位置
// 2. 循环监听
while (true) {
Message message = connector.getWithoutAck(100); // 批量获取
long batchId = message.getId();
if (batchId != -1 && !message.getEntries().isEmpty()) {
processEntries(message.getEntries());
connector.ack(batchId); // 确认消费
} else {
Thread.sleep(1000); // 没数据就睡会儿
}
}
}
private static void processEntries(List<CanalEntry.Entry> entries) {
for (CanalEntry.Entry entry : entries) {
if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA) {
continue;
}
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
CanalEntry.EventType eventType = rowChange.getEventType();
System.out.println("======> 监听到事件: " + eventType);
System.out.println("数据库: " + entry.getHeader().getSchemaName());
System.out.println("表: " + entry.getHeader().getTableName());
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
if (eventType == CanalEntry.EventType.DELETE) {
System.out.println("删除前数据: " + rowData.getBeforeColumnsList());
} else if (eventType == CanalEntry.EventType.INSERT) {
System.out.println("新增数据: " + rowData.getAfterColumnsList());
} else if (eventType == CanalEntry.EventType.UPDATE) {
System.out.println("修改前: " + rowData.getBeforeColumnsList());
System.out.println("修改后: " + rowData.getAfterColumnsList());
}
}
}
}
}
步骤4:测试效果
-- 在 MySQL 中执行
INSERT INTO user(name, email) VALUES('李四', 'lisi@test.com');
UPDATE user SET email='new_email@test.com' WHERE name='李四';
DELETE FROM user WHERE name='李四';
-- Canal 客户端输出:
======> 监听到事件: INSERT
数据库: test
表: user
新增数据: [id:1, name:李四, email:lisi@test.com]
======> 监听到事件: UPDATE
数据库: test
表: user
修改前: [id:1, name:李四, email:lisi@test.com]
修改后: [id:1, name:李四, email:new_email@test.com]
======> 监听到事件: DELETE
数据库: test
表: user
删除前数据: [id:1, name:李四, email:new_email@test.com]
七、高级玩法:Canal 的“七十二变”🎩
玩法1:同步到 Redis(缓存更新)
if (eventType == CanalEntry.EventType.UPDATE ||
eventType == CanalEntry.EventType.DELETE) {
String table = entry.getHeader().getTableName();
String key = table + ":" + getId(rowData);
redis.del(key); // 缓存失效
}
玩法2:同步到 Elasticsearch(搜索索引)
if (eventType == CanalEntry.EventType.INSERT ||
eventType == CanalEntry.EventType.UPDATE) {
// 构建文档
Map<String, Object> doc = buildDocument(rowData);
// 更新ES
esClient.index("users", doc);
} else if (eventType == CanalEntry.EventType.DELETE) {
esClient.delete("users", getId(rowData));
}
玩法3:同步到 Kafka(消息队列)
// Canal 已提供 Adapter,配置即可
# canal.adapter-1.1.7/conf/application.yml
canal.conf:
canalServerHost: 127.0.0.1:11111
srcDataSources:
defaultDS:
url: jdbc:mysql://127.0.0.1:3306/test
canalAdapters:
- instance: example
groups:
- groupId: g1
outerAdapters:
- name: kafka
hosts: 127.0.0.1:9092
properties:
topic: test
玩法4:数据过滤和转换
# instance.properties
# 只监听特定的库和表
canal.instance.filter.regex=test.user,test.order
# 或排除某些表
canal.instance.filter.black.regex=mysql.*,information_schema.*
玩法5:高可用部署
Canal Server 1 (主) ←→ ZooKeeper → Canal Server 2 (备)
↓ ↓
MySQL 主库 MySQL 主库
故障时自动切换,消息不丢失!
八、Canal 的“坑”与“避坑指南”🕳️
坑1:MySQL 配置不对
现象:Canal 连不上,报权限错误
解决:
-- 检查用户权限
SHOW GRANTS FOR 'canal'@'%';
-- 必须包含:SELECT, REPLICATION SLAVE, REPLICATION CLIENT
坑2:binlog 格式不对
现象:能连接但解析不出数据
解决:
-- 检查 binlog 格式
SHOW VARIABLES LIKE 'binlog_format';
-- 必须是 ROW 模式!
-- 修改 my.cnf: binlog-format=ROW
坑3:Position 不对
现象:启动后收不到新数据,或收到重复数据
解决:
# 指定从特定位置开始
canal.instance.master.journal.name=mysql-bin.000001
canal.instance.master.position=154
# 或从当前最新位置
canal.instance.master.timestamp=当前时间戳
坑4:内存溢出
现象:Canal Server 频繁重启
解决:
# 调整内存
canal.instance.memory.batch.size = 16384 # 默认16K
canal.instance.memory.buffer.size = 1024 # 默认1K
# 监控 Canal 内存使用
坑5:网络闪断
现象:偶尔收不到数据
解决:
// Client 端增加重试
while (true) {
try {
Message message = connector.getWithoutAck(100);
// 处理...
} catch (Exception e) {
connector.rollback(); // 回滚
reconnect(); // 重连
}
}
九、Canal vs 其他方案:华山论剑🗡️
| 方案 | 原理 | 实时性 | 侵入性 | 优点 | 缺点 |
|---|---|---|---|---|---|
| Canal | 解析 binlog | 毫秒级 | 无侵入 | 功能强大,阿里背书 | 部署稍复杂 |
| Debezium | 解析 WAL | 毫秒级 | 无侵入 | 支持多数据库 | 社区相对小 |
| 触发器 | 数据库触发器 | 实时 | 有侵入 | 简单 | 影响性能 |
| 轮询 | 定时查询 | 分钟级 | 无侵入 | 实现简单 | 延迟高,压力大 |
| 消息队列 | 业务发消息 | 实时 | 有侵入 | 灵活可控 | 代码侵入强 |
选择建议:
- 新项目,要实时同步 → Canal 或 Debezium
- 简单需求,可接受延迟 → 轮询
- 老系统改造困难 → Canal(无侵入是王道)
十、Canal 最佳实践:从“能用”到“好用”🚀
实践1:分表处理
// 处理分表逻辑
String tableName = entry.getHeader().getTableName();
if (tableName.startsWith("order_2024")) {
// 按年份分表的处理逻辑
String year = tableName.substring(6, 10);
processOrder(year, rowData);
}
实践2:监控告警
# 监控 Canal 状态
# 1. 检查 Canal 进程
ps aux | grep canal
# 2. 检查日志
tail -f logs/example/example.log | grep ERROR
# 3. 监控延迟
canal.prometheus.metrics.puller.delay
# 4. 告警规则
# - Canal 进程挂掉
# - 同步延迟 > 10秒
# - 解析错误数 > 10/min
实践3:数据一致性保证
// 1. 幂等处理
String messageId = entry.getHeader().getExecuteTime() + "-" + entry.getHeader().getLogfileOffset();
if (redis.setnx("canal:msg:" + messageId, "1") == 1) {
processMessage(entry); // 处理
redis.expire("canal:msg:" + messageId, 3600); // 1小时过期
}
// 2. 顺序性保证
// 单线程消费,或按表hash到同一线程
实践4:性能优化
# canal.properties
# 调整批量大小
canal.instance.memory.batch.size = 32768
# 调整拉取间隔
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
# 启用压缩
canal.instance.filter.query.dcl = false
canal.instance.filter.query.dml = false
十一、未来展望:Canal 2.0 🚀
1. 多数据库支持
MySQL → Canal → 多种目的地
PostgreSQL ↗
Oracle ↗
2. 云原生部署
# Kubernetes 部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: canal-server
spec:
replicas: 2
template:
spec:
containers:
- name: canal
image: canal-server:latest
3. 流处理集成
MySQL → Canal → Flink → 实时计算
↓
实时大屏
十二、总结:Canal 的哲学🧘
Canal 的核心思想是: “不要重复发明轮子,尤其当轮子自己会转时”
MySQL 已经有了完善的复制机制(binlog),我们不需要:
- 在业务代码里埋点
- 写复杂的定时任务
- 担心数据丢失
我们只需要: “优雅地偷听” ,然后做我们该做的事。
最后记住:
- Canal 是监听者,不是执行者
- 对线上影响极小,可放心使用
- 无侵入是最大的优点
- 适合数据同步、缓存更新、实时分析等场景
下次当你需要“实时知道数据库发生了什么”时,不妨试试 Canal。它会让你感受到:原来数据同步可以如此优雅!就像有个贴心的小助理,7x24小时帮你盯着数据库,然后悄悄告诉你:“老板,数据有动静了!” 📢
但记住:能力越大,责任越大。用 Canal 就像装监控摄像头——要合理使用,保护数据隐私,遵守公司安全规定。技术是工具,用好才是王道!🔧✨