使用canal订阅mysql的binlog,springboot使用canal订阅mysql的binlog

300 阅读4分钟

@[TOC]

写在前面

本文用到了docker,安装docker请移步:centos7安装docker-简单而详细无坑

canal开源地址:https://github.com/alibaba/canal

canal原理: canal 模拟 MySQL slave 的交互协议,伪装自己为MySQL slave,向 MySQL master 发送dump 协议 MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) canal 解析 binary log 对象(原始为 byte 流)

一、初始化mysql

1、安装mysql

docker安装mysql-简单无坑

# 拉取mysql最新版本
docker pull mysql
# 启动mysql,root/root
docker run -p 3306:3306 --name mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql

# 设置容器自启动:
sudo docker update 9dff25e9d363 --restart=always
# 进入容器
docker exec -it 9dff25e9d363 /bin/bash
# 安装vi
apt-get update && apt-get install -y vim

2、创建数据库

在这里创建了一个名为【mytest】的数据库。

3、修改mysql配置文件

修改my.cnf(默认在/etc/mysql目录),添加以下两行

[mysqld]
server_id=1
#开启二进制
log-bin=mysql-bin
binlog-format=ROW
#指定库,缩小监控的范围。
binlog-do-db=mytest

改完记得重启mysql。(/mysql/data目录下会出现mysql-bin.000001)

docker restart mysql

4、创建一个同步用户

在mysql中创建一个专门用来同步的用户

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;

5、查看主库的状态

在这里插入图片描述

二、初始化canal

1、使用docker启动canal

# 下载canal
docker pull canal/canal-server:v1.1.5

# 启动canal
docker run -p 11111:11111 --name canal -d canal/canal-server:v1.1.5

2、查看mysql地址

# 这里是172.17.0.2
docker inspect mysql

3、修改canal配置文件

修改canal.properties配置文件(/home/admin/canal-server/conf目录)(不需要改)

# 默认端口 11111
# 默认输出model为tcp, mysql就使用tcp
# tcp, kafka, RocketMQ
#canal.serverMode = tcp

#################################################
######### destinations ############# 
#################################################
# canal可以有多个instance,每个实例有独立的配置文件,默认只 有一个example实例。
# 如果需要处理多个mysql数据的话,可以复制出多个example,对其重新命名,
# 命令和配置文件中指定的名称一致。然后修改canal.properties 中的 canal.destinations
# canal.destinations=实例 1,实例 2,实例 3
#canal.destinations = example

修改instance.properties配置文件(/home/admin/canal-server/conf/example目录)(只需要修改mysql地址即可)

# 不能和mysql master重复
canal.instance.mysql.slaveId=2
# 使用mysql的虚拟ip和端口 需要改
canal.instance.master.address=172.17.0.2:3306
# 使用已创建的canal用户 默认就有
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
# canal.instance.defaultDatabaseName =test

# 表示匹配所有的库所有的表 默认就是
canal.instance.filter.regex =.*\\..*

4、重启canal

docker restart canal

当canal监听到binlog发生变化,会通知canal客户端。

三、java使用canal客户端

1、依赖

maven:

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

gradle:

implementation 'com.alibaba.otter:canal.client:1.1.2'

2、核心代码

import com.alibaba.fastjson.JSONObject;
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 com.google.protobuf.ByteString;

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

public class CanalClient {

    public static void main(String[] args) throws Exception{

        //1.获取 canal 连接对象
        CanalConnector canalConnector =
                CanalConnectors.newSingleConnector(new
                        InetSocketAddress("canal所在服务器IP", 11111), "example", "", "");

        System.out.println("canal启动并开始监听数据 ...... ");
        while (true){
            canalConnector.connect();
            //订阅表
            canalConnector.subscribe("shop001.*");
            //获取数据
            Message message = canalConnector.get(100);
            //解析message
            List<CanalEntry.Entry> entries = message.getEntries();
            if(entries.size() <=0){
                System.out.println("未检测到数据");
                Thread.sleep(1000);
            }
            for(CanalEntry.Entry entry : entries){
                //1、获取表名
                String tableName = entry.getHeader().getTableName();
                //2、获取类型
                CanalEntry.EntryType entryType = entry.getEntryType();
                //3、获取序列化后的数据
                ByteString storeValue = entry.getStoreValue();

                //判断是否rowdata类型数据
                if(CanalEntry.EntryType.ROWDATA.equals(entryType)){
                    //对第三步中的数据进行解析
                    CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
                    //获取当前事件的操作类型
                    CanalEntry.EventType eventType = rowChange.getEventType();
                    //获取数据集
                    List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
                    //便利数据
                    for(CanalEntry.RowData rowData : rowDatasList){
                        //数据变更之前的内容
                        JSONObject beforeData = new JSONObject();
                        List<CanalEntry.Column> beforeColumnsList = rowData.getAfterColumnsList();
                        for(CanalEntry.Column column : beforeColumnsList){
                            beforeData.put(column.getName(),column.getValue());
                        }
                        //数据变更之后的内容
                        List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
                        JSONObject afterData = new JSONObject();
                        for(CanalEntry.Column column : afterColumnsList){
                            afterData.put(column.getName(),column.getValue());
                        }
                        System.out.println("Table :" + tableName +
                                ",eventType :" + eventType +
                                ",beforeData :" + beforeData +
                                ",afterData : " + afterData);
                    }
                }else {
                    System.out.println("当前操作类型为:" + entryType);
                }
            }
        }
    }
}
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 com.google.protobuf.InvalidProtocolBufferException;

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

public class CanalStarter {


    public static void main(String[] args) {
        CanalConnector connector = CanalConnectors.newSingleConnector(
                new InetSocketAddress("192.168.56.10", 11111),
                "example",
                "",
                ""
        );

        try {
            // 连接
            connector.connect();
            // 订阅所有数据库
            connector.subscribe(".*\\..*");
            // 定位到上次消费的位置
            connector.rollback();

            while (true) {
                // 我拿到消息之后,不用去返回回馈,拿100条
                Message msg = connector.getWithoutAck(100);

                // 没拿到消息,重新拿
                if(msg == null || msg.getId() < 0 || msg.getEntries().size() == 0) {
                    System.out.println("nothing consumed");
                    Thread.sleep(1000);
                    continue;
                }

                // 解析信息
                printEntry(msg.getEntries());
                // ack
                connector.ack(msg.getId());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        } finally {
            // 连接关闭
            connector.disconnect();
        }
    }

    private static void printEntry(List<CanalEntry.Entry> entries) throws InvalidProtocolBufferException {
        for (CanalEntry.Entry entry : entries) {
            // 事务开启和事务结束不去管
            if(entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                    entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }
            System.out.println("*********** message event");
            System.out.println("table name:" + entry.getHeader().getTableName());
            System.out.println("entry type:" + entry.getEntryType());
            // 拿到消息
            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                System.out.println("event type : " + rowChange.getEventType());
                System.out.println("*********** before change");
                printRowData(rowData.getBeforeColumnsList());
                System.out.println("*********** after change");
                printRowData(rowData.getAfterColumnsList());
            }
        }
    }

    private static void printRowData(List<CanalEntry.Column> columns) {
        if (columns == null && columns.size() == 0) {
            return;
        }
        for (CanalEntry.Column column : columns) {
            // 真正每一行的内容
            System.out.println(column.getName() + ":" + column.getValue());
            System.out.println(column.getIndex());
        }

    }
}

四、springboot使用canal客户端(亲测该方式并不是很友好。。)

1、引入依赖

<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>

2、编写配置

canal:
  destination: example # canal的集群名字,要与安装canal时设置的名称一致
  server: 192.168.56.10:11111 # canal服务地址

3、监听类

import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;

@CanalTable("tb_item") //监听的表
@Component
public class ItemHandler implements EntryHandler<Testb> {


    @Override
    public void insert(Testb item) {
        System.out.println("insert");
        System.out.println(item);
    }

    @Override
    public void update(Testb before, Testb after) {
        System.out.println("update");
        System.out.println(before);
        System.out.println(after);

    }

    @Override
    public void delete(Testb item) {
        System.out.println("delete");
        System.out.println(item);

    }
}

class Testb{
    private Integer id;
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Testb{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}