课堂笔记

实时数仓搭建和flink分析Day01

共性问题

  1. HBase里面配置的zk地址后面不能有端口号

    node01:2181:2181

    HBase的zk地址不需要自己手动添加端口号,有可能是配置文件引错了.

  2. 启动的时候提示Hive不存在,或者HCAT_HOME有问题.

    先使用which hive查看Hive的目录是不是/usr/bin/hive,如果是这个目录,那么就直接删除,另外再看看有没有/usr/bin/hiveserver2,有的话也顺带删除.

  3. 提示Hadoop目录不存在,或者HDFS上没有spark-libs.jar

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I48kaeE9-1591285741093)(assets/image-20200424091334833.png)]

昨日回顾

学习Kylin

使用步骤:

  1. 加载数据源
  2. 创建Model: 指定事实表/维度表,指定计算包含的指标.指定分区字段/格式
  3. 创建Cube: 指定要分析的维度/指标/碎片化管理策略(自动删除/自动合并)/聚合组的设置/执行引擎.
  4. 构建Cube: 利用MR引擎执行计算,将结果保存到HBase,
  5. 编写SQL进行查询.

Kylin的增量构建: 主要是为了解决全量构建速度慢/冗余数据的问题.

增量构建会造成碎片化问题:

  • 合并: 手动/自动
  • 删除: 手动/自动

Cube优化:

  • 衍生维度: 关联维度表中的字段不会直接参与到计算,而是Kylin帮助我们维护一个关联关系,在需要查询的时候才实时的进行计算,所以效率会比较低.
  • 聚合组: 强制维度/层级维度/联合维度.

结合项目: 使用Kylin作为数仓的ADS层.

今日课程介绍

了解实时数仓的相关环境

学习Canal的使用.

学习ProtoBuf.

学习Canal原理.

课程介绍

项目中涉及的技术

MySQL数据实时采集: Canal.

序列化工具: Google ProtoBuf

消息队列(数仓分层): Kafka

使用Flink对数据进行实时ETL处理.

使用Redis保存维度数据.

关联查询Redis中的数据: 异步IO

使用HBase保存明细数据.便于后面进行一些查询.

使用Phoenix查询HBase中的数据.(即席查询)

实时OLAP分析引擎: Druid.

可视化BI分析工具: Superset.

使用FlinkCEP对订单超时数据进行监控.

项目中的业务

整体业务分为3个部分:

  • 点击流日志相关的业务(PV/UV)
  • 购物车/评论数据
  • 用户的订单相关指标开发(MySQL)

实时计算应用场景

公司内已经采用MR与spark之类的技术,做离线计算,为什么用实时计算?

  • 离线的伤痛就是数据出的太慢
  • 有对实时数据要求高的场景
    • 比如:滴滴的风控、淘宝双十一营销大屏、电商购物推荐、春晚的观众数统计

目的:

要解决离线计算结果太慢,都是T+1的形式,我们需要掌握当前平台的指标数据/状态信息,只能等到第二天才可以获取.

问题:

既然实时数仓能及时的获取到各种指标数据,那么还要离线有什么用?

因为实时数仓存在不可控因素,比如网络/集群的稳定性,有可能会造成计算的误差.我们可以用离线数仓对实时数仓进行计算结果的校准.

比如: 昨天公司搞活动,当天实时部门给老板的数据是3000万的成交量,第二天,离线部门给老板的数据是3200万的成交量.

一般,实时数仓都会将计算的明细数据进行保存,就是为了作为备份,进行数据校准.

实时计算引擎技术选型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rDgqFZW9-1591285741097)(assets/image-20200424101443109.png)]

项目实施环境

数据

  • 目前已经存在订单数据,业务系统会将订单写入到mysql
  • 流量日志数据(访问日志)

硬件

  • 4台物理服务器
  • 服务配置
    • CPU x 2:至强E5 主频2.8 - 3.6,24核(12核 per CPU)
    • 内存(768GB/1T)
    • 硬盘(1T x 8 SAS盘)
    • 网卡(4口 2000M)
    • 500G 7台 24核 128G 4T

人员

  • 4人
    • 前端(2人 JavaWeb + UI前端)
    • 大数据(2人)

时间

  • 一个月左右
  • 阶段
    • 需求调研、评审(2周)
    • 设计架构(3天)
    • 编码、集成(1周)
    • 测试、上线(2天)

常见的软件工程模型

瀑布模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EEMI8VyG-1591285741098)(assets/image-20200424103601268.png)]

敏捷开发

  • 以用户的需求进化为核心,采用迭代、循序渐进的方法进行软件开发
  • 把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成
  • 在开发过程中软件一直处于可使用状态

实时数仓实现方案

Java方式实现

使用Java技术栈进行实现,操作MySQL的数据进行计算,这种方式性能比较低,如果我们的数据量不大,可以使用,开发快/成本低.但是如果数据量很大,不推荐.

使用Flink方式实现

这种方式,适用于数据量很大的场景,同时还要求查询的效率比较高,比如1秒钟内返回数据.

但是这种方式也是有缺点的.

比如前面Flink阶段的电商指标分析系统,采用的就是Flink进行开发.

之前统计的PV/UV等指标数据,计算好之后存入HBase中,查询的时候通过RowKey获取数据.如果后期需求发生变更,那么我们就需要去修改代码,整体灵活性很差.这种方式也是18年以前会使用的方式,目前已经被淘汰了.

比如指标有按照小时/天月等时间段进行统计计算,但是如果实际使用中,时间分类满足不了需求怎么办?比如现在统计最近半个月的指标.

pv:2019090910这个是整点的数据,如果查询没半个小时的指标怎么办?要改代码吧.

实时数仓架构

我们可以将ETL相关的操作放在Flink中.而指标计算使用Druid来实现.后面我们需要什么指标,只需要通过编写SQL进行查询即可.而不需要修改Flink底层的代码逻辑.

实时数仓的整体架构(重点)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q7UJj0Vi-1591285741100)(assets/01_实时数仓架构图.png)]

Canal简介

简介

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IBwoHVLi-1591285741102)(assets/image-20200424142647905.png)]

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

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

基于日志增量订阅和消费的业务包括

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

工作原理

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环境准备

  1. 开启MySQL的binlog日志功能:vim /etc/my.conf

    [mysqld]
    log-bin=/var/lib/mysql/mysql-bin # 指定binlog存储的地址
    binlog-format=ROW # 以行的形式进行记录(记录每一条发生变更的数据)
    server_id=1 # Master的ID, 注意不要和slave的ID重复了.
    
  2. 重启MySQL: service mysqld restart

  3. 添加Canal的执行权限(可以不做):

    CREATE USER root IDENTIFIED BY '123456';  
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' ;
    FLUSH PRIVILEGES;
    

安装Canal

  1. 解压:

    mkdir /export/servers/canal
    tar -zxvf canal.deployer-1.0.24.tar.gz  -C /export/servers/canal/
    
  2. 修改配置文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-POTeIex3-1591285741103)(assets/image-20200424144118242.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7jhkPJUJ-1591285741104)(assets/image-20200424144322824.png)]

#################################################
## mysql serverId
canal.instance.mysql.slaveId = 1234

# position info
canal.instance.master.address = node1:3306
canal.instance.master.journal.name = 
canal.instance.master.position = 
canal.instance.master.timestamp = 

#canal.instance.standby.address = 
#canal.instance.standby.journal.name =
#canal.instance.standby.position = 
#canal.instance.standby.timestamp = 

# username/password
canal.instance.dbUsername = root
canal.instance.dbPassword = 123456
canal.instance.defaultDatabaseName =
canal.instance.connectionCharset = UTF-8

# table regex
canal.instance.filter.regex = .*\\..*
# table black regex
canal.instance.filter.black.regex =  

#################################################
  1. 启动Canal

    bin/startup.sh
    

    查询canal日志

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zNaLaLqi-1591285741105)(assets/image-20200424145412996.png)]

    查询example实例的日志:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqslwzkT-1591285741106)(assets/image-20200424145432521.png)]

如果Canal获取不到数据,可以尝试将索引重置.

  1. 先将canal删除,重新安装.

  2. 删除MySQL之前的binlog

    rm -rf /var/lib/mysql/mysql-bin*
    
  3. 重启MySQL

Canal客户端开发

开发步骤:

  1. 导入依赖
  2. 创建Canal连接对象
  3. 连接Canal服务端
  4. 回滚到最后一次消费的位置
  5. 订阅我们要消费的数据.比如我们只消费itcast_shop数据库
  6. 获取数据
  7. 解析数据
  8. 给服务器一个回执,告诉服务器本批次的数据以及消费过了.
  9. 关闭连接.
package cn.itcast.canal;

import cn.itcast.canal.bean.CanalModel;
import com.alibaba.fastjson.JSON;
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.net.SocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Canal客户端开发
 */
public class CanalClient {
     
    public static void main(String[] args) throws Exception {
     
        //2. 创建Canal连接对象
//        CanalConnectors.newSingleConnector() // 单机版的Canal服务端
//        CanalConnectors.newClusterConnector() // 高可用版本的Canal
        CanalConnector connector = CanalConnectors.newSingleConnector(
                new InetSocketAddress("node1", 11111),
                "example", "root", "123456");
        //3. 连接Canal服务端
        connector.connect();
        //4. 回滚到最后一次消费的位置
        connector.rollback();
        //5. 订阅我们要消费的数据.比如我们只消费itcast_shop数据库
        connector.subscribe("itcast_shop.*");
        // 获取数据不是只获取1次,而是应该源源不断的获取数据,只要数据发生了变更,我们就要进行数据的采集.
        // 所以我们应该用一个死循环,源源不断的获取数据.
        boolean flag = true;
        while (flag) {
     
            //6. 获取数据
//            connector.get() 从服务器端获取数据,并发送回执
//            connector.getWithoutAck() 从服务器获取数据,但是不发送回执.注意: 这种方式需要手动的发送回执.
            Message message = connector.getWithoutAck(1000);
            //7. 解析数据
            long id = message.getId();//本批次数据的ID
            List<CanalEntry.Entry> entries = message.getEntries();// 本批次的数据
            //判断当前批次有没有获取到数据.
            if (id == -1 || entries.size() == 0) {
     
                //没有数据,不执行任何操作
            } else {
     
                //有数据,开始进行数据解析
//                parseMessage(entries);
                // 将数据转换为Json
//                binlogToJson(message);
                // 将binlog转换为protobuf格式
                binlogToProtoBuf(message);
            }
            //8. 给服务器一个回执,告诉服务器本批次的数据已经消费过了.
            connector.ack(id);
            //每一批次都休息1秒钟
//            Thread.sleep(1000);
        }
        //9. 关闭连接.
        connector.disconnect();
    }

    // binlog解析为ProtoBuf
    private static void binlogToProtoBuf(Message message) throws InvalidProtocolBufferException {
     
        // 1. 构建CanalModel.RowData实体
        CanalModel.RowData.Builder rowDataBuilder = CanalModel.RowData.newBuilder();

        // 1. 遍历message中的所有binlog实体
        for (CanalEntry.Entry entry : message.getEntries()) {
     
            // 只处理事务型binlog
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                    entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
     
                continue;
            }

            // 获取binlog文件名
            String logfileName = entry.getHeader().getLogfileName();
            // 获取logfile的偏移量
            long logfileOffset = entry.getHeader().getLogfileOffset();
            // 获取sql语句执行时间戳
            long executeTime = entry.getHeader().getExecuteTime();
            // 获取数据库名
            String schemaName = entry.getHeader().getSchemaName();
            // 获取表名
            String tableName = entry.getHeader().getTableName();
            // 获取事件类型 insert/update/delete
            String eventType = entry.getHeader().getEventType().toString().toLowerCase();

            rowDataBuilder.setLogfileName(logfileName);
            rowDataBuilder.setLogfileOffset(logfileOffset);
            rowDataBuilder.setExecuteTime(executeTime);
            rowDataBuilder.setDbName(schemaName);
            rowDataBuilder.setTableName(tableName);
            rowDataBuilder.setEventType(eventType);

            // 获取所有行上的变更
            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            List<CanalEntry.RowData> columnDataList = rowChange.getRowDatasList();
            for (CanalEntry.RowData rowData : columnDataList) {
     
                if (eventType.equals("insert") || eventType.equals("update")) {
     
                    for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
     
                        rowDataBuilder.putColumns(column.getName(), column.getValue().toString());
                    }
                } else if (eventType.equals("delete")) {
     
                    for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
     
                        rowDataBuilder.putColumns(column.getName(), column.getValue().toString());
                    }
                }
            }

            byte[] bytes = rowDataBuilder.build().toByteArray();
            // 将bytes数组发送到Kafka中
            for (byte b : bytes) {
     
                //181610912111511310845981051104648484848525424-59-8938346117112100971161014211105116999711511695115104111112501110511699971151169510811110311556-24-91-12-37-1024666241021051001818495449534856505649564953485248485057662310311711410818161191191194610897110104117469911110946991106611107114101102101114101114180661110710710112111911111410018066910411612111210118151664410410311710510018365668495067566767455568485045525466514557654968455357506649525156505468706611107112971031019510510018066131091091111001171081019510510018066111071081051101079510510018066171013971161169799104101100951051101021111806646101011510111511510511111095105100183283688669546950665550768765738849499084806682895574546951658871556613109116114979910710111495117180661610121161149799107101114951161211121011806619102105112181349555346484649565746495157661510111161149799107101114951151149918066101069911111110710510118066141010111114100101114959911110010118066331010116114979910795116105109101181950484953454856455056324957584953584852662410111011101009511711510111495105100189504956555049504853661410101021051141151169510810511010718066191015115101115115105111110951181051011199511011118066141010112114111100117991169510510018066241015991171149510910111499104971101169510510018557495652506617101111211411111810511099101951051001824957661410799105116121951051001835050506671031021011011806616101210110010995979911610511810511612118066131091011001099510110997105108180661410101011001099510611198951051001806614101010510195118101114115105111110180661810811210897116102111114109186105801041111101016620101610511011610111411097108951071011211191111141001806614101011410111511710811695115117109180661610129911711411410111011695112971031011806617101310810511010795112111115105116105111110180661910159811711611611111095112111115105116105111110180
                System.out.print(b);
            }
            System.out.println();
        }
    }


    // binlog解析为json字符串
    private static void binlogToJson(Message message) throws InvalidProtocolBufferException {
     
        // 1. 创建Map结构保存最终解析的数据
        Map rowDataMap = new HashMap<String, Object>();

        // 2. 遍历message中的所有binlog实体
        for (CanalEntry.Entry entry : message.getEntries()) {
     
            // 只处理事务型binlog
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                    entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
     
                continue;
            }

            // 获取binlog文件名
            String logfileName = entry.getHeader().getLogfileName();
            // 获取logfile的偏移量
            long logfileOffset = entry.getHeader().getLogfileOffset();
            // 获取sql语句执行时间戳
            long executeTime = entry.getHeader().getExecuteTime();
            // 获取数据库名
            String schemaName = entry.getHeader().getSchemaName();
            // 获取表名
            String tableName = entry.getHeader().getTableName();
            // 获取事件类型 insert/update/delete
            String eventType = entry.getHeader().getEventType().toString().toLowerCase();

            rowDataMap.put("logfileName", logfileName);
            rowDataMap.put("logfileOffset", logfileOffset);
            rowDataMap.put("executeTime", executeTime);
            rowDataMap.put("schemaName", schemaName);
            rowDataMap.put("tableName", tableName);
            rowDataMap.put("eventType", eventType);

            // 封装列数据
            Map columnDataMap = new HashMap<String, Object>();
            // 获取所有行上的变更
            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            List<CanalEntry.RowData> columnDataList = rowChange.getRowDatasList();
            for (CanalEntry.RowData rowData : columnDataList) {
     
                if (eventType.equals("insert") || eventType.equals("update")) {
     
                    for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
     
                        columnDataMap.put(column.getName(), column.getValue());
                    }
                } else if (eventType.equals("delete")) {
     
                    for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
     
                        columnDataMap.put(column.getName(), column.getValue());
                    }
                }
            }

            rowDataMap.put("columns", columnDataMap);
            String json = JSON.toJSONString(rowDataMap);
            System.out.println(json);
            // 将这个json发送给Kafka,让Flink进行消费.
        }
    }


    /**
     * 解析BinLog日志数据
     *
     * @param entries
     */
    private static void parseMessage(List<CanalEntry.Entry> entries) throws InvalidProtocolBufferException {
     
        // 1. 遍历消息集合,本次遍历,相当于遍历每张表中的变化.
        for (CanalEntry.Entry entry : entries) {
     
            // 2. 判断消息的类型,如果是事务的开始,或者是事务的结束,那么这个数据我们不要.
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                    entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
     
                // 跳过本次循环,直接执行下一次循环
                continue;
            }
            // 3. 开始获取我们需要的数据,比如日志文件名称/偏移量/变更类型/数据库名称/表名称/列信息....
            CanalEntry.Header header = entry.getHeader();
            //binlog日志文件名称
            String logfileName = header.getLogfileName();
            //偏移量
            long logfileOffset = header.getLogfileOffset();
            //变更类型
            CanalEntry.EventType eventType = header.getEventType();
            //数据库名称
            String dbName = header.getSchemaName();
            //表名称
            String tableName = header.getTableName();
            //SQL的执行时间
            long executeTime = header.getExecuteTime();
            //打印基本信息
            System.out.println("logfileName: " + logfileName);
            System.out.println("logfileOffset: " + logfileOffset);
            System.out.println("eventType: " + eventType);
            System.out.println("dbName: " + dbName);
            System.out.println("tableName: " + tableName);
            System.out.println("executeTime: " + executeTime);

            // 4. 获取消息体中的当前行的所有记录 protobuf格式.
            ByteString storeValue = entry.getStoreValue();
            // ByteString中的数据都是字节,我们不能直接使用,需要转换为字符串或者是对象(能识别的)
            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
            // rowChange中存放的是当前表中的所有变化的行信息.
            List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
            // 遍历所有变化的行信息
            for (CanalEntry.RowData rowData : rowDatasList) {
     
                //RowData就是具体变化的某一行的数据
                //获取变化之前的数据,比如删除/修改
//                rowData.getBeforeColumnsList();
                //获取变化之后的数据,比如新增/修改.
//                rowData.getAfterColumnsList();
                // 我们可以获取删除之前的数据
                if ("DELETE".equals(eventType.toString())) {
     
                    //获取删除之前的数据
                    List<CanalEntry.Column> columnsList = rowData.getBeforeColumnsList();
                    printColumn(columnsList);
                } else {
     
                    //对于新增和修改,获取之后的数据
                    List<CanalEntry.Column> columnsList = rowData.getAfterColumnsList();
                    printColumn(columnsList);
                }


            }

        }
    }

    /**
     * 打印变化的信息
     *
     * @param columnsList
     */
    private static void printColumn(List<CanalEntry.Column> columnsList) {
     
        //遍历列信息
        for (CanalEntry.Column column : columnsList) {
     
            //column就是每一列的封装对象, 列名/列值/变更状态
            String columnName = column.getName();
            String columnValue = column.getValue();
            boolean updated = column.getUpdated();
            System.out.println("列名: " + columnName + " 列值: " + columnValue + "状态: " + updated);
        }
    }
}

Protocol Buffers介绍

  • Protocal Buffers(简称protobuf)是谷歌的一项技术,用于结构化的数据序列化、反序列化,常用于RPC 系统和持续数据存储系统。
  • 其类似于XML生成和解析,但protobuf的效率高于XML,不过protobuf生成的是字节码,可读性比XML差,类似的还有json、Java的Serializable等。
  • 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
  • 参考:https://zhuanlan.zhihu.com/p/53339153

我们在进行数据传递的时候,会有很多额外的字段,比如字段名.

[{username: zhangsan, address:北京市}]

0: username

1: address

[{0: zhangsan, 1:北京市}]

使用Protobuf压缩后的数据是XML/JSON的三分之一左右.

ProtoBuf的使用

IDEA要安装插件

可以在线安装或者本地安装: 实时数仓Day01\资料\软件\protobuf-jetbrains-plugin-0.13.0.zip

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r5bO3AIf-1591285741107)(assets/image-20200424163952820.png)]

项目中需要添加依赖

<dependencies>
        <dependency>
            <groupId>com.google.protobufgroupId>
            <artifactId>protobuf-javaartifactId>
            <version>3.4.0version>
        dependency>
dependencies>

    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.mavengroupId>
                <artifactId>os-maven-pluginartifactId>
                <version>1.6.2version>
            extension>
        extensions>
        <plugins>
            
            <plugin>
                <groupId>org.xolstice.maven.pluginsgroupId>
                <artifactId>protobuf-maven-pluginartifactId>
                <version>0.5.0version>
                <configuration>
                    <protoSourceRoot>${project.basedir}/src/main/protoprotoSourceRoot>
                    <protocArtifact>
                        com.google.protobuf:protoc:3.1.0:exe:${os.detected.classifier}
                    protocArtifact>
                configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compilegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>

${os.detected.classifier} 如果爆红色,不影响使用,这个是获取本地一些环境的工具

${project.basedir}/src/main/proto 指定protobuf文件存放位置的配置,不要放错了.

编写ProtoBuf文件

//指定语法
syntax = "proto3";

//配置可选项,比如设置包名/类名
option java_package = "cn.itcast.proto";
option java_outer_classname = "DemoModel";

//设置我们要传递的消息的实体
message User {
    // 指定当前字段的类型,还有识别码.
    // 识别码,这个值是从1开始,官方建议这个值尽量在15以内,这样一个字节就可以搞定了.
    int32 id = 1;
    string name = 2;
    string sex = 3;
}

编译proto文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-urRJDOzF-1591285741108)(assets/image-20200424165446349.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8fRZwUy-1591285741108)(assets/image-20200424165458645.png)]

因为Protobuf已经将class放在了输入目录里面,所以我们可以直接使用,而不需要再src目录下创建对应的源码文件.

protobuf的序列化和反序列化

package cn.itcast.canal;

import cn.itcast.proto.DemoModel;
import com.google.protobuf.InvalidProtocolBufferException;

/**
 * 测试ProtoBuf的使用
 */
public class ProtobufDemo {
     
    public static void main(String[] args) throws InvalidProtocolBufferException {
     
        //1. 将对象转换为字节
        DemoModel.User.Builder builder = DemoModel.User.newBuilder();
        // 使用构建器设置对象的属性信息
        builder.setId(1);
        builder.setName("张三");
        builder.setSex("男");
        //将对象转换为字节
        DemoModel.User user = builder.build();
        byte[] bytes = user.toByteArray();
        for (byte b : bytes) {
     
            //81186-27-68-96-28-72-119263-25-108-73
            System.out.print(b);
        }
        System.out.println();
        //2. 将字节转换为对象
        DemoModel.User user2 = DemoModel.User.parseFrom(bytes);
        System.out.println(user2.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getSex());
    }
}

定义Canal的Protobuf配置文件

syntax = "proto3";

option java_package = "cn.itcast.canal.bean";
option java_outer_classname = "CanalModel";

// 一行数据中包含的内容.
message RowData{
    string logfileName = 2;
    uint64 logfileOffset = 3;
    string eventType = 4;
    string dbName = 5;
    string tableName = 6;
    uint64 executeTime = 7;
    //列信息
    map columns = 8;
}

修改原来的Canal客户端代码

    // binlog解析为ProtoBuf
    private static void binlogToProtoBuf(Message message) throws InvalidProtocolBufferException {
     
        // 1. 构建CanalModel.RowData实体
        CanalModel.RowData.Builder rowDataBuilder = CanalModel.RowData.newBuilder();

        // 1. 遍历message中的所有binlog实体
        for (CanalEntry.Entry entry : message.getEntries()) {
     
            // 只处理事务型binlog
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
                    entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
     
                continue;
            }

            // 获取binlog文件名
            String logfileName = entry.getHeader().getLogfileName();
            // 获取logfile的偏移量
            long logfileOffset = entry.getHeader().getLogfileOffset();
            // 获取sql语句执行时间戳
            long executeTime = entry.getHeader().getExecuteTime();
            // 获取数据库名
            String schemaName = entry.getHeader().getSchemaName();
            // 获取表名
            String tableName = entry.getHeader().getTableName();
            // 获取事件类型 insert/update/delete
            String eventType = entry.getHeader().getEventType().toString().toLowerCase();

            rowDataBuilder.setLogfileName(logfileName);
            rowDataBuilder.setLogfileOffset(logfileOffset);
            rowDataBuilder.setExecuteTime(executeTime);
            rowDataBuilder.setDbName(schemaName);
            rowDataBuilder.setTableName(tableName);
            rowDataBuilder.setEventType(eventType);

            // 获取所有行上的变更
            CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            List<CanalEntry.RowData> columnDataList = rowChange.getRowDatasList();
            for (CanalEntry.RowData rowData : columnDataList) {
     
                if (eventType.equals("insert") || eventType.equals("update")) {
     
                    for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
     
                        rowDataBuilder.putColumns(column.getName(), column.getValue().toString());
                    }
                } else if (eventType.equals("delete")) {
     
                    for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
     
                        rowDataBuilder.putColumns(column.getName(), column.getValue().toString());
                    }
                }
            }

            byte[] bytes = rowDataBuilder.build().toByteArray();
            // 将bytes数组发送到Kafka中
            for (byte b : bytes) {
     
                System.out.print(b);
            }
            System.out.println();
        }
    }

Canal 原理(了解)

MySQL的日志记录形式:

它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间。主要用来备份和数据同步。

binlog-format=ROW

binlog 有三种: STATEMENT、ROW、MIXED

  • STATEMENT 记录的是执行的sql语句

    如果一个SQL对数据产生了变更,那么我只需要记录这条SQL就可以了.

    update table set username = "zhangsan" 
    

    假如有1000万条数据,那么这一个SQL就产生了1000万条修改.

    虽然这种方式能降低数据的记录量,但是本身不是特别稳定,有可能会造成数据不同步的问题.

  • ROW 记录的是真实的行数据记录 (Canal采用的是这种方式.)

    如果发生一条改变,就记录一条,特别稳定.

  • MIXED 记录的是1+2,优先按照1的模式记录

    mixed会自己选择数据的记录形式,会采用STATEMENT +ROW 2种方式进行记录.

Canal客户端和服务端的交互过程

基本组件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cnhjAHcs-1591285741109)(assets/image-20200424175357387.png)]

  • server 代表一个 canal 运行实例,对应于一个 jvm

  • instance 对应于一个数据队列 (1个 canal server 对应 1…n 个 instance )

  • instance 下的子模块

    • eventParser: 数据源接入,模拟 slave 协议和 master 进行交互,协议解析
    • eventSink: Parser 和 Store 链接器,进行数据过滤,加工,分发的工作
    • eventStore: 数据存储
    • metaManager: 增量订阅 & 消费信息管理器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bmgRHZWB-1591285741110)(assets/image-20200424175452379.png)]

EventParser的工作内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0SsIiDB-1591285741111)(assets/image-20200424180132812.png)]

EventStore负责存储解析后的Binlog事件,而解析动作负责拉取Binlog,它的流程比较复杂。需要和MetaManager进行交互。
比如要记录每次拉取的Position,这样下一次就可以从上一次的最后一个位置继续拉取。所以MetaManager应该是有状态的。

EventParser的流程如下:

  1. Connection获取上一次解析成功的位置 (如果第一次启动,则获取初始指定的位置或者是当前数据库的binlog位点)
  2. Connection建立链接,发送BINLOG_DUMP指令
  3. Mysql开始推送Binaly Log
  4. 接收到的Binaly Log的通过Binlog parser进行协议解析,补充一些特定信息
  5. 传递给EventSink模块进行数据存储,是一个阻塞操作,直到存储成功
  6. 存储成功后,定时记录Binaly Log位置

你可能感兴趣的:(大数据)