【Canal】数据实时增量工具

关于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.modecanal 客户端类型 目前支持4种类型 simple,cluster,zk,kafka(kafka 目前支持flatMessage 格式)simple
canal.filtercanal过滤的表名称,如配置则只订阅配置的表“”
canal.batch-size消息的数量,超过该次数将进行一次消费1
canal.timeout消费的时间间隔(s)1s
canal.server服务地址,多个地址以,分隔 格式 h o s t : {host}: host:{port}null
canal.destinationcanal 的instance 名称,kafka模式为topic 名称null
canal.user-namecanal 的用户名null
canal.passwordcanal 的密码null
canal.group-id kafka groupId消费者订阅消息时可使用,kafka canal 客户端null
canal.async是否是异步消费,异步消费时,消费时异常将导致消息不会回滚,也不保证顺序性true
canal.partitionkafka partitionnull

添加依赖配置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等,还是非常便捷的,此本不做表述,后期有空再做补充

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

总在寒冷清秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值