canal简介
canal 翻译为管道,主要用途是基于 MySQL 数据库的增量日志 Binlog 解析,提供增量数据订阅和消费。
早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。
基于日志增量订阅和消费的业务包括
数据库镜像;
数据库实时备份;
索引构建和实时维护(拆分异构索引、倒排索引等);
业务 cache 刷新;
带业务逻辑的增量数据处理;
工作原理
MySQL主备复制原理:
- MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件 binary log events,可以通过 show binlog events 进行查看);
- MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log);
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据;
canal 工作原理:
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送 dump 协议;
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );
- canal 解析 binary log 对象(原始为 byte 流);
一、MySQL设置
修改 my.cnf
中配置
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant
CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;
查看授权情况
select * from mysql.user where User = 'canal'\G
*************************** 1. row ***************************
Host: %
User: canal
Select_priv: Y
Repl_slave_priv: Y
Repl_client_priv: Y
二、启动Canal服务端
下载解压
wget https://github.com/alibaba/canal/releases/download/canal-1.1.6/canal.deployer-1.1.6.tar.gz
配置修改
conf/example/instance.properties
## mysql serverId
canal.instance.mysql.slaveId = 100
#position info,需要改成自己的数据库信息
canal.instance.master.address = 127.0.0.1:3306
#username/password,需要改成自己的数据库信息
canal.instance.dbUsername = canal
canal.instance.dbPassword = 123456
如果系统是1个 cpu,需要将 canal.instance.parser.parallel
设置为 false
然后通过命令启动
bash bin/startup.sh
三、通过Canal客户端消费数据
可以直接消费数据;也可以先放到MQ中,再消费数据
直接通过Canal客户端消费数据
依赖
<!-- canal -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.0</version>
</dependency>
客户端代码
package com.example.demo;
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 SimpleCanalClientExample {
public static void run() {
// 连接信息配置
String hostname = "127.0.0.1";
int port = 11111;
String destination = "example";
String username = "";
String password = "";
// 创建链接
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress(hostname, port), destination, username, password
);
int batchSize = 1000;
try {
connector.connect();
connector.subscribe(".*\\..*");
connector.rollback();
while (true) {
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
long batchId = message.getId();
int size = message.getEntries().size();
// 没有拿到数据
if (batchId == -1 || size == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
} else {
System.out.printf("message[batchId=%s, size=%s] \n", batchId, size);
printEntry(message.getEntries());
}
connector.ack(batchId); // 提交确认
// connector.rollback(batchId); // 处理失败, 回滚数据
}
} finally {
connector.disconnect();
}
}
private static void printEntry(List<Entry> entries) {
for (Entry entry : entries) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChange = null;
try {
rowChange = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
e);
}
EventType eventType = rowChange.getEventType();
System.out.println(String.format("binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(),
entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(),
entry.getHeader().getTableName(),
eventType));
// 数据变化
for (RowData rowData : rowChange.getRowDatasList()) {
if (eventType == EventType.DELETE) {
printColumn(rowData.getBeforeColumnsList());
} else if (eventType == EventType.INSERT) {
printColumn(rowData.getAfterColumnsList());
} else {
printColumn(rowData.getAfterColumnsList());
}
}
}
}
private static void printColumn(List<Column> columns) {
for (Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue());
}
}
public static void main(String[] args) {
run();
}
}
四、通过RabbitMQ消费数据
1、修改canal配置
canal.properties
# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ
# canal.serverMode = tcp
canal.serverMode = rabbitMQ
##################################################
######### RabbitMQ #############
##################################################
rabbitmq.host = 127.0.0.1
rabbitmq.virtual.host = /
rabbitmq.exchange = canal-exchange
rabbitmq.username = root
rabbitmq.password = 123456
instance.properties
# mq config
# canal.mq.topic=example
canal.mq.topic=canal-routing-key
2、消费mq
Canal消息生产者:
package com.example.demo.config;
import com.example.demo.constant.RabbitConstant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Canal消息提供者,canal-server生产的消息通过RabbitMQ消息队列发送
*/
@Configuration
public class CanalProvider {
/**
* 队列
*/
@Bean
public Queue canalQueue() {
/**
* durable:是否持久化,默认false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在;暂存队列:当前连接有效
* exclusive:默认为false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
* autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除
*/
return new Queue(RabbitConstant.CanalQueue, true);
}
/**
* 交换机,这里使用直连交换机
*/
@Bean
DirectExchange canalExchange() {
return new DirectExchange(RabbitConstant.CanalExchange, true, false);
}
/**
* 绑定交换机和队列,并设置匹配键
*/
@Bean
Binding bindingCanal() {
return BindingBuilder.bind(canalQueue()).to(canalExchange()).with(RabbitConstant.CanalRouting);
}
}
Canal消息消费者:
package com.example.demo.consumer;
import com.example.demo.constant.EventTypeConstant;
import com.example.demo.constant.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* Canal消息消费者
*/
@Component
@RabbitListener(queues = RabbitConstant.CanalQueue)
public class CanalConsumer {
@RabbitHandler
public void process(Map<String, Object> msg) {
System.out.println("收到canal消息:" + msg);
// 收到canal消息:{data=[{id=21, name=Mini+1, age=30}], database=data, es=1684302821000, id=2, isDdl=false, mysqlType={id=int unsigned, name=varchar(20), age=tinyint unsigned}, old=[{name=Mini}], pkNames=[id], sql=, sqlType={id=4, name=12, age=-6}, table=tb_user, ts=1684302821452, type=UPDATE}
boolean isDdl = (boolean) msg.get("isDdl");
// 不处理DDL事件
if (isDdl) {
return;
}
// 数据库
String database = (String) msg.get("database");
// 表
String table = (String) msg.get("table");
// 类型:INSERT/UPDATE/DELETE
String type = (String) msg.get("type");
// 每一列的数据值
List<?> data = (List<?>) msg.get("data");
System.out.printf("%s.%s", database, table);
// 只处理指定类型
if (EventTypeConstant.INSERT.equalsIgnoreCase(type)) {
System.out.println("INSERT");
} else if (EventTypeConstant.UPDATE.equalsIgnoreCase(type)) {
System.out.println("UPDATE");
} else if (EventTypeConstant.DELETE.equalsIgnoreCase(type)) {
System.out.println("DELETE");
} else {
// 其他事件
}
}
}
数据变化事件类型
package com.example.demo.constant;
/**
* 数据变化事件类型
*/
public class EventTypeConstant {
public static final String INSERT = "INSERT";
public static final String UPDATE = "UPDATE";
public static final String DELETE = "DELETE";
}
配置
package com.example.demo.constant;
/**
* 配置
*/
public class RabbitConstant {
public static final String CanalQueue = "canal-queue";
public static final String CanalExchange = "canal-exchange";
public static final String CanalRouting = "canal-routing-key";
}