canal 使用

使用场景

场景1: 更新缓存
在这里插入图片描述
场景2:抓取业务数据表的新增和变化数据,用于制作拉链表
场景3:抓取业务数据表的新增和变化数据,用于实时统计

工作原理

把自己伪装成slave 假装从master 复制数据

在这里插入图片描述
复制过程分为三步
1、master 主库 DDL DML 除了查询语句写进二进制文件(binary log)中。
2、slave 从库向master 发送dump协议,将master 的 binary log events 拷贝到自己的中继日志(relay log) 中。
3、slave 读取并重做 中继日志中的事件,将改变的数据同步到自己的数据库。

mysql的binlog

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

二进制日志包括两类文件:二进制日志索引文件(文件名后缀为.index)用于记录所有的二进制文件,二进制日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML(除了数据查询语句)语句事件

binlog的开启

编辑my.cnf文件 在[mysqld]区块添加

log-bin=mysql-bin

binlog的分类设置
mysql binlog的格式,那就是有三种,分别是STATEMENT,MIXED,ROW。
在配置文件中可以选择配置

binlog_format=row

区别:
1) statement
语句级,binlog会记录每次一执行写操作的语句。
相对row模式节省空间,但是可能产生不一致性,比如
update tt set create_date=now()
如果用binlog日志进行恢复,由于执行时间不同可能产生的数据就不同。
优点: 节省空间
缺点: 有可能造成数据不一致。
2) row
行级, binlog会记录每次操作后每行记录的变化。
优点:保持数据的绝对一致性。因为不管sql是什么,引用了什么函数,他只记录执行后的效果。
缺点:占用较大空间。

3) mixed
statement的升级版,一定程度上解决了,因为一些情况而造成的statement模式不一致问题
在某些情况下譬如:
当函数中包含 UUID() 时;
包含 AUTO_INCREMENT 字段的表被更新时;
执行 INSERT DELAYED 语句时;
用 UDF 时;
会按照 ROW的方式进行处理
优点:节省空间,同时兼顾了一定的一致性。
缺点:还有些极个别情况依旧会造成不一致,另外statement和mixed对于需要对binlog的监控的情况都不方便。

mysql准备

在mysql 中执行
创建用户赋予权限

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

修改my.cnf 文件

server-id= 1
log-bin=mysql-bin
binlog_format=row
binlog-do-db=你要监控的数据库名
如果改了mysql 的配置 一定要重启,配置才会生效

重启之后bin-log 文件会生成一个新的,我们验证一下是否开启成功

这是我新生成的
在这里插入图片描述
可以执行一些DDL 看文件大小是否变化

canal 安装

https://github.com/alibaba/canal/releases
在这里插入图片描述
把canal.deployer-1.1.2.tar.gz拷贝到linux

修改canal的配置

下面是canal 的配置结构
一个主服务
多个实例服务
一个实例服务对应一个mysql 服务器

也就是说一个canal 可以监控多台mysql服务器
在这里插入图片描述

mkdir canal
tar -zxvf canal.deployer-1.1.2.tar.gz -C canal
cd canal
vim conf/canal.properties

这个文件是canal的基本通用配置,主要关心一下端口号,不改的话默认就是11111
在这里插入图片描述

这里就是定义起多少个实例 多个的话用逗号分隔
canal.destinations = example,example1,example2,example3

这是目录结构,实例的实例的名称和文件夹的名称可以自定义,只要两个能对应上就行
在这里插入图片描述

vim conf/example/instance.properties

instance.properties是针对要追踪的mysql的实例配置
在这里插入图片描述

需要配置的项
canal.instance.mysql.slaveId=2
#mysql 的ip
canal.instance.master.address=localhost:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
#实例的名称
canal.mq.topic=example

启动canal

./bin/startup.sh

在这里插入图片描述

拿canal监控的数据

把canal 监控到的数据写进kafka
需要canal-client kafka-client 的依赖
maven 工程
版本不同自己适配

<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.1.0-cdh6.2.1</version>
        </dependency>

    </dependencies>

通用监视类 CanalClient
在这里插入图片描述
在这里插入图片描述

mysql 数据表准备

create database testcanal;
create table student(id int(11),name varchar(255));
create table teacher(id int(11),name varchar(255));

java代码
在这里插入图片描述
CanalClient

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

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



public class CanalClient {

    
    public static void watch(String hostname, int port, String destination, String tables) {

        //创建连接器
        //这里的用户名密码是canal server 的用户名密码 可以才server的配置文件中配置 用作权限管理
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress(hostname, port), destination,"","" );
        //实时监控
        while (true) {
            //建立连接
            canalConnector.connect();

            //订阅表
            canalConnector.subscribe(tables);

            //抓取canal监控到的消息  一次多少个entry
            //canal 相当于一个队列 mysql 往里放,canal client 往外拿
            //有1000 每次拿100
            //有10 拿10
            Message message = canalConnector.get(100);

            int size = message.getEntries().size();

            if (size == 0) {
                //没有数据休息5S
                System.out.println("没有数据!!休息5秒");

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            } else {
                //有数据
                for (CanalEntry.Entry entry : message.getEntries()) {
                    //每一个entry 对应一条sql 产生变化的行 如果一条sql造成50行的数据变化 这50行数据会放在一个entry里面
                    //mysql是有事务的 每条sql 语句之前都会有事务开启 语句之后有事务关闭,但是这些都会记录在bin-log 里面,我们这里要过滤一下
                    //如果数据是row 类型的  前面我们有设置
                    if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {

                        CanalEntry.RowChange rowChange = null;
                        try {
                            //反序列化storeValue
                            rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                        } catch (InvalidProtocolBufferException e) {
                            e.printStackTrace();
                        }

                        String tableName = entry.getHeader().getTableName();// 表名

                        CanalEntry.EventType eventType = rowChange.getEventType();//insert update delete drop alter ?

                        List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();//行集  数据

                        CanalHandler canalHandler = new CanalHandler(eventType, tableName, rowDatasList);
                        canalHandler.handle();

                    }
                }
            }
        }

    }

    public static void main(String[] args) {
        watch("localhost",11111,"example","testcanal.*");
    }


}

CanalHandler

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

import java.util.List;


public class CanalHandler {

    CanalEntry.EventType eventType;

    String tableName;

    List<CanalEntry.RowData> rowDataList;

    public CanalHandler(CanalEntry.EventType eventType, String tableName, List<CanalEntry.RowData> rowDataList) {
        this.eventType = eventType;
        this.tableName = tableName;
        this.rowDataList = rowDataList;
    }

    public void handle(){
        //如果是student 表,并且是插入操作
        if("student".equals(tableName)&& CanalEntry.EventType.INSERT==eventType){
            rowDateList2Kafka( "canal_student_insert");
        }else if ("teacher".equals(tableName)&& (CanalEntry.EventType.INSERT==eventType||CanalEntry.EventType.UPDATE==eventType)) {
            rowDateList2Kafka( "canal_teacher_insert_update");
        }

    }


    private void  rowDateList2Kafka(String kafkaTopic){
        for (CanalEntry.RowData rowData : rowDataList) {
            List<CanalEntry.Column> columnsList = rowData.getAfterColumnsList();
            JSONObject jsonObject = new JSONObject();
            for (CanalEntry.Column column : columnsList) {

                System.out.println(column.getName()+"::"+column.getValue());
                jsonObject.put(column.getName(),column.getValue());
            }

            MykafkaSender.send(kafkaTopic,jsonObject.toJSONString());
        }

    }

}

MykafkaSender

package canal;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;


public class MykafkaSender {
    public static KafkaProducer<String, String> kafkaProducer = null;


    public static KafkaProducer<String, String> createKafkaProducer() {
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hw-node1:9092");//kafka集群,broker-list
        properties.put(ProducerConfig.ACKS_CONFIG, "all");
        properties.put(ProducerConfig.RETRIES_CONFIG, 100);//重试次数
        properties.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);//这个参数不设置,失败重试时会改变消息的顺序
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,536870912 );//重试次数
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 10000);//批次大小
        properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,360000);
        properties.put(ProducerConfig.MAX_BLOCK_MS_CONFIG,1800000);

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        KafkaProducer<String, String> producer = null;

        try {
            producer = new KafkaProducer<String, String>(properties);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return producer;
    }

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

    }
}

消费kafka topic

kafka-console-consumer --bootstrap-server  hw-node3:9092 --topic canal_student_insert

往表中插入数据

insert into student values(1,'yang');
insert into student values(1,'yang');

kafka topic
在这里插入图片描述

监控数据的后续处理

1、上传到hdfs 用于制作拉链表
2、spark streaming 实时分析

发布了48 篇原创文章 · 获赞 5 · 访问量 1171

猜你喜欢

转载自blog.csdn.net/qq_34897849/article/details/103364923