Spring boot整合canal实现数据同步

一、前言

canal:阿里巴巴 MySQL binlog 增量订阅&消费组件

Home · alibaba/canal Wiki · GitHub

canal 是阿里巴巴 MySQL 数据库 Binlog 的增量订阅 & 消费组件。

名称:canal [kə'næl]
译意: 水道 / 管道 / 沟渠
语言: 纯 java 开发
定位: 基于数据库增量日志解析,提供增量数据订阅 & 消费,目前主要支持了 MySQL、Kafka、es、Hbase、mq等

早期,阿里巴巴 B2B 公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于 trigger 的方式获取增量 变更,不过从 2010 年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅 & 消费的业务,从此开 启了一段新纪元。ps. 目前内部使用的同步,已经支持 MySQL 5.x 和 Oracle 部分版本的日志解析

基于日志增量订阅 & 消费支持的业务:

  1. 数据库镜像

  2. 数据库实时备份

  3. 多级索引 (卖家和买家各自分库索引)

  4. search build

  5. 业务 cache 刷新

  6. 价格变化等重要业务消息

Canal 工作原理:

Spring boot整合canal实现数据同步_第1张图片

MySQL主从复制实现

复制遵循三步过程:

主服务器将更改记录到binlog中(这些记录称为binlog事件,可以通过来查看show binary events)
从服务器将主服务器的二进制日志事件复制到其中继日志。
中继日志中的从服务器重做事件随后将更新其旧数据。
如何运作

原理很简单:

  1. canal 模拟 mysql slave 的交互协议,伪装自己为 mysql slave,向 mysql master 发送 dump 协议

  2. mysql master 收到 dump 请求,开始推送 binary log 给 slave (也就是 canal)

  3. canal 解析 binary log 对象 (原始为 byte 流)

三、mysql安装

有mysql的情况

只需要在工具下运行  SHOW VARIABLES LIKE 'log_%';

Spring boot整合canal实现数据同步_第2张图片

 

无mysql的情况

以下是我以Centos7服务器安装MySql 5.7为例,自行去官网下载

在my.conf文件中的 [mysqld] 下添加以下三行内容

  • log-bin=mysql-bin # 开启 binlog
  • binlog-format=ROW # 选择 ROW 模式 读行
  • server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

或者复制我这个my.conf的全部内容只需要修改对应的目录就行 

[client]
port = 3306
socket = /var/lib/mysql/mysql.sock
default-character-set = utf8mb4

[mysqld]
port = 3306
socket = /var/lib/mysql/mysql.sock
basedir = /usr/local/mysql
datadir = /data/mysql
pid-file = /var/run/mysqld/mysqld.pid
user = mysql
server-id = 1
log-bin = mysql-bin
log_slave_updates = on
auto_increment_increment=1
auto_increment_offset=1
lower_case_table_names=1

init-connect = 'SET NAMES utf8mb4'
character-set-server = utf8mb4
performance_schema_max_table_instances = 200
table_definition_cache=200
table_open_cache=128
log_error = /var/log/mysqld.log

character_set_server=utf8mb4
collation_server=utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'
bind-address = 0.0.0.0

skip-name-resolve
back_log = 600
max_connections = 1000
max_connect_errors = 6000
open_files_limit = 65535
max_allowed_packet = 512M
binlog_cache_size = 1M

log_bin = mysql-bin
binlog_format = row
expire_logs_days = 7
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /var/log/mysql-slow.log

四、Canal安装

Releases · alibaba/canal · GitHub

Spring boot整合canal实现数据同步_第3张图片

 下载

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

解压

mv canal.deployer-1.1.6.tar.gz /data/canal/

cd /data/canal

tar -zxvf canal.deployer-1.1.6.tar.gz 

 修改配置信信息

vi  conf/example/instance.properties

# position info
canal.instance.master.address=127.0.0.1:3306

# username/password
canal.instance.dbUsername=改成数据库用户
canal.instance.dbPassword=改成数据库密码

Spring boot整合canal实现数据同步_第4张图片

启动

 cd /data/canal/bin

 sh start.sh

 

 Springboot项目整合


    
        org.springframework.boot
        spring-boot-starter-web
    
    
        com.alibaba.otter
        canal.client
        1.1.3
    

 监听程序

 

package com.toms.canal.utils;

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.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;
import java.util.Date;
import java.util.List;

/**
 * @author 重庆阿汤哥
 * @Description: 基于canal实现mysql binLog监听
 * @date 2022/6/7  18:23
 */
@Slf4j
@Component
public class CanalCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
       //在canal部署的conf/canal.properties ip和端口信息
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("canal 部署的ip", canal 的端口), "example", "", "");
        try {
            //打开连接
            connector.connect();
            //订阅数据库表,全部表q
            // connector.subscribe(".*\\..*");
            // 监听jes库中的字典表
            connector.subscribe("jes.jes_dictionary");
            //回滚到未进行ack的地方,下次fetch的时候,可以从最后一个没有ack的地方开始拿
            connector.rollback();
            while (true) {
                // 获取指定数量的数据
                Message message = connector.getWithoutAck(1);
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId > 0 && size != 0) {
                    handleDATAChange(message.getEntries());
                }
                // 提交确认
                connector.ack(batchId);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            connector.disconnect();
            //防止频繁访问数据库链接: 线程睡眠 10秒
            try {
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void handleDATAChange(List entrys) {
        for (CanalEntry.Entry entry : entrys) {
            // 只解析mysql事务的操作,其他的不解析
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }
            //RowChange对象,包含了一行数据变化的所有特征
            CanalEntry.RowChange rowChange;
            try {
                rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e);
            }
            CanalEntry.EventType eventType = rowChange.getEventType();
            // 获取当前操作所属的数据库
            String dbName = entry.getHeader().getSchemaName();
            // 获取当前操作所属的表
            String tableName = entry.getHeader().getTableName();

            // 事务提交时间
            long timestamp = entry.getHeader().getExecuteTime();

            log.info("Canal监测到更新:【{}】库的【{}】表", dbName, tableName);

            for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                dataDetails(rowData.getBeforeColumnsList(), rowData.getAfterColumnsList(), dbName, tableName, eventType, timestamp);
                log.info("-------------------------------------------------------------");
            }

        }
    }

    /**
     * 解析具体一条Binlog消息的数据
     *
     * @param dbName    当前操作所属数据库名称
     * @param tableName 当前操作所属表名称
     * @param eventType 当前操作类型(新增、修改、删除)
     */
    private static void dataDetails(List beforeColumns,
                                    List afterColumns,
                                    String dbName,
                                    String tableName,
                                    CanalEntry.EventType eventType,
                                    long timestamp) {

        log.info("数据库:{},表名:{},操作类型:{}", dbName, tableName, eventType);

        if (CanalEntry.EventType.INSERT.equals(eventType)) {
            log.info("新增数据:");
            printColumn(afterColumns);
        } else if (CanalEntry.EventType.DELETE.equals(eventType)) {
            log.info("删除数据:");
            printColumn(beforeColumns);
        } else {
            log.info("更新数据:更新前数据--");
            printColumn(beforeColumns);
            log.info("更新数据:更新后数据--");
            printColumn(afterColumns);
        }
        log.info("操作时间:{}", new Date(timestamp));
    }

    private static void printColumn(List columns) {
        for (CanalEntry.Column column : columns) {
            log.info(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
        }
    }
}

 效果

 

 

总结

  • canal服务端以mysql主从复制的形式监听mysql的数据表的变化,然后将监听的数据发送给canal客户端
  •  通过CanalCommandLineRunner.java代码分析,canal客户端以while死循环的方式实时获取canal发送的数据
  • canal客户端获取数据后,程序可以根据CanalEntry.EventType判断属于什么操作,根据业务需要进行业务代码开发
     

你可能感兴趣的:(Canal,MySQL,spring,boot,数据库)