Canal搭建

需求:解决私有环境数据库的基础数据同步问题,每当中心库基础数据发生改变时,其他私有库都会增量同步

Canal主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

Canal将自己伪装成MySQL的从服务器,接收主服务器的binlog日志,然后转发给客户端消费

搭建环境

MySQL环境

这里使用docker来快速启动一个mysql来模拟

docker run --name mysql -p 3307:3306 -d -e MYSQL_ROOT_PASSWORD=mysql mysql

启动后需要开启binlog,还需要将binlog的储存格式设置为ROW模式因为用的镜像是8.0的,默认开启了binlig,模式也是ROW

可以根据

SHOW VARIABLES LIKE 'log_bin'
SHOW VARIABLES LIKE 'binlog_format'

这两个sql来查看mysql是设置好

如果要设置的话可以进入到容器里边设置

docker exec -it mysql bash

配置文件在/etc/mysql/

echo 'log-bin=mysql-bin' >> my.cnf
echo 'binlog-format=ROW' >> my.cnf

将配置追加到my.cnf里,然后重启容器即可

以上命令可以简化为

docker exec -it mysql  bash -c "echo 'log-bin=mysql-bin' >> /etc/mysql/my.cnf"
docker exec -it mysql  bash -c "echo 'binlog-format=ROW' >> /etc/mysql/my.cnf"

这样就可以省去进入容器的那一步

如果修改了配置记得重启容器

docker restart mysql

最后需要授权一个账号给canal

CREATE USER canal IDENTIFIED BY 'canal';  
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;

Canal环境

Canal使用的是软件包

官网:https://github.com/alibaba/canal/wiki/QuickStart

下载

wget https://github.com/alibaba/canal/releases/download/canal-1.0.17/canal.deployer-1.0.17.tar.gz

解压

mkdir /tmp/canal
tar zxvf canal.deployer-$version.tar.gz  -C /tmp/canal

配置文件夹里有个example的文件夹,这是一个实列配置(instance),需要将这个实列配置成自己的数据源

vi conf/example/instance.properties

主要是修改数据库地址和用户名

canal.instance.master.address
canal.instance.dbUsername
canal.instance.dbPassword

在启动前还需要改一下启动脚本,因为canal启动默认是需要1G以上的jvm内存,如果内存太小会报错

canal Cannot allocate memory

在启动脚本startup.sh里找到JAVA_OPTS,将内存参数改成256m

然后启动

sh bin/startup.sh

logs里查看canal的日志,观察是否启动成功

客户端

启动成功后需要一个客户端来接收消息

先添加依赖

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.0</version>
</dependency>

官网提供了一个Java版本得Demo

https://github.com/alibaba/canal/wiki/ClientExample

将程序里得ip地址改成canal所在得ip,然后就可以启动了

在连接后会注册一个过滤规则

connector.subscribe(".*\\..*");

这个规则可以指定哪些表被监听

常见例子:

1.  所有表:.*   or  .*\\..*
2.  canal schema下所有表: canal\\..*
3.  canal下的以canal打头的表:canal\\.canal.*
4.  canal schema下的一张表:canal\\.test1
5.  多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)

注意在canal中得instance.properties配置文件里也有这个配置canal.instance.filter.regex,如果在客户端重新注册新的规则,配置文件的规则会被覆盖

使用了哪个规则可以看example日志

2022-02-14 14:41:59.773 [main] INFO  c.a.otter.canal.instance.core.AbstractCanalInstance - subscribe filter change to .*\..*
2022-02-14 14:41:59.773 [main] WARN  c.a.o.canal.parse.inbound.mysql.dbsync.LogEventConvert - --> init table filter : ^.*\..*$

在这个demo中,处理空事件超过120次就会停止

int totalEmptyCount = 120;
while (emptyCount < totalEmptyCount)

可以设置为while(true)

接下来获取事件

Message message = connector.getWithoutAck(batchSize);

Message中保存了List<CanalEntry.Entry>,遍历这个集合就能拿到消息,CanalEntry.Entry保存了元数据以及变更的详情

//反序列化
CanalEntry.RowChange rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
//具体事件
CanalEntry.EventType eventType = rowChage.getEventType();

接下来就是消费消息

 for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == CanalEntry.EventType.DELETE) {
                    printColumn(rowData.getBeforeColumnsList());
                } else if (eventType == CanalEntry.EventType.INSERT) {
                    printColumn(rowData.getAfterColumnsList());
                } else {
                    System.out.println("-------&gt; before");
                    printColumn(rowData.getBeforeColumnsList());
                    System.out.println("-------&gt; after");
                    printColumn(rowData.getAfterColumnsList());
                }
            }

demo中监听了删除事件、新增事件和修改事件,可以看到

getBeforeColumnsList()是变更前的
getAfterColumnsList()是变更后的

更多的事件类型被封装在EventType

最后需要转化为具体的SQL,并在具体环境中执行变更SQL来解决业务场景

新增

   /**
     * @param entry
     * @param rowData
     * @param flag    忽略主键新增
     */
    static void insertSql(CanalEntry.Entry entry, CanalEntry.RowData rowData, boolean flag) {

        StringBuilder sql = new StringBuilder();
        sql.append("insert into ").append(entry.getHeader().getTableName()).append(" (");
        for (int i = 0; i < rowData.getAfterColumnsList().size(); i++) {
            if (flag && rowData.getAfterColumnsList().get(i).getIsKey()) {
                continue;
            }
            sql.append(rowData.getAfterColumnsList().get(i).getName());
            if (i != (rowData.getAfterColumnsList().size() - 1)) {
                sql.append(",");
            }
        }

        sql.append(") ").append("values (");

        for (int i = 0; i < rowData.getAfterColumnsList().size(); i++) {
            if (flag && rowData.getAfterColumnsList().get(i).getIsKey()) {
                continue;
            }
            sql.append(rowData.getAfterColumnsList().get(i).getValue());
            if (i != (rowData.getAfterColumnsList().size() - 1)) {
                sql.append(",");
            }
        }
        sql.append(") ");
        System.out.println(sql);
    }

更新

  static void updateSql(CanalEntry.Entry entry, CanalEntry.RowData rowData) {
        StringBuilder sql = new StringBuilder();
        sql.append("update ").append(entry.getHeader().getTableName()).append(" set ");
        for (int i = 0; i < rowData.getAfterColumnsList().size(); i++) {
            sql.append(rowData.getAfterColumnsList().get(i).getName())
                    .append("=").append(rowData.getAfterColumnsList().get(i).getValue());
            if (i != (rowData.getAfterColumnsList().size() - 1)) {
                sql.append(",");
            }
        }

        sql.append(" where ");

        for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
            if (column.getIsKey()) {
                sql.append(column.getName()).append("=").append(column.getValue());
            }
        }
        System.out.println(sql);
 
    }

删除

    static void deleteSql(CanalEntry.Entry entry, CanalEntry.RowData rowData) {
        StringBuilder sql = new StringBuilder();
        sql.append("delete ").append(entry.getHeader().getTableName()).append(" where ");
        for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
            if (column.getIsKey()) {
                sql.append(column.getName()).append("=").append(column.getValue());
            }
        }
        System.out.println(sql);
 
    }

最后消费了需要提交或者回滚事务

/logs/example中的meta.log中可以看到消费到哪个binlog,哪个偏移量

总结

Canal使用起来并不是非常复杂,虽然需要额外的写一个客户端,但实现起来代码量并不大

在我这个业务场景中,因为基础数据的变更不会非常频繁,所以对性能这方面没有太高要求

posted @ 2022-02-14 14:40  阿弱  阅读(446)  评论(0编辑  收藏  举报