简介
canal是阿里巴巴的一个开源项目,基于java实现,已经在很多大型的互联网项目生产环境中使用,包括阿里、美团等都有广泛的应用,
是一个非常成熟的数据库同步方案,基础的使用只需要进行简单的配置即可。canal是通过模拟成为mysql的slave的方式,监听mysql的binlog日志来获取数据的。
binlog设置为row模式以后,不仅能获取到执行的每一个增删改的脚本,同时还能获取到修改前和修改后的数据,基于这个特性,canal就能高性能的获取到mysql数据数据的变更。
canal分为server端和client端。
用于同步mysql数据库的增量数据到其他的存储应用。
只能监听mysql的增删改。
工作原理
canal的工作原理就是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送dump协议,MySQL mater收到canal发送过来的dump请求,
开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如Hbase,Kafka,Elastic Search等。
使用
canal的部署主要分为server端和client端。
server端部署好以后,可以直接监听mysql binlog,因为server端是把自己模拟成了mysql slave,所以,只能接受数据,没有进行任何逻辑的处理,具体的逻辑处理,需要client端去进行处理。
而client端一般是需要大家自己进行简单的开发的。
canal Adapter
为了便于大家的使用,官方做了一个独立的组件Adapter,Adapter可以将canal server端获取的数据适配于一些常用的中间件。现在支持kafka、rocketmq、hbase、elasticsearch,针对这几个中间件的支持,直接配置即可,无需开发。
上文中,如果需要将mysql的数据同步到elasticsearch,直接运行canal Adapter,修改相关的配置即可。
有了canal和canal Adapter神器,同步数据到elasticsearch(非关系型数据库)、hbase(非结构化数据存储的分布式数据库,即基于列的模式)就轻而易举了。
常见问题
1.无法接收到数据,程序也没有报错?
一定要确保mysql的binlog模式为row模式,canal的原理就是解析Binlog文件,并且直接依靠该文件获取数据的。
2.Adapter使用无法同步数据?
按照官方文档,检查配置项,如sql的大小写,字段的大小写可能都会有影响,如果还无法搞定,可以自己获取代码调试下,Adapter的代码还是比较容易看懂的。
canal能做什么
canal的数据同步不是全量的,而是增量的。基于binary log增量订阅和消费,canal可以做:
数据库镜像
数据库实时备份
索引构建和实时维护
业务cache(缓存)刷新
带业务逻辑的增量数据处理
如何搭建canal
Mysql之前已经安装过,启动即可!
service mysql start
然后在MySQL中需要创建一个用户,并授权:
-- 使用命令登录:mysql -u root -p
-- 创建用户 用户名:canal 密码:Canal@123456
create user 'canal'@'%' identified by 'Canal@123456';
-- 授权 *.*表示所有库
grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%' identified by 'Canal@123456';
说明:一般复制账号需要这两个权限。授予复制账号REPLICATION SLAVE权限,复制才能真正地工作。授予复制账号REPLICATION CLIENT权限,
复制用户可以使用SHOW MASTER STATUS, SHOW SLAVE STATUS和 SHOW BINARY LOGS命令来确定复制状态。
下一步在MySQL配置文件my.cnf设置如下信息:
service mysql restart
[mysqld]
# 打开binlog
log-bin=mysql-bin
# 选择ROW(行)模式
binlog-format=ROW
# 配置MySQL replaction需要定义,不要和canal的slaveId重复
server_id=1
改了配置文件之后,重启MySQL,使用命令查看是否打开binlog模式:
show VARIABLES like 'log_bin';
查看binlog日志文件列表:
查看当前正在写入的binlog文件:
MySQL这边就搞定了,很简单。
安装canal
官网下载:https://github.com/alibaba/canal/releases
我这里下载的是1.1.4的版本:
解压:tar -zxvf canal.deployer-1.1.4.tar.gz
接着打开配置文件conf/example/instance.properties,配置信息如下:
## mysql serverId , v1.0.26+ will autoGen
## v1.0.26版本后会自动生成slaveId,所以可以不用配置
# canal.instance.mysql.slaveId=0
# 数据库地址
canal.instance.master.address=127.0.0.1:3306
# binlog日志名称
canal.instance.master.journal.name=mysql-bin.000001
# mysql主库链接时起始的binlog偏移量
canal.instance.master.position=154
# mysql主库链接时起始的binlog的时间戳
canal.instance.master.timestamp=
canal.instance.master.gtid=
# username/password
# 在MySQL服务器授权的账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=Canal@123456
# 字符集
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
# table regex .*\\..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\\..*
# mysql 数据解析表的黑名单,多个表用,隔开
canal.instance.filter.black.regex=
启动或停止canal-server
./startup.sh
./stop.sh
查看启动状态
1.jps查询启动状态
2.查看log文件 cat canal/log/canal/canal.log
这就启动成功了。
通过Java编写Canal客户端
创建springboot项目
首先引入maven依赖:
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>
提示:如果是maven项目,需要改造为springboot项目的话,需要引入以下依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>
</dependencies>
然后创建一个canal项目,使用SpringBoot构建,如图所示:
客户端实现:
package com.kg.canal;
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.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
import java.util.List;
/**
* @author z 2021/6/4
*/
@Component
public class CannalClient implements InitializingBean {
private final static int BATCH_SIZE = 1000;
@Override
public void afterPropertiesSet() throws Exception {
// 创建链接
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("192.168.152.129", 11111), "example", "", ""
);
try {
//打开连接
connector.connect();
//订阅数据库表,全部表
connector.subscribe(".*\\..*");
//回滚到未进行ack的地方,下次fetch的时候,可以从最后一个没有ack的地方开始拿
connector.rollback();
while (true) {
// 获取指定数量的数据
Message message = connector.getWithoutAck(BATCH_SIZE);
//获取批量ID
long batchId = message.getId();
//获取批量的数量
int size = message.getEntries().size();
//如果没有数据
if (batchId == -1 || size == 0) {
try {
//线程休眠2秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//如果有数据,处理数据
printEntry(message.getEntries());
}
//进行 batch id 的确认。确认之后,小于等于此 batchId 的 Message 都会被确认。(提交确认)
connector.ack(batchId);
// connector.rollback(batchId); // 处理失败, 回滚数据
}
} catch (Exception e) {
e.printStackTrace();
} finally {
connector.disconnect();
}
}
/**
* 打印canal server解析binlog获得的实体类信息
*/
private static void printEntry(List<CanalEntry.Entry> entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
//开启/关闭事务的实体类型,跳过
continue;
}
//RowChange对象,包含了一行数据变化的所有特征
//比如isDdl 是否是ddl变更操作 sql 具体的ddl sql beforeColumns afterColumns 变更前后的数据字段等等
RowChange rowChage;
try {
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e);
}
//获取操作类型:insert/update/delete类型
EventType eventType = rowChage.getEventType();
//打印Header信息
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));
//判断是否是DDL语句
if (rowChage.getIsDdl()) {
System.out.println("================》;isDdl: true,sql:" + rowChage.getSql());
}
//获取RowChange对象里的每一行数据,打印出来
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());
}
}
}
Application.yml
spring:
application:
name: canal
server:
port: 8767
最后我们开始测试,首先启动MySQL、Canal Server(服务端),然后启动刚刚实现的Canal Client(客户端)
然后创建表:
CREATE TABLE `tb_commodity_info` (
`id` varchar(32) NOT NULL,
`commodity_name` varchar(512) DEFAULT NULL COMMENT '商品名称',
`commodity_price` varchar(36) DEFAULT '0' COMMENT '商品价格',
`number` int(10) DEFAULT '0' COMMENT '商品数量',
`description` varchar(2048) DEFAULT '' COMMENT '商品描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';
然后我们在控制台就可以看到监听信息:
如果新增一条数据到表中:
INSERT INTO tb_commodity_info VALUES('3e71a81fd80711eaaed600163e046cc3','叉烧包','3.99',3,'又大又香的叉烧包,老人小孩都喜欢');
控制台可以看到如下信息:
增删改可以监听,那查询呢?
不能监听查询。
小结
canal的好处在于对业务代码没有侵入,因为是基于监听binlog日志去进行数据同步的。实时性也能做到准实时,其实是很多企业一种比较常见的数据同步的方案。
实际项目我们是canal+MQ模式,配合RocketMQ或者Kafka,canal会把监听数据发送到MQ的topic中,然后通过消息队列的消费者进行处理。
设置canal server的ip和port,还有一个destination参数。
destination对应的是canal server的
instance,默认是example,它代表一个完整的监听实例,这里如果有多个连接example实例的client,则只有一个client能获取mysql的数据变更通知。所以要想不同的应用都获取变更通知,则需要连接不同的
instance,再此我们可以在canal server复制一个conf/example文件夹,并重命名为example1即可。
server/client交互协议
get/ack/rollback协议介绍:
(1)Message getWithoutAck(int batchSize)
允许指定batchSize,一次可以获取多条,每次返回的对象为Message,包含的内容为:
batch id: 唯一标识
entries: 具体的数据对象
(2)getWithoutAck(int batchSize, Long timeout, TimeUnit unit)
相比于getWithoutAck(int batchSize),允许设定获取数据的timeout超时时间
(3)void rollback(long batchId)
顾命思议,回滚上次的get请求,重新获取数据。基于get获取的batchId进行提交,避免误操作
(4)void ack(long batchId)
顾命思议,确认已经消费成功,通知canal server删除数据。基于get获取的batchId进行提交,避免误操作
canal的get/ack/rollback协议和常规的
jms
协议有所不同,允许
get/ack
异步处理,比如可以连续调用
get
多次,后续异步按顺序提交
ack/
回滚rollback,项目中称之为流式api
数据对象格式
Entry
Header
logfileName [binlog文件名]
logfileOffset [binlog position]
executeTime [binlog里记录变更发生的时间戳,精确到秒]
schemaName
tableName
eventType [insert/update/delete类型]
entryType [事务头BEGIN/事务尾END/数据ROWDATA]
storeValue [byte数据,可展开,对应的类型为RowChange]
RowChange
isDdl [是否是ddl变更操作,比如create table/drop table]
sql [具体的ddl sql]
rowDatas [具体insert/update/delete的变更数据,可为多条,1个binlog event事件可对应多条变更,比如批处理]
beforeColumns [Column类型的数组,变更前的数据字段]
afterColumns [Column类型的数组,变更后的数据字段]
Column
index
sqlType [jdbc type]
name [column name]
isKey [是否为主键]
updated [是否发生过变更]
isNull [值是否为null]
value [具体的内容,注意为string文本]