Canal 学习笔记

Canal 介绍

canal 官网 https://github.com/alibaba/canal

canal [kə’næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
在这里插入图片描述

Canal 应用场景

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

常见场景1:更新缓存
在这里插入图片描述

常见场景2:抓取业务数据新增变化表,用于制作拉链表

常见场景3:抓取业务表的新增变化数据,用于制作实时统计

Canal 工作原理

MySQL主备复制原理
在这里插入图片描述

MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

canal 工作原理

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

MySQL的binlog

(1)什么是binlog
MySQL的二进制日志可以说MySQL最重要的日志了,它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。
二进制日志包括两类文件:二进制日志索引文件(文件名后缀为.index)用于记录所有的二进制文件,二进制日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML(除了数据查询语句)语句事件。
一般来说开启二进制日志大概会有1%的性能损耗。二进制有两个最重要的使用场景:
其一:MySQL Replication在Master端开启binlog,Master把它的二进制日志传递给slaves来达到master-slave数据一致的目的。
其二:自然就是数据恢复了,通过使用mysqlbinlog工具来使恢复数据。

(2)binlog的开启
找到MySQL配置文件的位置
Linux: /etc/my.cnf 如果/etc目录下没有,可以通过locate my.cnf查找位置
Windows: \my.ini
在mysql的配置文件下,修改配置:在[mysqld] 区块,设置/添加 log-bin=mysql-bin
这个表示binlog日志的前缀是mysql-bin,以后生成的日志文件就是 mysql-bin.123456 的文件后面的数字按顺序生成,每次mysql重启或者到达单个文件大小的阈值时,新生一个文件,按顺序编号。

(3)binlog的分类设置
mysql binlog的格式有三种,分别是statement,mixed,row
在配置文件中可以选择配置 binlog_format= statement|mixed|row

三种格式的区别:
1.statement: 语句级,binlog会记录每次一执行写操作的语句。相对row模式节省空间,但是可能产生不一致性,比如update tt set create_date=now() 如果用binlog日志进行恢复,由于执行时间不同可能产生的数据就不同。
优点: 节省空间
缺点: 有可能造成数据不一致
2.row: 行级, binlog会记录每次操作后每行记录的变化。
优点:保持数据的绝对一致性。因为不管sql是什么,引用了什么函数,他只记录执行后的效果
缺点:占用较大空间
3.mixed:statement的升级版,一定程度上解决了,因为一些情况而造成的statement模式不一致问题。默认还是statement,在某些情况下譬如:
当函数中包含 UUID() 时;包含 AUTO_INCREMENT 字段的表被更新时;执行 INSERT DELAYED 语句时;用 UDF 时;会按照 ROW的方式进行处理
优点:节省空间,同时兼顾了一定的一致性。
缺点:还有些极个别情况依旧会造成不一致,另外statement和mixed对于需要对binlog的监控的情况都不方便。
综合上面对比,Maxwell想做监控分析,选择row格式比较合适

Canal 安装部署

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

MySQL准备

1.创建canal用户,密码canal,并赋予它操作其他数据库的权限

GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal';

2.修改/etc/my.cnf文件

[linux@node1 example]$  sudo vim /etc/my.cnf 
server-id= 1
log-bin=mysql-bin
binlog_format=row
binlog-do-db=bigdata

注意:binlog-do-db根据自己的情况进行修改,指定具体要同步的数据库

3.重启MySQL使配置生效

sudo systemctl restart mysqld

安装和修改配置

1.上传安装包到/opt/software上

canal.deployer-1.1.4.tar.gz

2.解压到opt/module/canal里
注意:canal解压后是散的,我们在指定解压目录的时候需要将canal指定上

[linux@node1 module]$ mkdir canal
[linux@node1 software]$ tar -zxvf canal.deployer-1.1.2.tar.gz -C /opt/module/canal/

3.修改conf/canal.properties的配置

[linux@node1 conf]$ vim canal.properties 
这个文件是canal的基本通用配置

#canal端口号默认就是11111
canal.port = 11111
#修改canal的输出model,默认tcp,改为输出到kafka
canal.serverMode = kafka
#修改Kafka集群的地址
canal.mq.servers = node1:9092,node2:9092,node3:9092

如果创建多个实例
通过前面canal架构,我们可以知道,一个canal服务中可以有多个instance,conf/下的每一个example即是一个实例,每个实例下面都有独立的配置文件。默认只有一个实例example,如果需要多个实例处理不同的MySQL数据的话,直接拷贝出多个example,并对其重新命名,命名和配置文件中指定的名称一致,然后修改canal.properties中的canal.destinations=实例1,实例2,实例3。

4.修改instance.properties
我们这里只读取一个MySQL数据,所以只有一个实例,这个实例的配置文件在conf/example目录下

[linux@node1 example]$ vim instance.properties
#配置MySQL服务器地址
canal.instance.master.address=node1:3306
#配置连接MySQL的用户名和密码,默认就是我们前面授权的canal
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
#修改输出到Kafka的主题以及分区数
canal.mq.topic=canal_db

注意:默认还是输出到指定Kafka主题的一个kafka分区,因为多个分区并行可能会打乱binlog的顺序
如果要提高并行度,首先设置kafka的分区数>1,然后设置canal.mq.partitionHash属性

测试

1)启动canal

[linux@node1 canal]$ cd /opt/module/canal/
[linux@node1 canal]$ bin/startup.sh
[linux@node1 canal]$ jps
4454 Jps
4425 CanalLauncher

看到CanalLauncher你表示启动成功

2)启动Kafka消费客户端,观察结果

[linux@node1 bin]$ kafka-console-consumer.sh --bootstrap-server node1:9092 --topic canal_db --from-beginning

3)创建数据库bigdata

4)创建表

CREATE TABLE IF NOT EXISTS `runoob_tbl`(
   `runoob_id` INT UNSIGNED AUTO_INCREMENT,
   `runoob_title` VARCHAR(100) NOT NULL,
   `runoob_author` VARCHAR(40) NOT NULL,
   `submission_date` DATE,
   PRIMARY KEY ( `runoob_id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

5)插入数据

INSERT INTO runoob_tbl(runoob_title, runoob_author, submission_date) VALUES ("学习 MySQL", "菜鸟教程", NOW());

INSERT INTO runoob_tbl(runoob_title, runoob_author, submission_date) VALUES ("学习 linux", "菜鸟教程", NOW());

在这里插入图片描述
成功

canal高可用(了解)

这种zookeeper为观察者监控的模式,只能实现高可用,而不是负载均衡,即同一时间点只有一个canal-server节点能够监控某个数据源,只要这个节点能够正常工作,那么其他监控这个数据源的canal-server只能做stand-by,直到工作节点停掉,其他canal-server节点才能抢占。因为有一个stand-by也要占用资源,同时canal传输数据宕机的情况也比较少,所以好多企业是不配置canal的高可用的。

1.在node1上修改canal.properties

[linux@node1 conf]$ vim canal.properties 
#配置zookeeper
canal.zkServers = node1:2181,node2:2181,node3:2181

#避免发送重复数据(否则在切换active的时候会重复发送数据)
#canal.instance.global.spring.xml = classpath:spring/file-instance.xml              (注释掉)
canal.instance.global.spring.xml = classpath:spring/default-instance.xml          (取消注释)

2.把canal目录分发给其他虚拟机

[linux@node1 module]$ xsync canal/

3.测试
先在node1启动canal

[linux@node1 canal]$ bin/startup.sh
[atguigu@hadoop202 canal]$ bin/startup.sh

再在node2上启动canal

[linux@node2 canal]$ bin/startup.sh 
found canal.pid , Please run stop.sh first ,then startup.sh
[linux@node2 canal]$ bin/stop.sh 
node2: stopping canal 6320 ... 
bin/stop.sh:52:kill: (6320) - 没有那个进程
Oook! cost:0

#再次启动就ok了
[linux@node2 canal]$ bin/startup.sh 

启动kafka消费客户端

[[linux@node1 ~]$ kafka-console-consumer.sh --bootstrap-server node1:9092 --topic canal_db

在数据库bigdata随便一张表中修改一条数据,查看效果

INSERT INTO runoob_tbl(runoob_title, runoob_author, submission_date) VALUES ("学习 canal", "菜鸟", NOW());

挂掉node1再次修改数据,查看效果
在这里插入图片描述
成功

Canal自定义客户端

数据不直接发完kafka,通过自己自定义客户端的方式写入kafka中
在这里插入图片描述
导入相关依赖

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

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>2.4.1</version>
        </dependency>
</dependencies>

重新解压一份canal测试

[linux@node1 module]$ mkdir canal-demo
[linux@node1 software]$ tar -zxvf canal.deployer-1.1.4.tar.gz -C /opt/module/canal-demo/

canal.properties不做修改,instance.properties是针对要追踪的MySQL的实例配置,修改instance.properties文件

[linux@node1 example]$ vim instance.properties 
#不能与mysql的server.id重复,这边不做修改
# canal.instance.mysql.slaveId=0

canal.instance.master.address=node1:3306

启动canal,测试

[linux@node1 canal-demo]$ bin/startup.sh 

代码编写

通用监视类 –CanalClient

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 com.google.protobuf.InvalidProtocolBufferException;

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

public class CanalClient {
    public static void main(String[] args) throws InterruptedException, InvalidProtocolBufferException {
        //获取一个连接器
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("node1", 11111), "example", "", "");
        //创建一个连接
        canalConnector.connect();
        //订阅数据,过滤数据
        canalConnector.subscribe("bigdata.*");
        //流式应用,源源不断拉取数据
        while (true){
            //一次拉取100条数据
            Message message = canalConnector.get(100);
            //如何判断当前有没有拉取到数据 根据message的id判断,如果是-1,没有拉取到数据
            if (message.getId()==-1){
                System.out.println("停止十秒钟再拉取数据");
                Thread.sleep(10000);
                continue;
            }

            List<CanalEntry.Entry> entries = message.getEntries();

            for (CanalEntry.Entry entry:entries){
                //只需要ROWDATA类型数据
                if(entry.getEntryType().equals(CanalEntry.EntryType.ROWDATA)){
                    //获取表名
                    String tableName = entry.getHeader().getTableName();
                    //获取影响的数据
                    ByteString storeValue = entry.getStoreValue();
                    //获取反序列化的数据
                    CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
                    //获取可读数据
                    List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
                    //获取数据操作类型
                    CanalEntry.EventType eventType = rowChange.getEventType();

                    //可根据不同的表,不同的操作类型,处理数据
                    CanalHandler.handle(tableName,rowDatasList,eventType);
                }
            }
        }
    }
}

业务处理类 –CanalHandler

import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.protocol.CanalEntry;

import java.util.List;

public class CanalHandler {

    public static void handle(String tableName, List<CanalEntry.RowData> rowDatasList, CanalEntry.EventType eventType){
        if("runoob_tbl".equals(tableName)){
            for (CanalEntry.RowData rowData:rowDatasList) {
                JSONObject jsonObject = new JSONObject();
                //获取更新之后的数据
                List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
                for (CanalEntry.Column column:afterColumnsList){
                    jsonObject.put(column.getName(),column.getValue());
                    KafkaProducerUtil.sendMessage("canal_db",jsonObject.toString());
                }
            }
        }
    }
}

Kafka发送工具类 –KafkaProducerUtil

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;

public class KafkaProducerUtil {

    public static KafkaProducer<String, String> kafkaProducer = null;

    public static KafkaProducer getKafkaProducer() {
        Properties properties = new Properties();
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        return new KafkaProducer<String, String>(properties);
    }

    public static void sendMessage(String topic, String value) {
        if (kafkaProducer == null) {
            kafkaProducer = getKafkaProducer();
        }
        kafkaProducer.send(new ProducerRecord<String, String>(topic, value));
    }
}

向bigdata数据库中插入一条数据

INSERT INTO runoob_tbl(runoob_title, runoob_author, submission_date) VALUES ("学习 linux", "菜鸟教程", NOW());

kafka客户端测试

[linux@node1 ~]$ kafka-console-consumer.sh --bootstrap-server node1:9092 --topic canal_db

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值