Bootstrap

canal订阅mysql的binlog日志+springboot详解

简介:

本次想实现的功能:订阅mysql的binlog日志,通过canal中间件将对应日志通过springboot整合的方式打印出来,后面可以根据具体需求实现后续业务功能。之前也看网上有很多的例子,实践过程中发现有些还是不可用的居多,所以综合网上楼主的实例,自己也记录一下搭建成功的详细过程。

详细过程:

1:搭建mysql环境,开启binlog日志

这里是采用的docker的形式搭建。
首先通过docker拉取mysql的镜像

docker pull mysql:5.7

创建并运行容器,指定对应挂载数据目录,默认root账号密码等

docker run -d --name mysql -v /data/mysql:/var/lib/mysql --restart=always -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql:5.7

查看我们mysql容器是否启动成功

docker ps

启动成功后接下来我们要开启我们的binglog日志,

#进入到我们mysql容器内部
docker exec -it mysql sh
修改对应的配置文件
vim /etc/mysql/mysql.conf.d/mysqld.cnf
加入以下内容
#binlong setting
log-bin=/var/lib/mysql/mysql-bin
server-id=123454

docker里面使用vim命令会报错说找不到命令,执行以下操作
apt-get update
apt-get install vim

在这里插入图片描述
这一个参数的作用是mysql会根据这个配置自动设置log_bin为on状态,自动设置log_bin_index文件为你指定的文件名后跟.index
第二个参数 ,用的如果是5.7及以上版本的话,重启mysql服务会报错,这个时候我们必须还要指定这样一个参数,随机指定一个不能和其他集群中机器重名的字符串,如果只有一台机器,那就可以随便指定
为了避免后面插入数据为中文字符串出现乱码情况,还可以追加以下内容设置编码格式

[mysqld]
character_set_server=utf8
[client]
default-character_set=utf8
[mysql]
default-character_set=utf8

改完后保存后重新启动mysql容器

docker restart mysql

启动完成后,我们连接mysql,输入以下命令看binlog是否启动

#进入mysql容器
docker exec -it mysql sh
#登录mysql
mysql -u root -p
输入密码
连接mysql后输入以下命令
show variables like '%log_bin%'

在这里插入图片描述
可以看到我们binlog变量为on即为开启状态,进入下一个环节

2:搭建canal环境,并连接我们之前的mysql容器

对于canal这里简单介绍以下,canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。了解详情的话可以去看canal的官网,这里不多介绍。
docker先拉取canal对应的镜像

docker pull canal/canal-server:v1.1.4

先启动canal容器

docker run -p 11111:11111 --name canal -d canal/canal-server:v1.1.4

初次启动Canal镜像后,将instance.properties文件复制到宿主机并通过挂载进行加载

docker cp \
canal:/home/admin/canal-server/conf/example/instance.properties \
/root/data

这里/home/admin/canal-server/conf/example/instance.properties为canal对应的配置文件的目录地址
/root/data为楼主linux上指定的文件夹目录,这个根据你自己的环境指定一个目录

#修改instance.properties
vim /root/data/instance.properties 

在这里插入图片描述
这里的172.18.0.2对应mysql容器的docker对外暴露的ip地址,可以使用以下命令查看


docker inspect mysql| findstr IPAddress

把之前的canal容器移除,重新创建容器启动

docker run -p 11111:11111 \
--name canal \
-v /root/data/instance.properties:/home/admin/canal-server/conf/example/instance.properties \
-d canal/canal-server:v1.1.4

启动成功后记得我们对应的服务器要开启11111端口,不然后面我们项目是访问不了的

3:搭建springboot环境

创建我们的maven项目
引入依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.2</version>
        </dependency>
    </dependencies>

在resources建一个application.yml配置文件

canal:
  # instance 实例所在ip
  host: 服务器公网的ip地址
  # tcp通信端口对应canal的端口
  port: 11111
  # 这里用户名密码我们都没指定所以直接为空
  username:
  # 密码
  password:
  #实例名称,这里对应之前的instance.properties配置里的不用改,默认为  example
  instance: example

新建springboot启动类StartApplication类

package cn.zwh;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StartApplication {
    public static void main(String[] args) {
        SpringApplication.run(StartApplication.class,args);
    }
}

新建一个canalListener配置类里面写具体的逻辑

package cn.zwh.listener;

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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

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


@Component
public class CanalListener implements ApplicationRunner {

    private static final Logger logger = LogManager.getLogger();

    @Value("${canal.host}")
    private String host;
    @Value("${canal.port}")
    private int port;
    @Value("${canal.username}")
    private String username;
    @Value("${canal.password}")
    private String password;
    @Value("${canal.instance}")
    private String instance;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        CanalConnector conn = getConn();
        conn.connect();
        //订阅实例中所有的数据库和表
        conn.subscribe(".*\\..*");
        // 回滚到未进行ack的地方
        conn.rollback();


        try {
            while (true)
            {
                //获取指定数量的数据,如果不做确认标记,下一次取还会取到这些信息
                Message message = conn.getWithoutAck(100);
                //获取消息id
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId != -1 && size > 0) {
                    logger.info("msgId -> " + batchId);
                    //转换sql
                    analysis(message.getEntries());
                    //消息确认
                    conn.ack(batchId);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭连接
            conn.disconnect();
        }


    }


    private static void analysis(List<CanalEntry.Entry> entries) {
        for (CanalEntry.Entry entry : entries) {
            if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA) {
                continue;
            }
            try {
                CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                    System.out.println(rowChange.getEventType());
                    List<CanalEntry.Column> columnList = null;
                    StringBuffer sql = null;
                    switch (rowChange.getEventType()) {
                        //如果希望监听多种事件,可以手动增加case
                        case INSERT:
                            columnList = rowData.getAfterColumnsList();
                            sql = new StringBuffer("insert into " +
                                    entry.getHeader().getTableName() + " (");
                            for (int i = 0; i < columnList.size(); i++) {
                                sql.append(columnList.get(i).getName());
                                if (i != columnList.size() - 1) {
                                    sql.append(",");
                                }
                            }
                            sql.append(") VALUES (");
                            for (int i = 0; i < columnList.size(); i++) {
                                sql.append("'" + columnList.get(i).getValue() + "'");
                                if (i != columnList.size() - 1) {
                                    sql.append(",");
                                }
                            }
                            sql.append(")");
                            logger.info("插入sql:" + sql);
                            break;
                        case UPDATE:
                            columnList = rowData.getAfterColumnsList();
                            sql = new StringBuffer("update " +
                                    entry.getHeader().getTableName() + " set ");
                            String mainKey = "";
                            boolean firstFlag=true;
                            for (int i = 0; i < columnList.size(); i++) {
                                if (columnList.get(i).getUpdated()) {
                                    if (!firstFlag) {
                                        sql.append(",");
                                    }
                                    firstFlag=false;
                                    sql.append(" ")
                                            .append(columnList.get(i).getName())
                                            .append(" = '").append(columnList.get(i).getValue())
                                            .append("'");
                                } else {
                                    if (columnList.get(i).getIsKey()) {
                                        mainKey = columnList.get(i).getName() + "=" + columnList.get(i).getValue();
                                    }
                                }

                            }
                            sql.append(" where " + mainKey);
                            logger.info("更新sql:" + sql);
                            break;
                        case DELETE:
                            columnList = rowData.getBeforeColumnsList();
                            sql = new StringBuffer("delete from " +
                                    entry.getHeader().getTableName() + " where ");
                            for (CanalEntry.Column column : columnList) {
                                if (column.getIsKey()) {
                                    sql.append(column.getName()).append("=").append(column.getValue());
                                    break;
                                }
                            }
                            logger.info("删除sql:" + sql);
                            break;
                        default:
                    }
                }
            } catch (InvalidProtocolBufferException e) {
                e.printStackTrace();
            }


        }


    }


    /**
     * 获取连接
     */
    public CanalConnector getConn() {
        return CanalConnectors.newSingleConnector(new InetSocketAddress(host, port), instance, username, password);
    }

}

启动我们的springboot项目,编写测试sql看控制台打印内容

4:测试打印sql

使用我们的navicate工具连接到我们的mysql或者你也可以直接使用命令行建立我们的测试数据库,测试表
这里我建了一个测试数据库test,里面建了一张测试user表
在这里插入图片描述

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `remark` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

写sql测试打印效果

insert into user(`name`,`remark`) values ('test','testRemark');
update user set name='testaa' where id=2;
delete from user where id=2;

查看我们idea控制台
在这里插入图片描述
可以看到通过canal订阅我们mysql的binlog,我们的日志转换的sql正确打印出来了,至此我们的整个搭建过程就完成了。后续如果涉及到具体业务处理时,可以进行扩展,代码调整,这里是测试demo所以写的比较简单。正式项目时一般会把我们的sql写入的中间件例如rabbitmq中后续再做相关的增量同步操作等。

总结

本次搭建的过程还算比较顺利,主要时间花在springboot整合上,之前也看过网上很多的参考例子,但是经过调试发现有些还是存在问题的,所以此次搭建成功后记录一下过程,后面如果碰到到具体业务需求就可以作为参考了,感兴趣的小伙伴可以一起交流学习呀。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;