Canal入门

Canal是阿里开源的一个增量数据变更收集的工具,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费的一种中间件。
Canal主要包含以下几个组件:
canal-server:运行在后台的服务,用于监听MySQL的binlog日志,解析binlog日志的变更内容,然后将变更信息推送到canal-client。
canal-adapter:一个适配器,用于将canal-server获取的变更信息适配到其他数据源,如Elasticsearch、HBase等。
canal-admin:一个管理工具,用于管理和监控canal-server和canal-adapter的运行状态。

首先,你需要部署Canal Server和Canal Admin。
配置Canal Server,指定数据库和binlog的位置。 一个 server 里可以包含多个 instance

  • canal-instance: canal-server中的一个处理实例,可以处理不同的业务逻辑, 一个最小的订阅 MySQL 的队列.

启动Canal Admin,它会自动连接到Canal Server。
通过Canal Admin可视化界面查看和管理Canal Server的状态。
接下来,配置Canal Adapter,指定要同步的数据库和对应的消息中间件。
启动Canal Adapter,它会消费Canal Server的数据,并将数据同步到指定的消息中间件。

 

下载地址: https://github.com/alibaba/canal/releases

下载 canal.addpter, canal.admin和canal.deployer(canal-server)  ,安装之前,先安装mysql , 这个步骤略。

mysql需要 开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式。

# 设置服务器ID(如果是复制集群,确保每个节点的ID唯一)
server-id=1
# 启用二进制日志
log-bin=mysql-bin
# 选择 ROW 模式
binlog-format=ROW 
#指定对哪个database记录binary log events,这里记录test这个库
#binlog-do-db=test

创建用于主从复制的用户canal,并授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限,如果已有账户可直接 grant。 

CREATE USER 'canal'@'%' identified by 'canal'; 
GRANT SELECT, SUPER, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES; 

安装canal-admin

解压canal.admin文件到目录 /usr/local/canal/admin 中,

mysql中创建数据库 canal_manager  ,具体脚本在 admin/conf/canal_manager.sql 中 

修改配置文件 vim conf/application.yml

server:
  port: 8089
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

spring.datasource:
  address: 192.168.1.100:3306
  database: canal_manager
  username: canal
  password: canal
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  hikari:
    maximum-pool-size: 30
    minimum-idle: 1

canal:
  adminUser: admin
  adminPasswd: admin

启动admin服务

[root@master admin]# bin/startup.sh

在浏览器打开:  http://192.168.1.181:8089/

默认账号密码为:admin:123456, 登录后可以在账号管理中修改密码。

 

安装canal-deployer(server)

解压canal.depllyer 文件到目录 /usr/local/canal/server中,

配置: conf目录中有2个重要的配置文件: 具体可参加: https://github.com/alibaba/canal/wiki/Canal-Admin-ServerGuide

  • canal_local.properties 为注册admin配置
  • canal.properties 为本地配置 

canal_local.properties配置比较少: 引入canal_local.properties,针对canal-server启动所需要的配置,做了最小化配置,只需要关注和admin请求的必要参数即可 

# register ip
canal.register.ip =

# canal-admin的访问地址
canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110

# canal-admin的访问账号和密码,密码与adminPasswd对应,这里是密文
canal.admin.user = admin
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
# 启动后自动注册到admin中
canal.admin.register.auto = true
# 集群名称 ,这里是单点可空着即可
canal.admin.register.cluster =
# 节点名称名称,里是单点可空着即可
canal.admin.register.name =

canal.propertie文件可以先不用修改,先进行实例配置

canal配置分为两部分:

一个是server/conf/canal.propertie配置文件;

一个是server/conf/example/instance.properties配置文件。

前者定义instance的公共参数,后者定义instance级别的配置,每个instance一份。

先看一个示例配置

[root@master server]# vim conf/example/instance.properties  

## mysql serverId , v1.0.26+ will autoGen
## mysql serverId,这里的slaveId不能和myql集群中已有的server_id一样
canal.instance.mysql.slaveId=10

# enable gtid use true/false
canal.instance.gtidon=false
ZZ
# position info
canal.instance.master.address=192.168.1.100:3306
canal.instance.master.journal.name=mysql-bin.000007
canal.instance.master.position=68666
canal.instance.master.timestamp=
canal.instance.master.gtid=

# rds oss binlog
canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=

# table meta tsdb info
canal.instance.tsdb.enable=true
#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
#canal.instance.tsdb.dbUsername=canal
#canal.instance.tsdb.dbPassword=canal
 
# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
#canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==

# # .*\..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\\..*
# 数据解析表的黑名单,多个表用,隔开
canal.instance.filter.black.regex=mysql\\.slave_.*

如上配置信息所示,我们需要的配置是:

1:(mysql serverId)配置slaveId,之一在集群中,各个服务器的id是唯一的。
2:(position info)配置master 服务器的相关信息:服务器地址、binlog日志名称、mysql主库链接时起始的binlog偏移量。
这个数数据可以在mysql中获取:

 

3:(username/password)配置用户主从复制的账号和密码。
4:(table regex and table black regex)配置要监听的表和不需要解析的表。

 

注意:

1、修改成管理方式之后,之前的server 配置、instance配置全动态都保存到Canal admin的数据库里面了。所以之前在磁盘上的的配置文件就不生效了.
2、所以你可能会看到server的报错:Caused by: com.alibaba.otter.canal.common.CanalException: instance : example config is not found。明明已经有example这个instance了(磁盘上有文件夹)。
     你只需在需要在 Canal Admin 手动添加实例example 的 instance 就行了。配置直接copy保存就行了

 

启动canan server. 在canal admin中可以到

添加一个instance, 名称为 example  , 配置需要订阅的数据库信息地址,添加完成后如果:

 

测试新建一个SpringBoot项目 

在pom中添加相关jar包

<canal.version>1.1.5</canal.version>

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>${canal.version}</version>
</dependency>
<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.protocol</artifactId>
    <version>${canal.version}</version>
</dependency>

定义一个类,连接到canal服务。 

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 org.springframework.stereotype.Component;

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

@Component
public class CanalClient {

    private final static int BATCH_SIZE = 1000;

    public void run() {
        // 创建链接
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.1.181", 11111),
                "example", "canal", "canal");
        try {
            //打开连接
            connector.connect();
            //订阅数据库表,全部表
            connector.subscribe(".*\\..*");
            //回滚到未进行ack的地方,下次fetch的时候,可以从最后一个没有ack的地方开始拿
            connector.rollback();
            while (true) {
                // 获取指定数量的数据
                Message message = connector.getWithoutAck(BATCH_SIZE);
                //获取批量ID
                long batchId = message.getId();
                //获取批量的数量
                int size = message.getEntries().size();
                //如果没有数据
                if (batchId == -1 || size == 0) {
                    try {
                        //线程休眠2秒
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    //如果有数据,处理数据
                    printEntry(message.getEntries());
                }
                //进行 batch id 的确认。确认之后,小于等于此 batchId 的 Message 都会被确认。
                connector.ack(batchId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            connector.disconnect();
        }
    }

    /**
     * 打印canal server解析binlog获得的实体类信息
     */
    private static void printEntry(List<CanalEntry.Entry> entrys) {
        for (CanalEntry.Entry entry : entrys) {
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN 
             || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) { //开启/关闭事务的实体类型,跳过 continue; } //RowChange对象,包含了一行数据变化的所有特征 //比如isDdl 是否是ddl变更操作 sql 具体的ddl sql beforeColumns afterColumns 变更前后的数据字段等等 CanalEntry.RowChange rowChage; try { rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); } catch (Exception e) { throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e); } //获取操作类型:insert/update/delete类型 CanalEntry.EventType eventType = rowChage.getEventType(); //打印Header信息 System.out.println(String.format("================》; binlog[%s:%s] , name[%s,%s] , eventType : %s", entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType)); //判断是否是DDL语句 if (rowChage.getIsDdl()) { System.out.println("================》;isDdl: true,sql:" + rowChage.getSql()); } //获取RowChange对象里的每一行数据,打印出来 for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) { //如果是删除语句 if (eventType == CanalEntry.EventType.DELETE) { printColumn(rowData.getBeforeColumnsList()); //如果是新增语句 } else if (eventType == CanalEntry.EventType.INSERT) { printColumn(rowData.getAfterColumnsList()); //如果是更新的语句 } else { //变更前的数据 System.out.println("------->; before"); printColumn(rowData.getBeforeColumnsList()); //变更后的数据 System.out.println("------->; after"); printColumn(rowData.getAfterColumnsList()); } } } } private static void printColumn(List<CanalEntry.Column> columns) { for (CanalEntry.Column column : columns) { System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated()); } } }

新建一个CommandLineRunner,项目启动完成执行

@Component
public class MyCommandLineRunner implements CommandLineRunner {

    @Resource
    private CanalClient canalClient;

    @Override
    public void run(String... args) throws Exception {
        System.out.printf("MyCommandLineRunner...");
        canalClient.run();
    }
}

启动springboot,在MySQL中执行一条update语句

UPDATE t_appuser SET realname='123567' where openid='o2Hsa0a_6hokVUBE6M9b4xmoWLHk' ;

可以看到项目的输入内容

================》; binlog[mysql-bin.000009:28656] , name[wuliu,t_appuser] , eventType : UPDATE
------->; before
openid : o2Hsa0a_6hokVUBE6M9b4xmoWLHk    update=false
nickname : 林文    update=false
realname : 123    update=false
gender : 0    update=false
type : 0    update=false
province :     update=false
city :     update=false
avatarurl : https://thirdwx.qlogo.cn/mmhead/mibQlicQlBOl2ziaOAdLGiclUviaEdUF4hKMlOf3vlmWxZMo/132    update=false
unionid : HO/TLqd+KhJykMRz+pt1fQ==    update=false
appid : wxbe4698a8050d0ebf    update=false
sessionkey : HO/TLqd+KhJykMRz+pt1fQ==    update=false
loginid : df2c22d706f842aaa1bb46a23730e6db    update=false
time : 1610789370111    update=false
etime : 1610789370111    update=false
mid : 0    update=false
------->; after
openid : o2Hsa0a_6hokVUBE6M9b4xmoWLHk    update=false
nickname : 林文    update=false
realname : 123567    update=true
gender : 0    update=false
type : 0    update=false
province :     update=false
city :     update=false
avatarurl : https://thirdwx.qlogo.cn/mmhead/mibQlicQlBOl2ziaOAdLGiclUviaEdUF4hKMlOf3vlmWxZMo/132    update=false
unionid : HO/TLqd+KhJykMRz+pt1fQ==    update=false
appid : wxbe4698a8050d0ebf    update=false
sessionkey : HO/TLqd+KhJykMRz+pt1fQ==    update=false
loginid : df2c22d706f842aaa1bb46a23730e6db    update=false
time : 1610789370111    update=false
etime : 1610789370111    update=false
mid : 0    update=false

可以看到数据变化前后的记录

参考:https://mp.weixin.qq.com/s/0nmaGqPJar5ssnYN223r1g

 

posted @ 2022-01-07 10:58  南极山  阅读(153)  评论(0)    收藏  举报