Canal

Canal

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

  • canal是 阿里巴巴 的一个使用Java开发的开源项目
  • 它是专门用来进行 数据库同步 的
  • 目前支持 mysql 、以及(mariaDB)

1、Canal原理

在这里插入图片描述

  1. Canal模拟mysql slave的交互协议,伪装自己为mysql slave

  2. 向mysql master发送dump协议

  3. mysql master收到dump协议,发送binary log给slave(canal) 4. canal解析binary log字节流对象


2、Canal安装部署

2.1 MySQL开启binlog

binlog日志介绍:

  • 用来记录mysql中的 增加 、 删除 、 修改 操作

  • select操作 不会 保存到binlog中

  • 必须要 打开 mysql中的binlog功能,才会生成binlog日志

用来记录mysql中的 增加 、 删除 、 修改 操作select操作 不会 保存到binlog中

必须要 打开 mysql中的binlog功能,才会生成binlog日志

  • binlog日志就是一系列的二进制文件

修改MySQL的配置文件: /etc/my.cnf

# 添加以下配置
log-bin=/var/lib/mysql/mysql-bin 
binlog-format=ROW 
server_id=1

# 配置说明
# 1、配置binlog日志的存放路径为/var/lib/mysql目录,文件以mysql-bin开头
# 2、配置mysql中每一行记录的变化都会详细记录下来
# 3、配置当前机器器的服务ID(如果是mysql集群,不能重复)

重启mysql

systemctl restart mysqld
# mysql -u root -p123456

登录mysql执行以下SQL命令查看配置是否生效

show variables like '%log_bin%';

mysql输出以下内容,表示binlog已经成功开启

+---------------------------------+--------------------------------+
| Variable_name                   | Value                          |
+---------------------------------+--------------------------------+
| log_bin                         | ON                             |
| log_bin_basename                | /var/lib/mysql/mysql-bin       |
| log_bin_index                   | /var/lib/mysql/mysql-bin.index |
| log_bin_trust_function_creators | OFF                            |
| log_bin_use_v1_row_events       | OFF                            |
| sql_log_bin                     | ON                             |
+---------------------------------+--------------------------------+
6 rows in set (0.00 sec)

查看生产的binlog日志,为二进制格式

[root@node01 ~]# cd /var/lib/mysql
[root@node01 mysql]# ll -ah | grep mysql-bin.000001
-rw-r-----   1 mysql mysql  154 Jul 17 10:36 mysql-bin.000001
2.2 安装Canal

下载Canal

https://github.com/alibaba/canal/releases

选择canal.deployer-1.1.4.tar.gz这个版本

上传并解压Canal

# 创建canal的安装目录
mkdir -p /baicdt/servers/canal.deployer-1.1.4
# 上传canal安装包至/baicdt/softwares/
tar -zxvf /baicdt/softwares/canal.deployer-1.1.4.tar.gz -C /baicdt/servers/canal.deployer-1.1.4

修改 conf/example 目录中的 instance.properties 配置文件

# vim /baicdt/servers/canal.deployer-1.1.4/conf/example/instance.properties

## mysql serverId 这个ID是canal伪装的mysql从节点的id,所有不能与mysql实际配置的 service_id 重复
canal.instance.mysql.slaveId=1001
# position info
canal.instance.master.address=node01.hadoop.com:3306
canal.instance.dbUsername=root
canal.instance.dbPassword=123456

启动Canal

cd /baicdt/servers/canal.deployer-1.1.4
bin/startup.sh

注意:

Canal的远程连接端口号默认为 11111 ,当然如果需要,可以在 canal.properties 文件中修改

3、Canal Java API

功能描述:

使用java api创建一个canal客户端获取服务器binlog日志信息,解析结果并写入到Kafka中。

3.1 pom依赖
    <repositories>
        <repository>
            <id>aliyun</id>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>
        <repository>
            <id>cloudera</id>
            <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
        </repository>
        <repository>
            <id>jboss</id>
            <url>http://repository.jboss.com/nexus/content/groups/public</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <!--<version>1.0.24</version>-->
            <version>1.1.4</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.9.0.1</version>
        </dependency>

        <!--对象和json 互相转换的-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.44</version>
        </dependency>
    </dependencies>
3.2 配置文件

application.properties文件

# canal配置
canal.host=node01.hadoop.com
canal.port=11111
canal.instance=example
mysql.username=root
mysql.password=123456

#kafka的配置
kafka.bootstrap.servers=node01.hadoop.com:9092
kafka.zookeeper.connect=node01.hadoop.com:2181
kafka.input.topic=canal

log4j.properties文件

log4j.rootLogger=error,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender 
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 
log4j.appender.stdout.layout.ConversionPattern=%5p - %m%n
3.3 配置文件公共类
import java.util.ResourceBundle;

public class GlobalConfigUtil {
    //读取application.properties文件
    private static ResourceBundle resourceBundle = ResourceBundle.getBundle("application");

    public static String canalHost= resourceBundle.getString("canal.host");
    public static String canalPort = resourceBundle.getString("canal.port");
    public static String canalInstance = resourceBundle.getString("canal.instance");
    public static String mysqlUsername = resourceBundle.getString("mysql.username");
    public static String mysqlPassword=  resourceBundle.getString("mysql.password");
    public static String kafkaBootstrap= resourceBundle.getString("kafka.bootstrap.servers");
    public static String kafkaZookeeper= resourceBundle.getString("kafka.zookeeper.connect");
    public static String kafkaInput = resourceBundle.getString("kafka.input.topic");
}
3.4 Kafka生产消息
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
import kafka.serializer.StringEncoder;
import java.util.Properties;

//Kafka生产消息工具类
public class KafkaSender {
    private String topic;

    public KafkaSender(String topic) {
        super();
        this.topic = topic;
    }

    //发送消息到Kafka指定topic
    public static void sendMessage(String topic, String key, String data) {
        Producer<String, String> producer = createProducer();
        producer.send(new KeyedMessage<String, String>(topic, key, data));
    }

    // 创建生产者实
    private static Producer<String, String> createProducer() {
        Properties properties = new Properties();
        properties.put("metadata.broker.list", GlobalConfigUtil.kafkaBootstrap);
        properties.put("zookeeper.connect", GlobalConfigUtil.kafkaZookeeper);
        properties.put("serializer.class", StringEncoder.class.getName());
        return new Producer<String, String>(new ProducerConfig(properties));
    }
}
3.5 canal客户端
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
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.baicdt.core.util.GlobalConfigUtil;
import com.baicdt.core.util.KafkaSender;


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

/**
 * Canal解析binlog日志工具类
 */
public class CanalClient {

    /**
     * 内部类: 解析列的信息
     */
    static class ColumnValuePair {
        private String columnName;
        private String columnValue;
        private Boolean isValid;

        public ColumnValuePair(String columnName, String columnValue, Boolean isValid) {
            this.columnName = columnName;
            this.columnValue = columnValue;
            this.isValid = isValid;
        }

        public String getColumnName() { return columnName; }
        public void setColumnName(String columnName) { this.columnName = columnName; }
        public String getColumnValue() { return columnValue; }
        public void setColumnValue(String columnValue) { this.columnValue = columnValue; }
        public Boolean getIsValid() { return isValid; }
        public void setIsValid(Boolean isValid) { this.isValid = isValid; }
    }

    /**
     * 获取Canal连接
     *
     * @param host     主机名
     * @param port     端口号
     * @param instance Canal实例名
     * @param username 用户名
     * @param password 密码
     * @return Canal连接器
     */
    public static CanalConnector getConn(String host, int port, String instance, String username, String password) {
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress(host, port), instance, username, password);
        return canalConnector;
    }

    /**
     * 解析Binlog日志
     *
     * @param entries    Binlog消息实体
     * @param emptyCount 操作的序号
     */
    public static void  analysis(List<CanalEntry.Entry> entries, int emptyCount) {
        for (CanalEntry.Entry entry : entries) {
            // 只解析mysql事务的操作,其他的不解析
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                    entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }

            // 那么解析binlog
            CanalEntry.RowChange rowChange = null;

            try {
                rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                e.printStackTrace();
            }

            // 获取操作类型字段(增加  删除  修改)
            CanalEntry.EventType eventType = rowChange.getEventType();
            // 获取binlog文件名称(当前数据来自那个文件)
            String logfileName = entry.getHeader().getLogfileName();
            // 读取当前操作在binlog文件的位置,偏移量
            long logfileOffset = entry.getHeader().getLogfileOffset();
            // 获取当前操作所属的数据库
            String dbName = entry.getHeader().getSchemaName();
            // 获取当前操作所属的表
            String tableName = entry.getHeader().getTableName();//当前操作的是哪一张表
            long timestamp = entry.getHeader().getExecuteTime();//执行时间

            // 解析操作的行数据
            for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                // 删除操作
                if (eventType == CanalEntry.EventType.DELETE) {
                    // 获取删除之前的所有列数据
                    dataDetails(rowData.getBeforeColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
                }
                // 新增操作
                else if (eventType == CanalEntry.EventType.INSERT) {
                    // 获取新增之后的所有列数据
                    dataDetails(rowData.getAfterColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
                }
                // 更新操作
                else {
                    // 获取更新之后的所有列数据
                    dataDetails(rowData.getAfterColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
                }
            }
        }
    }

    /**
     * 解析具体一条Binlog消息的数据
     *
     * @param columns       当前行所有的列数据
     * @param logFileName   binlog文件名
     * @param logFileOffset 当前操作在binlog中的位置
     * @param dbName        当前操作所属数据库名称
     * @param tableName     当前操作所属表名称
     * @param eventType     当前操作类型(新增、修改、删除)
     * @param emptyCount    操作的序号
     */
    private static void dataDetails(List<CanalEntry.Column> columns,
                                    String logFileName,
                                    Long logFileOffset,
                                    String dbName,
                                    String tableName,
                                    CanalEntry.EventType eventType,
                                    int emptyCount,
                                    long timestamp) {

        // 找到当前那些列发生了改变  以及改变的值
        List<ColumnValuePair> columnValueList = new ArrayList<ColumnValuePair>();

        for (CanalEntry.Column column : columns) {
            ColumnValuePair columnValuePair = new ColumnValuePair(column.getName(), column.getValue(), column.getUpdated());
            columnValueList.add(columnValuePair);
        }

        String key = UUID.randomUUID().toString();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("logFileName", logFileName);
        jsonObject.put("logFileOffset", logFileOffset);
        jsonObject.put("dbName", dbName);
        jsonObject.put("tableName", tableName);
        jsonObject.put("eventType", eventType);
        jsonObject.put("columnValueList", columnValueList);
        jsonObject.put("emptyCount", emptyCount);
        jsonObject.put("timestamp", timestamp);


        // 拼接所有binlog解析的字段
        String data = JSON.toJSONString(jsonObject);

        System.out.println(data);

        // 解析后的数据发送到kafka
        KafkaSender.sendMessage(GlobalConfigUtil.kafkaInput, key, data);
    }

    /**
     * 客户端入口方法
     * @param args
     */
    public static void main(String[] args) {
        // 加载配置文件
        String host = GlobalConfigUtil.canalHost;
        int port = Integer.parseInt(GlobalConfigUtil.canalPort);
        String instance = GlobalConfigUtil.canalInstance;
        String username = GlobalConfigUtil.mysqlUsername;
        String password = GlobalConfigUtil.mysqlPassword;

        // 获取Canal连接
        CanalConnector conn = getConn(host, port, instance, username, password);

        // 从binlog中读取数据
        int batchSize = 100;//每次请求的数据条数
        int emptyCount = 1;

        try {
            conn.connect();
            // canal提供数据库增量订阅和消费业务
            conn.subscribe(".*\\..*"); //subscribe方法:通过正则表达式,指定订阅那个数据库
            //维护偏移量
            conn.rollback();

            int totalCount = 120; //循环次数

            while (totalCount > emptyCount) {
                // 获取数据(100条)
                Message message = conn.getWithoutAck(batchSize);

                long id = message.getId();
                int size = message.getEntries().size();
                if (id == -1 || size == 0) {
                    //没有读取到任何数据
                } else {
                    //有数据,那么解析binlog日志
                    analysis(message.getEntries(), emptyCount);
                    emptyCount++;
                }
                // 确认消息
                conn.ack(message.getId());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            conn.disconnect();
        }
    }
}
3.6 SQL脚本
CREATE DATABASE IF NOT EXISTS pyg;
create table `pyg`.`commodity`(
    commodityId     int(10),       -- 商品ID
    commodityName   varchar(20),   -- 商品名称
    commodityTypeId int(20),       -- 商品类别ID
    originalPrice   double(16, 2), -- 原价
    activityPrice   double(16, 2)  -- 活动价
);

insert into commodity values (1, '耐克', 1, 888.00, 820.00);
update commodity set originalPrice = 999.88 where commodityId = 1;
delete from commodity where commodityId = 1;
3.7 kafka脚本
# 启动kafka
nohup /baicdt/servers/kafka_2.11-1.0.0/bin/kafka-server-start.sh \
 /baicdt/servers/kafka_2.11-1.0.0/config/server.properties 2>&1 &

# 查看Kafka当中已存在的主题
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--list \
--zookeeper node01.hadoop.com:2181

# 创建一个kafka主题 canal
# 单节点kafka服务器,副本数设置为1,分区可设置多个  3
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--create \
--zookeeper node01.hadoop.com:2181 \
--replication-factor 1 \
--partitions 3 \
--topic canal

# 创建一个kafka消费者,用来查看数据
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-console-consumer.sh \
--from-beginning \
--bootstrap-server node01.hadoop.com:9092 \
--topic canal

# 向topic生产数据
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-console-producer.sh \
--broker-list node01.hadoop.com:9092 \
--topic canal

# 删除topic
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--zookeeper node01.hadoop.com:2181 \
--delete \
--topic canal
3.8 binlog日志格式分析

客户端获取到的binlog日志json数据如下:

在这里插入图片描述

格式分析:字段以及说明

字段名称说明示例
emptyCount操作序号(第几条记录)12
logFileNamebinlog文件名mysql-bin.000001
dbName数据库名称pyg
logFileOwsetbinlog文件偏移位置100
eventType操作类型INSERT或UPDATE或DELETE
columnValueList列值列表{ “columnName”: “列名”,“columnValue”: “列值”,“isValid”: “是否有效”}
tableName表名commodity
timestamp执行时间戳1553701139000
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值