关于Canal
基于 MySQL 数据库增量日志解析(mysql-binlog),提供增量数据订阅和消费
基于日志增量订阅和消费的业务包括
- 数据库镜像
- 数据库实时备份
- 索引构建和实时维护(拆分异构索引、倒排索引等)
- 业务 cache 刷新
- 带业务逻辑的增量数据处理(常用)
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
MySQL主备复制原理
- master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
- slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
- slave 重放 relay log 中事件,将数据变更反映它自己的数据
canal 工作原理
- canal 模拟 slave 的交互协议,伪装自己为slave ,向 master 发送dump 协议
- master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
- canal 解析 binary log 对象(原始为 byte 流)
环境搭建
PS:各中间件使用Docker搭建
Mysql
数据库版本8.0.27
,默认情况下MySQL8的二进制日志是打开的,但是我还是做了配置挂载手动开启。
这个就略过了,直接贴出数据库挂载的配置文件
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1
重启容器后查看是否异常,并确认是否开启了binlog。
Canal
- 拉取Canal镜像,版本latest
docker pull canal/canal-server
- 启动Canal,拷出Canal配置文件
docker run --name canal -d canal/canal-server # 拷贝配置文件 docker cp canal:/home/admin/canal-server/conf/canal.properties /docker/canal/conf docker cp canal:/home/admin/canal-server/conf/example/instance.properties /docker/canal/conf
- 删除容器并修改拷出的配置文件
- 修改instance.properties配置文件
- 设置相关连接、账户、密码
- 挂载配置文件并启动Canal
docker run --name canal -p 11111:11111 -d \ -v /docker/canal/conf/instance.properties:/home/admin/canal-server/conf/example/instance.properties \ -v /docker/canal/conf/canal.properties:/home/admin/canal-server/conf/canal.properties \ -v /docker/canal/log/:/home/admin/canal-server/logs/ \ canal/canal-server:latest
- 查看Canal容器是否出现异常
启动无异常[root@localhost ~]# docker logs -f canal DOCKER_DEPLOY_TYPE=VM ==> INIT /alidata/init/02init-sshd.sh ==> EXIT CODE: 0 ==> INIT /alidata/init/fix-hosts.py ==> EXIT CODE: 0 ==> INIT DEFAULT Generating SSH1 RSA host key: [ OK ] Starting sshd: [ OK ] Starting crond: [ OK ] ==> INIT DONE ==> RUN /home/admin/app.sh ==> START ... start canal ... start canal successful ==> START SUCCESSFUL ...
应用
canal 特别设计了 client-server 模式,交互协议使用 protobuf 3.0 , client 端可采用不同语言实现不同的消费逻辑。
canal 作为 MySQL binlog 增量获取和解析工具,可将变更记录投递到 MQ 系统中,比如 Kafka/RocketMQ,可以借助于 MQ 的多语言能力
参考文档: Canal Kafka/RocketMQ QuickStart
Java客户端使用
将上述MySql , Canal Server 部署完毕
创建Maven项目并添加依赖
<!-- cenal(阿里巴巴实现binlog订阅的框架,用于redis和DB数据同步) -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.0</version>
</dependency>
贴上测试类
package com.test;
import java.net.InetSocketAddress;
import java.util.List;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.common.utils.AddressUtils;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
import com.alibaba.otter.canal.protocol.Message;
public class ClientSample {
public static void main(String args[]) {
// 创建链接 ip 端口
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.1.110", 11111), "example", "", "");
int batchSize = 1000;
try {
connector.connect();
// 所有库 所有表
connector.subscribe(".*\\..*");
connector.rollback();
boolean flag =true;
while (flag) {
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) {
e.printStackTrace();
}
} else {
printEntry(message.getEntries());
}
connector.ack(batchId); // 提交确认
}
} finally {
connector.disconnect();
}
}
private static void printEntry( List<Entry> entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
e);
}
EventType eventType = rowChage.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 : rowChage.getRowDatasList()) {
if (eventType == EventType.DELETE) {
printColumn(rowData.getBeforeColumnsList());
} else if (eventType == EventType.INSERT) {
printColumn(rowData.getAfterColumnsList());
} else {
System.out.println("-------> before");
printColumn(rowData.getBeforeColumnsList());
System.out.println("-------> after");
printColumn(rowData.getAfterColumnsList());
}
}
}
}
// 这里可以做一些业务处理逻辑
private static void printColumn( List<Column> columns) {
for (Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
}
触发数据库变更
观察控制台打印记录
可以看到,已经监测到了数据的变化,实时监测到数据后,可以做一些自己业务系统上的一些处理。
整合SpringBoot使用
SpringBoot项目整合 Canal依赖
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>xxx-RELEASE</version>
</dependency>
附带配置说明
属性 | 描述 | 默认值 |
---|---|---|
canal.mode | canal 客户端类型 目前支持4种类型 simple,cluster,zk,kafka(kafka 目前支持flatMessage 格式) | simple |
canal.filter | canal过滤的表名称,如配置则只订阅配置的表 | “” |
canal.batch-size | 消息的数量,超过该次数将进行一次消费 | 1 |
canal.timeout | 消费的时间间隔(s) | 1s |
canal.server | 服务地址,多个地址以,分隔 格式 h o s t : {host}: host:{port} | null |
canal.destination | canal 的instance 名称,kafka模式为topic 名称 | null |
canal.user-name | canal 的用户名 | null |
canal.password | canal 的密码 | null |
canal.group-id kafka groupId | 消费者订阅消息时可使用,kafka canal 客户端 | null |
canal.async | 是否是异步消费,异步消费时,消费时异常将导致消息不会回滚,也不保证顺序性 | true |
canal.partition | kafka partition | null |
添加依赖配置Canal配置文件
canal:
server: 192.168.1.110:11111 # ip 端口
destination: example # 目的地
监听的实体对象(POJO)省略了getter、setter。
@Table(name = "chip_category_language")
public class ChipCategoryLanguage {
/**
*
*/
private Integer cid;
/**
*
*/
private Integer lid;
/**
* 分类名称
*/
private String categoryName;
/**
* 上级id
*/
private Integer parentId;
/**
* 创建时间
*/
private Date createTime;
/**
* 修改时间
*/
private Date updateTime;
/**
* 删除 0 未删除 1 删除
*/
private Integer del;
实现 EntryHandler<T>
接口,泛型为想要订阅的数据库表的实体对象,如果只要监听增加操作,只实现增加方法即可。
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;
import top.javatool.canal.example.model.ChipCategoryLanguage;
@Component
@CanalTable(value = "chip_category_language")
public class ChipCategoryLanguageHandler implements EntryHandler<ChipCategoryLanguage> {
@Override
public void insert(ChipCategoryLanguage chipCategoryLanguage) {
System.out.println("insert message " + chipCategoryLanguage.toString());
}
@Override
public void update(ChipCategoryLanguage before, ChipCategoryLanguage after) {
System.out.println("update before " + before.toString());
System.out.println("update after " + after.toString());
}
@Override
public void delete(ChipCategoryLanguage chipCategoryLanguage) {
System.out.println("delete message " + chipCategoryLanguage.toString());
}
}
启动程序进行监听订阅
修改数据库数据,程序打印得到体现
对于before之前的null值属性,我查阅了一下文档:对于更新操作来讲,before 中的属性只包含变更的属性,after 包含所有属性,所以null值是正常的。
本文只做了简单应用,实际上从原理图可以看到,Canal可以直接将获取到的数据推送至Es、MQ等,还是非常便捷的,此本不做表述,后期有空再做补充