单机canal监听binlog

本文介绍了基于MySQL数据库的增量日志解析工具Canal,用于实现数据库的实时同步、备份、索引维护等功能。讲解了Canal的环境部署,包括MySQL的配置、Canal的安装与实例配置,以及Canal客户端的开发。同时,展示了如何将Canal消息转换为JSON数据。
摘要由CSDN通过智能技术生成

简介

  • 基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

  • 早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger(触发器) 获取增量变更

  • 从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务,基于日志增量订阅和消费的业务包括

    • 数据库镜像
    • 数据库实时备份
    • 索引构建和实时维护(拆分异构索引、倒排索引等)
    • 业务 cache 刷新
    • 带业务逻辑的增量数据处理
  • 当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

  • github地址:https://github.com/alibaba/canal

环境部署

MySQL

  • docker安装mysql,MySQL需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式
 docker run --name mysql5.7 -p 3309:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7

docker exec mysql5.7 bash -c "echo 'log-bin=/var/lib/mysql/mysql-bin' >> /etc/mysql/mysql.conf.d/mysqld.cnf"

docker exec mysql5.7 bash -c "echo 'server-id=123454' >> /etc/mysql/mysql.conf.d/mysqld.cnf"

docker restart mysql
  • 授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant

    GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' ;
    FLUSH PRIVILEGES;
    

Canal安装

重要版本更新说明:

  1. canal 1.1.x 版本(release_note),性能与功能层面有较大的突破,重要提升包括:
  2. canal 1.1.4版本,迎来最重要的WebUI能力,引入canal-admin工程,支持面向WebUI的canal动态管理能力,支持配置、任务、日志等在线白屏运维能力,具体文档:Canal Admin Guide

注意:本次学习使用的版本canal1.0.24

环境要求:

  • 安装好ZooKeeper
  • 解压缩

    mkdir /export/servers/canal
    tar -zxvf canal.deployer-1.0.24.tar.gz  -C /export/servers/canal/
    
  • 解压完成后,进入 /export/servers/canal/ 目录,可以看到如下结构

    drwxr-xr-x. 2 root root 4096 21 14:07 bin
    drwxr-xr-x. 4 root root 4096 21 14:07 conf
    drwxr-xr-x. 2 root root 4096 21 14:07 lib
    drwxrwxrwx. 2 root root 4096 41 2017 logs
    
  • canal server的conf下有几个配置文件

    [root@node1 canal]# tree conf/ 
    conf/
    ├── canal.properties
    ├── example
    │   └── instance.properties
    ├── logback.xml
    └── spring
        ├── default-instance.xml
        ├── file-instance.xml
        ├── group-instance.xml
        ├── local-instance.xml
        └── memory-instance.xml
    
    • 先来看canal.propertiescommon属性前四个配置项:

      canal.id= 1
      canal.ip=
      canal.port= 11111
      canal.zkServers=
      

      canal.id是canal的编号,在集群环境下,不同canal的id不同,注意它和mysql的server_id不同。

      ip这里不指定,默认为本机,比如上面是192.168.1.120,端口号是11111。zk用于canal cluster。

    • 再看下canal.propertiesdestinations相关的配置:

      #################################################
      #########       destinations        ############# 
      #################################################
      canal.destinations = example
      canal.conf.dir = ../conf
      canal.auto.scan = true
      canal.auto.scan.interval = 5
      
      canal.instance.global.mode = spring 
      canal.instance.global.lazy = false
      canal.instance.global.spring.xml = classpath:spring/file-instance.xml
      

      这里的canal.destinations = example可以设置多个,比如example1,example2,
      则需要创建对应的两个文件夹,并且每个文件夹下都有一个instance.properties文件。

      全局的canal实例管理用spring,这里的file-instance.xml最终会实例化所有的destinations instances:

    • 全局的canal实例管理用spring,这里的file-instance.xml最终会实例化所有的destinations instances:

      <!-- properties -->
      <bean class="com.alibaba.otter.canal.instance.spring.support.PropertyPlaceholderConfigurer" lazy-init="false">
      	<property name="ignoreResourceNotFound" value="true" />
          <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/><!-- 允许system覆盖 -->
          <property name="locationNames">
          	<list>
              	<value>classpath:canal.properties</value>                     <value>classpath:${canal.instance.destination:}/instance.properties</value>
               </list>
          </property>
      </bean>
      
      <bean id="socketAddressEditor" class="com.alibaba.otter.canal.instance.spring.support.SocketAddressEditor" />
      <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> 
         <property name="propertyEditorRegistrars">
      	   <list>
          		<ref bean="socketAddressEditor" />
             </list>
         </property>
      </bean>
      <bean id="instance" class="com.alibaba.otter.canal.instance.spring.CanalInstanceWithSpring">
      	<property name="destination" value="${canal.instance.destination}" />
          <property name="eventParser">
          	<ref local="eventParser" />
          </property>
          <property name="eventSink">
              <ref local="eventSink" />
          </property>
          <property name="eventStore">
              <ref local="eventStore" />
          </property>
          <property name="metaManager">
              <ref local="metaManager" />
          </property>
          <property name="alarmHandler">
              <ref local="alarmHandler" />
          </property>
      </bean>
      

      比如canal.instance.destination等于example,就会加载example/instance.properties配置文件

  • 修改instance 配置文件

    vi conf/example/instance.propertiess

    ## mysql serverId,这里的slaveId不能和myql集群中已有的server_id一样
    canal.instance.mysql.slaveId = 1234
    
    #  按需修改成自己的数据库信息
    #################################################
    ...
    canal.instance.master.address=192.168.1.120:3306
    # username/password,数据库的用户名和密码
    ...
    canal.instance.dbUsername = root
    canal.instance.dbPassword = 123456
    #################################################
    
  • 启动

    sh bin/startup.sh
    
  • 查看 server 日志

  • vi logs/canal/canal.log
    
    2013-02-05 22:45:27.967 [main] INFO  com.alibaba.otter.canal.deployer.CanalLauncher - ## start the canal server.
    2013-02-05 22:45:28.113 [main] INFO  com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[10.1.29.120:11111]
    2013-02-05 22:45:28.210 [main] INFO  com.alibaba.otter.canal.deployer.CanalLauncher - ## the canal server is running now ......
    
  • 查看 instance 的日志

    vi logs/example/example.log
    
    2013-02-05 22:50:45.636 [main] INFO  c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [canal.properties]
    2013-02-05 22:50:45.641 [main] INFO  c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [example/instance.properties]
    2013-02-05 22:50:45.803 [main] INFO  c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start CannalInstance for 1-example 
    2013-02-05 22:50:45.810 [main] INFO  c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start successful....
    
  • 关闭

    sh bin/stop.sh
    

Canal客户端开发

创建client_demo项目

Maven依赖

<dependencies>
	<dependency>
    	<groupId>com.alibaba.otter</groupId>
        <artifactId>canal.client</artifactId>
        <version>1.0.24</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.58</version>
    </dependency>
</dependencies>

在canal_demo模块创建包结构

包名说明
com.itheima.canal_demo代码存放目录

开发步骤

  1. 创建Connector
  2. 连接Cannal服务器,并订阅
  3. 解析Canal消息,并打印
Canal消息格式
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文本]

参考代码:

public class CanalClientEntrance {
    public static void main(String[] args) {
        // 1. 创建链接
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.88.120",
                11111), "example", "canal", "canal");

        // 指定一次性获取数据的条数
        int batchSize = 5 * 1024;
        boolean running = true;

        try {
            while(running) {
                // 2. 建立连接
                connector.connect();
                // 回滚上次的get请求,重新获取数据
                connector.rollback();
                // 订阅匹配日志
                connector.subscribe("itcast_shop.*");
                while(running) {
                    // 批量拉取binlog日志,一次性获取多条数据
                    Message message = connector.getWithoutAck(batchSize);
                    // 获取batchId
                    long batchId = message.getId();
                    // 获取binlog数据的条数
                    int size = message.getEntries().size();
                    if(batchId == -1 || size == 0) {

                    }
                    else {
                        printSummary(message);
                    }
                    // 确认指定的batchId已经消费成功
                    connector.ack(batchId);
                }
            }
        } finally {
            // 断开连接
            connector.disconnect();
        }
    }

    private static void printSummary(Message message) {
        // 遍历整个batch中的每个binlog实体
        for (CanalEntry.Entry entry : message.getEntries()) {
            // 事务开始
            if(entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }

            // 获取binlog文件名
            String logfileName = entry.getHeader().getLogfileName();
            // 获取logfile的偏移量
            long logfileOffset = entry.getHeader().getLogfileOffset();
            // 获取sql语句执行时间戳
            long executeTime = entry.getHeader().getExecuteTime();
            // 获取数据库名
            String schemaName = entry.getHeader().getSchemaName();
            // 获取表名
            String tableName = entry.getHeader().getTableName();
            // 获取事件类型 insert/update/delete
            String eventTypeName = entry.getHeader().getEventType().toString().toLowerCase();

            System.out.println("logfileName" + ":" + logfileName);
            System.out.println("logfileOffset" + ":" + logfileOffset);
            System.out.println("executeTime" + ":" + executeTime);
            System.out.println("schemaName" + ":" + schemaName);
            System.out.println("tableName" + ":" + tableName);
            System.out.println("eventTypeName" + ":" + eventTypeName);

            CanalEntry.RowChange rowChange = null;

            try {
                // 获取存储数据,并将二进制字节数据解析为RowChange实体
                rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (InvalidProtocolBufferException e) {
                e.printStackTrace();
            }

            // 迭代每一条变更数据
            for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                // 判断是否为删除事件
                if(entry.getHeader().getEventType() == CanalEntry.EventType.DELETE) {
                    System.out.println("---delete---");
                    printColumnList(rowData.getBeforeColumnsList());
                    System.out.println("---");
                }
                // 判断是否为更新事件
                else if(entry.getHeader().getEventType() == CanalEntry.EventType.UPDATE) {
                    System.out.println("---update---");
                    printColumnList(rowData.getBeforeColumnsList());
                    System.out.println("---");
                    printColumnList(rowData.getAfterColumnsList());
                }
                // 判断是否为插入事件
                else if(entry.getHeader().getEventType() == CanalEntry.EventType.INSERT) {
                    System.out.println("---insert---");
                    printColumnList(rowData.getAfterColumnsList());
                    System.out.println("---");
                }
            }
        }
    }

    // 打印所有列名和列值
    private static void printColumnList(List<CanalEntry.Column> columnList) {
        for (CanalEntry.Column column : columnList) {
            System.out.println(column.getName() + "\t" + column.getValue());
        }
    }
}

转换为JSON数据

  • 复制上述代码,将binlog日志封装在一个Map结构中,使用fastjson转换为JSON格式

参考代码:

    // binlog解析为json字符串
    private static String binlogToJson(Message message) throws InvalidProtocolBufferException {
        // 1. 创建Map结构保存最终解析的数据
        Map rowDataMap = new HashMap<String, Object>();

        // 2. 遍历message中的所有binlog实体
        for (CanalEntry.Entry entry : message.getEntries()) {
            // 只处理事务型binlog
            if(entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
            entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }

            // 获取binlog文件名
            String logfileName = entry.getHeader().getLogfileName();
            // 获取logfile的偏移量
            long logfileOffset = entry.getHeader().getLogfileOffset();
            // 获取sql语句执行时间戳
            long executeTime = entry.getHeader().getExecuteTime();
            // 获取数据库名
            String schemaName = entry.getHeader().getSchemaName();
            // 获取表名
            String tableName = entry.getHeader().getTableName();
            // 获取事件类型 insert/update/delete
            String eventType = entry.getHeader().getEventType().toString().toLowerCase();

            rowDataMap.put("logfileName", logfileName);
            rowDataMap.put("logfileOffset", logfileOffset);
            rowDataMap.put("executeTime", executeTime);
            rowDataMap.put("schemaName", schemaName);
            rowDataMap.put("tableName", tableName);
            rowDataMap.put("eventType", eventType);

            // 封装列数据
            Map columnDataMap = new HashMap<String, Object>();
            // 获取所有行上的变更
            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            List<CanalEntry.RowData> columnDataList = rowChange.getRowDatasList();
            for (CanalEntry.RowData rowData : columnDataList) {
                if(eventType.equals("insert") || eventType.equals("update")) {
                    for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
                        columnDataMap.put(column.getName(), column.getValue());
                    }
                }
                else if(eventType.equals("delete")) {
                    for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
                        columnDataMap.put(column.getName(), column.getValue());
                    }
                }
            }

            rowDataMap.put("columns", columnDataMap);
        }

        return JSON.toJSONString(rowDataMap);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值