思维导图
前言
我们都知道一个系统最重要的是数据,数据是保存在数据库里。但是很多时候不单止要保存在数据库中,还要同步保存到Elastic Search、HBase、Redis等等。
这时我注意到阿里开源的框架Canal,他可以很方便地同步数据库的增量数据到其他的存储应用。
一、什么是canal
我们先看官网的介绍
canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
这里我们可以简单地把canal理解为一个用来同步增量数据的一个工具。
这是一张官网提供的示意图:
基于日志增量订阅和消费的业务包括
- 数据库镜像
- 数据库实时备份
- 索引构建和实时维护(拆分异构索引、倒排索引等)
- 业务 cache 刷新
- 带业务逻辑的增量数据处理
canal的工作原理就是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。
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官网。
与其问canal能做什么,不如说数据同步有什么作用。
但是canal的数据同步不是全量的,而是增量。基于binary log增量订阅和消费,canal可以做:
- 数据库镜像
- 数据库实时备份
- 索引构建和实时维护
- 业务cache(缓存)刷新
- 带业务逻辑的增量数据处理
三、如何搭建canal
3.1 首先有一个MySQL服务器
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
我的Linux服务器安装的MySQL服务器是5.7版本。
MySQL的安装这里就不演示了,比较简单,网上也有很多教程。
canal的原理是基于mysql binlog技术,所以这里一定需要开启mysql的binlog写入功能
1.检查binlog功能是否有开启,如果显示状态为OFF表示该功能未开启,开启binlog功能
mysql> show variables like 'log_bin';
2.如果显示状态为OFF表示该功能未开启,开启binlog功能
1,修改 mysql 的配置文件 my.cnf
vi /etc/my.cnf
追加内容:
log-bin=mysql-bin #binlog文件名
binlog_format=ROW #选择row模式
server_id=1 #mysql实例id,不能和canal的slaveId重复
2,重启 mysql:
service mysql restart
然后在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';
FLUSH PRIVILEGES;
下一步在MySQL配置文件my.cnf设置如下信息:
[mysqld]
# 打开binlog
log-bin=mysql-bin
# 选择ROW(行)模式
binlog-format=ROW
# 配置MySQL replaction需要定义,不要和canal的slaveId重复
server_id=1
改了配置文件之后,重启MySQL,使用命令查看是否打开binlog模式:
查看binlog日志文件列表:
查看当前正在写入的binlog文件:
MySQL服务器这边就搞定了,很简单。
3.2 安装canal
去官网下载页面进行下载:https://github.com/alibaba/canal/releases
我这里下载的是1.1.4的版本:
解压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
# canal.instance.defaultDatabaseName 默认那个库,这里指定为test(需要在MySQL中建立一个test库)
# table regex .*\\..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\\..*
# mysql 数据解析表的黑名单,多个表用,隔开
canal.instance.filter.black.regex=
我这里用的是win10系统,所以在bin目录下找到startup.bat启动:
启动就报错,坑呀:【注意查看启动日志,根据报错信息处理问题】
要修改一下启动的脚本startup.bat:
然后再启动脚本:
这就启动成功了。
3.3、Canal 服务端
1.1 canal.properties:
该文件是canal 服务端的配置文件, 在改配置文件中需要修改如下:
# 启动端口,也是客户端连接的端口
canal.port = 11111
# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ 与canal 连接的客户端
# 如果是通过代码进行连接,这里为tcp
canal.serverMode = tcp
# canal 加载mysql 的实例
canal.destinations = example
1.2 canal的监听实例:
经过实践得知 canal.destinations 中定义的监听实例 与数据库中的某个实例名称是无关的:
也就是说 在canal.destinations 可以定义任意名字的实例,比如我们定义 aabbcc:
然后只需要在 canal\conf 的目录下新建一个文件夹,名字为 aabbcc 即可:
然后将 example 下的文件全部拷贝到 aabbcc 下:
然后设置要连接的数据库:
canal.instance.master.address=localhost:3406
canal.instance.dbUsername=root
canal.instance.dbPassword=ddsoft
3.4 、canal-admin
但是你有没有发现这种方式每新增一个instance,都需要修改配置文件并重启,这样会导致数据同步中断不太友好,而且也没有canal server服务的状态监控,着实觉得这框架不够完善。阿里巴巴也考虑到了这些问题,所以提供了canal-admin,canal-admin设计上是为canal提供整体配置管理、节点运维等面向运维的功能,提供相对友好的WebUI操作界面,方便更多用户快速和安全的操作。注意:canal-admin有以下限制要求
MySQL,用于存储配置和节点等相关数据 canal版本,要求>=1.1.4 (需要依赖canal-server提供面向admin的动态运维管理接口)
在官网下载canal-admin的安装包解压如下:
bin canal.admin-1.1.5.tar.gz conf lib logs
直接来看conf下的文件:
application.yml canal_manager.sql canal-template.properties instance-template.properties logback.xml public
这里看到的就是一个spring boot框架开发的web项目啦,anal_manager.sql就是canal-admin服务所依赖的数据库初始化脚本,我们得去MySQL执行,然后修改配置文件application.yml
server:
port: 8089
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
spring.datasource:
address: 10.10.0.10:3306
database: canal_manager
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
hikari:
maximum-pool-size: 30
minimum-idle: 1
canal:
adminUser: admin
adminPasswd: admin
这里就配置一下前面执行SQL脚本数据库的连接信息即可,当然如果端口8089被占用了就改成别的,到时候canal server配置对应的就行。在canal-admin的目录执行下面命令就能启动了:
sh bin/startup.sh
这时候通过主机ip:8089就能在浏览器访问:
默认登录用户名密码:admin/123456,成功进入之后:
我们可以通过界面管理canal集群、canal server 、server下的instance。这样无论是我们修改instance的配置还是新增一个instance都不需要去服务器操作并重启服务了,是不是很方便,直接通过界面操作修改、重启即可。
当然还是需要像一开始一样在服务器启动canal server的,需要把配置canal.properties改成如下:
# register ip
canal.register.ip =
# canal admin config
canal.admin.manager = 10.10.0.10:8089
canal.admin.port = 11110
canal.admin.user = admin
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
# admin auto register
canal.admin.register.auto = true
canal.admin.register.cluster =
canal.admin.register.name =
这里最主要是绑定关联canal-admin,配置admin的地址信息。这里提一下canal.register.ip这个配置是和canal集群有关的,canal集群是依靠zookeeper实现,这里就不展开细讲了。成功启动canal server之后,就可以在admin界面看到了:
然后我们可以基于canal server新增instance:mall和fast-api
Java客户端操作
基础操作
首先引入maven依赖:
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>
然后创建一个canal项目,使用SpringBoot构建,如图所示:
在CannalClient类使用Spring Bean的生命周期函数afterPropertiesSet():
@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("127.0.0.1", 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);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
connector.disconnect();
}
}
/**
* 打印canal server解析binlog获得的实体类信息
*/
private static void printEntry(List<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());
}
}
}
以上就完成了Java客户端的代码。这里不做具体的处理,仅仅是打印,先有个直观的感受。
最后我们开始测试,首先启动MySQL、Canal Server,还有刚刚写的Spring Boot项目。然后创建表:
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客户端
2.1 客户端配置要监听的实例:
客户端通过 CanalConnectors.newSingleConnector 来创建连接对象:
@Bean
public CanalConnector canalConnector() {
CanalConnector canalConnector1 = CanalConnectors.newSingleConnector(new InetSocketAddress("localhost", "11111"), "aabbcc", "", "");
canalConnectors.add(canalConnector1);
return canalConnector1;
}
2.2 通过连接获取信息
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
@Slf4j
@Component
public class CanalService {
@Autowired
private CanalConnector canalConnector;
@Autowired
private CanalListener canalListener;
@PostConstruct
public void run() {
// 定义最后消费的位点
long lastOffset = fetchFromPosition();
while (true) {
Message message = canalConnector.getWithoutAck(10);
long batchId = message.getId();
List<CanalEntry.Entry> entryList = message.getEntries();
int size = message.getEntries().size();
if (batchId == -1 || entryList.isEmpty()) {
try {
// 线程休眠2秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
long nowOffset = entryList.get(0).getHeader().getLogfileOffset();
if (nowOffset <= lastOffset) {
continue;
}
try {
canalListener.onMessage(message);
canalConnector.ack(batchId);
// 保存最后消费的位点
lastOffset = message.getEntries().get(size - 1).getHeader().getLogfileOffset();
savePositionState(lastOffset);
} catch (Exception ex) {
log.error("consume error:{}", ex.getMessage());
}
}
}
// 获取并设置消费的起始位点
private long fetchFromPosition() {
// Canal 连接器连接
canalConnector.connect();
// 订阅数据变更:这里是连接服务端 aabbcc 实例下 监听哪些表 其中biglog 和 bluegrass 都是改实例下的mysql 实例
// user,student,about_us 是各自数据库下的表
canalConnector.subscribe("biglog.user|biglog.student|biglog.about_us|bluegrass.about_us");
// 从存储中获取上次消费的位点
long position = getPositionState();
if (position != -1) {
// 回滚到上次保存的位点
canalConnector.rollback(position);
}
return position;
}
// 获取位点状态
private static long getPositionState() {
// TODO: 从存储中获取上次消费的位点
return -1;
}
// 保存位点状态
private static void savePositionState(long position) {
// TODO: 将 position 保存到存储中
}
}
2.3、方法一:修改canal deploy conf下example Instance.properties配置的过滤正则
2.4、方法二:修改java程序下connector.subscribe配置的过滤正则
总结
canal的好处在于对业务代码没有侵入,因为是基于监听binlog日志去进行同步数据的。实时性也能做到准实时,其实是很多企业一种比较常见的数据同步的方案。
通过上面的学习之后,我们应该都明白canal是什么,它的原理,还有用法。实际上这仅仅只是入门,因为实际项目中我们不是这样玩的…
实际项目我们是配置MQ模式,配合RocketMQ或者Kafka,canal会把数据发送到MQ的topic中,然后通过消息队列的消费者进行处理。
Canal的部署也是支持集群的,需要配合ZooKeeper进行集群管理。
Canal还有一个简单的Web管理界面。