数据同步神器Canal

简介

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文本]  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值