Springboot集成canal 1.1.5简单实践

一. Canal简介

Springboot集成canal 1.1.5简单实践_第1张图片
主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
基于日志增量订阅和消费的业务包括

1.数据库镜像
2.数据库实时备份
3.索引构建和实时维护(拆分异构索引、倒排索引等)
4.业务 cache 刷新
5.带业务逻辑的增量数据处理

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

canal 工作原理

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

二.服务端环境准备(Liunx)

1.开启Mysql Binlog 写入功能,配置 binlog-format 为 ROW 模式

修改mysql my.cnf文件
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
(因为cancal是作为从库监听主库)

查看Binlog开启状态,登陆mysql后:  ON为开启
show variables like '%log_bin%';

Springboot集成canal 1.1.5简单实践_第2张图片

2. 下载 canal 图中deployer,放到liunx环境中解压

adapter: canal自带的适配器,通过配置修改就可以实现向MQ、DB、ES中同步数据
admin: canal的WEB面板,为canal提供整体配置管理、节点运维等面向运维的功能
deployer: canal服务端,也就是canal监听数据源的服务
example:这是客户端代码的demo例子

Springboot集成canal 1.1.5简单实践_第3张图片

3.修改配置文件

修改解压后conf/example 下的 instance.properties 文件:
## mysql serverId 不要和监听的mysql server_id一样
canal.instance.mysql.slaveId = 1234

## 需要改成自己的数据库地址
canal.instance.master.address = 127.0.0.1:3306 

# 这三个配置和同步信息位置有关 增量同步不需要管
#canal.instance.master.journal.name = 
#canal.instance.master.position = 
#canal.instance.master.timestamp = 

#username/password,需要改成自己的数据库信息
canal.instance.dbUsername = canal  
canal.instance.dbPassword = canal
canal.instance.defaultDatabaseName =
canal.instance.connectionCharset = UTF-8

#这个是监听的库表Perl正则表达式 (可以选择性监听需要的库表)
#也可以不配,在代码中设置(两边都配了,则以代码的配置为准)
canal.instance.filter.regex = .\*\\\\..\*
常见例子:
1.  所有表:.*   or  .*\\..*
2.  canal schema下所有表: canal\\..*
3.  canal下的以canal打头的表:canal\\.canal.*
4.  canal schema下的一张表:canal.test1
5.  多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)

4.启动 运行bin下的startup.sh 文件

Springboot集成canal 1.1.5简单实践_第4张图片

三.自定义客户端实现

1.导入包

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

2.代码集成

// 主逻辑代码
    public void canalClient(){
    	// 地址、端口、example默认就好(就是instance.properties文件所在文件夹名称)
        CanalConnector connector = CanalConnectors.newSingleConnector(new
                InetSocketAddress("192.168.1.105",
                11111), "example", "", "");
        int batchSize = 1000;
        try {
            connector.connect();
            connector.subscribe("test\\..*"); // 监听的库表Perl正则表达式(我这里是test库下所有表)
            connector.rollback();
            try {
                while (true) {
                    //尝试从master那边拉去数据batchSize条记录,有多少取多少
                    Message message = connector.getWithoutAck(batchSize);
                    long batchId = message.getId();
                    int size = message.getEntries().size();
                    if (batchId == -1 || size == 0) {
                        Thread.sleep(1000); //没有数据则等待1s
                    } else {
                        dataHandle(message.getEntries()); //数据处理
                    }
                    connector.ack(batchId);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (InvalidProtocolBufferException e) {
                connector.connect();
                e.printStackTrace();
            }
        } finally {
            connector.disconnect();
        }
    }

    /**
     * 数据处理
     *
     * @param entrys
     */
    private void dataHandle(List<Entry> entrys) throws
            InvalidProtocolBufferException {
        for (Entry entry : entrys) {
            if (EntryType.ROWDATA == entry.getEntryType()) {
                RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
                EventType eventType = rowChange.getEventType();
                if (eventType == EventType.DELETE) {
                    saveDeleteSql(entry);
                } else if (eventType == EventType.UPDATE) {
                    saveUpdateSql(entry);
                } else if (eventType == EventType.INSERT) {
                    saveInsertSql(entry);
                }
            }
        }
    }
    /**
     * 保存更新语句
     *
     * @param entry
     */
    private void saveUpdateSql(Entry entry) {
        try {
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) {
                List<Column> newColumnList = rowData.getAfterColumnsList();
                Map<String, String> data = new HashMap<>();
                newColumnList.stream().forEach(item -> {
                    if(StringUtils.isNotEmpty(item.getValue())){
                        data.put(lineToHump(item.getName()), item.getValue());
                    }
                });
                List<Column> oldColumnList = rowData.getBeforeColumnsList();
                CanalVO update = CanalVO.ok(entry.getHeader().getTableName(), "UPDATE", data, oldColumnList.get(0).getValue());
                System.out.println("更新返回 : " + JSON.toJSONString(update));
            }
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }
    }
    /**
     * 保存删除语句
     *
     * @param entry
     */
    private void saveDeleteSql(Entry entry) {
        try {
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) {
                List<Column> oldColumnList = rowData.getBeforeColumnsList();
                CanalVO delete = CanalVO.ok(entry.getHeader().getTableName(), "DELETE", null, oldColumnList.get(0).getValue());
                System.out.println("删除返回 : " + JSON.toJSONString(delete));
            }
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }
    }
    /**
     * 保存插入语句
     *
     * @param entry
     */
    private void saveInsertSql(Entry entry) {
        try {
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            List<RowData> rowDatasList = rowChange.getRowDatasList();
            for (RowData rowData : rowDatasList) {
                List<Column> columnList = rowData.getAfterColumnsList();
                Map<String, String> data = new HashMap<>();
                columnList.stream().forEach(item -> {
                    if(StringUtils.isNotEmpty(item.getValue())){
                        data.put(lineToHump(item.getName()), item.getValue());
                    }
                });
                CanalVO insert = CanalVO.ok(entry.getHeader().getTableName(), "INSERT", data, null);
                System.out.println("插入返回 : " + JSON.toJSONString(insert));
            }
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }
    }

    /**
     * 下划线转驼峰
     * 
     * @param str
     */
    private static Pattern linePattern = Pattern.compile("_(\\w)");
    public static String lineToHump(String str) {
        str = str.toLowerCase();
        Matcher matcher = linePattern.matcher(str);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
        }
        matcher.appendTail(sb);
        return sb.toString();
    }
// CanalVO 如下:
public class CanalVO {
    private String tableName; // 表名
    private String type; // 类型(更新、删除、插入)
    private Map<String,String> data; // 数据JSON  自己转对应表格实体类
    private String id;  // 更新或删除都是根据ID来

    public static CanalVO ok(String tableName,String type,Map<String,String> data,String id){
        CanalVO canalVO=new CanalVO();
        canalVO.setId(id);
        canalVO.setTableName(tableName);
        canalVO.setType(type);
        canalVO.setData(data);
        return canalVO;
    }

    public String getTableName() {
        return tableName;
    }

    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public Map<String, String> getData() {
        return data;
    }

    public void setData(Map<String, String> data) {
        this.data = data;
    }

    public String getId() {
        return id;
    }

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

3.启动服务端后,启动代码,然后修改库内数据(这是默认情况下增量同步)

在这里插入图片描述

4.全量同步实现

1.修改instance.properties配置

# 配置log文件与log位点 或者 配置时间戳

#bin-log文件 与log位点构成要开始同步的位置
canal.instance.master.journal.name = mysql-bin.000009 //从000009 bin-log文件0位点开始同步
#bin-log位点
canal.instance.master.position = 0
#指定一个时间戳,canal会自动遍历mysql binlog,找到对应时间戳的binlog位点后,进行启动
canal.instance.master.timestamp = 

注意
1.修改同步位置时,如果该目录下已生成meta.dat文件,要把这个删掉,因为这个文件记录了已经同步的位置
2.bin-log文件一定要是存在的,这个可以去mysql bin-log文件生成地方去确认,不然example的log会报找不到bin-log文件的错误

2.重启服务端和客户端

就能看见之前的记录了
Springboot集成canal 1.1.5简单实践_第5张图片

拓展

1. 两个相同配置客户端服务同时连接了同一个服务端会怎样?
只有一个服务能收到消息

2.一个服务端有没有办法同时监听不同环境的数据库?
可以,上述的exmaple文件夹就等于是一个数据库,如果要监听多个可以把这个文件夹复制一份,同时修改instance.properties文件配置,注意要删除meta.dat,同时要修改conf/canal.properties,找到destinations配置处

修改canal.destinations   多个用,隔开
列如:  canal.destinations = example1,example2

总结

上述只是一个非常简单的demo列子,实际中我们可以通过工厂优雅的处理不同表的各种消息,或者直接交给spring容器管理,我们只需要关注具体的业务数据处理即可,更多的还可以去看canal官方使用指南

你可能感兴趣的:(springboot,mysql,spring,boot,java,mysql,中间件)