对接MQ实时同步mysql数据到kudu-附案例代码

目录

  1. 背景简介
  2. 需求分析
  3. 项目实现
  4. 案例实现

 

一:背景简介

    近几年,随着大数据的兴起,每个公司越来越重视对数据的利用,从广泛的定义来看,数据分为历史数据即离线数据和实时流数据,实时数据的处理往往比离线数据更加复杂,对机器资源的要求更加的苛刻,但是实时数据的采集,加工,利用确实在当前互联网行业有着独特的应用。本人从事电商行业,就拿电商举例,双11大屏,各种业务方对接,需要查看实时数据等,实时的数据比起离线数据更具有时效性。本文就拿实时同步mysql数据到kudu来举例,来大概讲述一下实时系统的一套完整解决方案,当然技术选型没有固定,需要根据公司当前的业务来定。

 

二:需求分析

    实时同步mysql数据到kudu上,大致思路是实时采集mysql的binlog日志,解析binlog日志,通过MQ进行实时的传输,消费者实时消费数据写到kudu当中去,根据以上思路,大致可以分为以下几点:

  1. 实时监控binlog日志并采集
  2. 解析binlog日志,并作为生产者发送给MQ
  3. 作为消费者消费数据
  4. 复构数据源,写入kudu

 

三:项目实现

 

    第一步就是实时监控binlog日志并采集,我们采用的是阿里开源的canal框架,纯java开发的,详细关于canal的介绍请阅读该篇博客:canal介绍。简而言之就是canal可以复制实时监控binlog日志,集采集,解析,过滤,存储一套的完整框架,我们针对canal进行了轻量级的封装(DataHub),以供更加方便的使用,以下是结构流程图:

大家都知道binlog里面存储的是二进制的数据,想要直接利用二进制的数据来进行二次开发是相当复杂的,工程量很难维护,所以采用canal的好处是,canal复制解析二进制数据,生成较好的结构化数据,大概结构如下所示:

以下为本人采集的一条模拟数据,主要方便大家理解解析以后的数据格式:

[INFO] 2019-02-25 14:38:40.069 [DafkaConsumerThread0-bigdata_dmall_pos_sale][LOG_KUDU_JOB]com.dmall.data.pos.PosOrderBinlogHandler:124 - msg : {"batchId":"42|0000000002","dbName":"dmall_pos_sale","ddl":false,"ddlSql":"","eventType":"INSERT","executionTime":1550817636000
,"logicTableName":"sale","partitionKey":"sale","realTableName":"sale","rowData":[{"afterColumns":[{"key":true,"name":"id","null":false,"type":"int(10) unsigned","updated":true,"value":"1"},{"key":false,"name":"group_no","null":false,"type":"varchar(30)","updated":true,"
value":"10"},{"key":false,"name":"region_no","null":false,"type":"varchar(10)","updated":true,"value":"300"},{"key":false,"name":"org_no","null":false,"type":"varchar(10)","updated":true,"value":"2013"},{"key":false,"name":"pos_id","null":false,"type":"smallint(6)","upd
ated":true,"value":"1"},{"key":false,"name":"sale_dt","null":false,"type":"datetime","updated":true,"value":"2019-02-22 14:24:20"},{"key":false,"name":"sale_id","null":false,"type":"int(11)","updated":true,"value":"4"},{"key":false,"name":"total_amt","null":false,"type"
:"decimal(12,4)","updated":true,"value":"1.3000"},{"key":false,"name":"total_discount","null":false,"type":"decimal(12,2)","updated":true,"value":"0.0"},{"key":false,"name":"mem_need_score","null":false,"type":"decimal(12,4)","updated":true,"value":"0.0"},{"key":false,"
name":"mem_ecard_no","null":false,"type":"varchar(60)","updated":true,"value":""},{"key":false,"name":"mem_user_id","null":false,"type":"varchar(60)","updated":true,"value":""},{"key":false,"name":"mem_code","null":false,"type":"varchar(20)","updated":true,"value":""},{
"key":false,"name":"mem_card_channel","null":false,"type":"varchar(30)","updated":true,"value":""},{"key":false,"name":"mem_input_code","null":false,"type":"varchar(60)","updated":true,"value":""},{"key":false,"name":"mem_input_type","null":false,"type":"int(11)","updat
ed":true,"value":"0"},{"key":false,"name":"mem_card_level","null":false,"type":"varchar(30)","updated":true,"value":""},{"key":false,"name":"mem_score_flag","null":false,"type":"int(11)","updated":true,"value":"0"},{"key":false,"name":"eorder_id","null":false,"type":"va
rchar(30)","updated":true,"value":""},{"key":false,"name":"eorder_status","null":false,"type":"varchar(30)","updated":true,"value":"False"},{"key":false,"name":"business_id","null":false,"type":"varchar(30)","updated":true,"value":"1"},{"key":false,"name":"coupon_temple
_no","null":false,"type":"varchar(128)","updated":true,"value":""},{"key":false,"name":"merch_input_dur","null":false,"type":"int(11)","updated":true,"value":"436"},{"key":false,"name":"trans_totl_dur","null":false,"type":"int(11)","updated":true,"value":"442"},{"key":f
alse,"name":"cashier_type","null":false,"type":"int(11)","updated":true,"value":"0"},{"key":false,"name":"cashier_id","null":false,"type":"int(11)","updated":true,"value":"0"},{"key":false,"name":"cashier_no","null":false,"type":"varchar(30)","updated":true,"value":"000
00000"},{"key":false,"name":"qr_code","null":false,"type":"varchar(128)","updated":true,"value":""},{"key":false,"name":"upload_flag","null":false,"type":"int(11)","updated":true,"value":"0"},{"key":false,"name":"upload_msg","null":false,"type":"varchar(60)","updated":t
rue,"value":""}],"beforeColumns":[]}]}

完成了binlog的实时采集和解析以后,接下来需要对接MQ来进行实时消费了,市面上常见的MQ有很多,我们公司采用的是kafka和阿里开源的RocketMQ。大概的结构图如下:

有的小伙伴可能就会为为什么对接了两个MQ(AWS现在已经摒弃不用了),那是因为kafka和Rocket适用的场景不一样。

1.Kafka特点:

1) Kafka的高吞吐建立在批量发送的基础上,而批量发送存在丢数据的风险。
2) 一个partition有多个replica,同步模型要求一次写入,所有replica都写成功才返回。
3) 注重高吞吐,对低延迟没有太多优化。其内部对写同步做了大量的异步处理,因此耗时不稳定,尤其是在多副本的情况下。
4) Topic以Partition为写单位,一个Broker上多个Partition写入让顺序写变成了随机写,牺牲了写性能。

从业务方使用消息系统的特点及Kafka特点,依然坚持用Kafka来实现业务方要求的低延迟是不现实的,并且新版本的Kafka对延迟并没有明显的优化。因此考虑引入其他MQ作为DMG的核心组件。

 

2.为什么是RocketMQ

所有数据单独存储到一个Commit Log,完全顺序写,随机读。

主备数据同步方式:同步写入,异步刷盘。

对磁盘的访问串行化,避免磁盘竟争,不会因为队列增加导致IOWAIT增高。

基于java实现,便于二次开发。

 

3.RocketMQ特性摘要

1) 与Kafka集群类比,NameServer取代了Zookeeper作为服务发现的替代组件。

2) 位置点信息存在Broker上,消费成功的消息提交位置点是Schedule线程异步进行的。

3) Topic有write queue和read queue的概念(与Kafka partition类似),write queue分散写压力,read queue扩大读并发。Read queue数据来源于write queue,一般write queue与read queue的数量相同即可,但也可以不一致。

4) 写入一个Broker的不同Topic的所有消息都顺序写入同一个文件,然后对该文件按Topic、QueueId建索引,保证了顺序写,但读的消耗会增加。

5) 消费端消费失败的数据,会被发回Broker,写入一个后台创建的名为%RETRY%{consume_group_name}的Topic,供消费重试;消费重试的次数如果超过5次,消息会被发回Broker,写入一个后台创建的名为%DLQ%{consume_group_name}的死信队列,死信队列的消息不可再被消费,但可以被检索,重发。

6) RocketMQ亮点之一,所有消息都支持查询。

7) Broker group可水平扩展,增加Broker group,可同时提升吞吐量,降低写延迟。

整个项目的流程差不多就是这个样子,相关组件框架,如果各位读者需要详细介绍的,请自行百度。

 

四:案例实现

    说了这么多结构呀,框架呀,该实现一下需求了,那就是作为消费者从kafka中消费数据,实时的写入kudu当中去,当然这边的数据就是通过DataHub解析完的数据,结构在上文已经给出,下面给出具体的案例代码:

package com.dmall.data.mysql2kudu;

import com.alibaba.fastjson.JSONObject;
import com.dmall.data.kudu.KuduAgentClient;
import com.dmall.data.kudu.KuduColumn;
import com.dmall.data.kudu.KuduRow;
import com.dmall.data.mq.core.BaseDataHub;
import com.dmall.datahub.clientmodel.prototype.ColumnModel;
import com.dmall.datahub.clientmodel.prototype.EventBatchModel;
import com.dmall.datahub.clientmodel.prototype.RowDataModel;
import org.apache.kudu.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

public class Mysql2KuduHandler extends BaseDataHub implements InitializingBean {
    private static String MYSQL_DB_NAME = null;
    private static String MYSQL_TABLE_NAME = null;
    private static final Properties prop = new Properties();
    static BufferedReader br = null;
    static KuduAgentClient agent = null;
    static Logger log = null;

    static {
        log = LoggerFactory.getLogger("LOG_KUDU_JOB_TEST");
        String masterHost = "idc-10-248-3-71.ddw.dmall.com:7051,idc-10-248-3-72.ddw.dmall.com:7051,idc-10-248-3-73.ddw.dmall.com:7051";
        agent = new KuduAgentClient(masterHost);
        try {
            br = new BufferedReader(new FileReader(new File(".").getAbsolutePath().replace(".", "") + "table.properties"));
            prop.load(br);
            Iterator<String> it = prop.stringPropertyNames().iterator();
            while (it.hasNext()) {
                String key = it.next();
                String value = prop.getProperty(key);
                if (key.equals("MYSQL_DB_NAME")) {
                    MYSQL_DB_NAME = value;
                } else if (key.equals("MYSQL_TABLE_NAME")) {
                    MYSQL_TABLE_NAME = value;
                }
                log.info("the key is:{} and value is:{}", key, value);
            }
            br.close();
        } catch (Exception e) {
            e.printStackTrace();
            log.error("执行静态代码块报错:{}", e);
        }
    }

    @Override
    protected void dataHub(EventBatchModel eventBatchModel) throws Exception {
        log.info("msg : {}", JSONObject.toJSONString(eventBatchModel));
//        Thread.sleep(1000);
        if (eventBatchModel.getDbName().equals(MYSQL_DB_NAME)) {
            if (eventBatchModel.getLogicTableName().equals(MYSQL_TABLE_NAME)) {
                RowDataModel[] rowDatas = eventBatchModel.getRowData();
                log.info("need rowDatas : {}", JSONObject.toJSONString(rowDatas));
                for (RowDataModel rowModel : rowDatas) {
                    // 对mysql来的一条数据进行处理
                    ColumnModel[] rowAft = rowModel.getAfterColumns();
                    List<KuduColumn> row = new ArrayList<>();
                    for (ColumnModel column : rowAft) {
                        Type column_kudu_type = null;
                        Object kudu_colValue = null;
                        String colName = column.getName();
                        String colValue = column.getValue();
                        String colType = column.getType();
                        Boolean isKey = column.isKey();
                        // 对colType做相关处理,使之符合需求,如:INT,VARCHAR,DOUBLE,FLOAT等
                        if (colType.startsWith("int")) {
                            colType = "INT";
                        } else if (colType.startsWith("tinyint")) {
                            colType = "TINYINT";
                        } else if (colType.startsWith("smallint")) {
                            colType = "SMALLINT";
                        } else if (colType.startsWith("bigint")) {
                            colType = "BIGINT";
                        } else if (colType.startsWith("varchar")) {
                            colType = "VARCHAR";
                        } else if (colType.startsWith("double")) {
                            colType = "DOUBLE";
                        } else if (colType.startsWith("float")) {
                            colType = "FLOAT";
                        } else if (colType.startsWith("decimal")) {
                            colType = "DECIMAL";
                        } else if (colType.startsWith("datetime")) {
                            colType = "DATETIME";
                        }
                        switch (colType) {
                            case "INT":
                                column_kudu_type = Type.INT32;
                                kudu_colValue = Integer.parseInt(colValue);
                                break;
                            case "TINYINT":
                                // mysql中的TINYINT类型一律对应kudu的INT类型
                                column_kudu_type = Type.INT8;
                                kudu_colValue = Integer.parseInt(colValue);
                                break;
                            case "SMALLINT":
                                // mysql中的SMALLINT类型一律对应kudu的INT类型
                                column_kudu_type = Type.INT16;
                                kudu_colValue = Integer.parseInt(colValue);
                                break;
                            case "BIGINT":
                                column_kudu_type = Type.INT64;
                                kudu_colValue = Long.parseLong(colValue);
                                break;
                            case "VARCHAR":
                                column_kudu_type = Type.STRING;
                                kudu_colValue = colValue;
                                break;
                            case "FLOAT":
                                column_kudu_type = Type.FLOAT;
                                kudu_colValue = Float.parseFloat(colValue);
                                break;
                            case "DOUBLE":
                                column_kudu_type = Type.DOUBLE;
                                kudu_colValue = Double.parseDouble(colValue);
                                break;
                            case "DECIMAL":
                                // mysql中的DECIMAL类型一律对应kudu的DOUBLE类型
                                column_kudu_type = Type.DOUBLE;
                                kudu_colValue = Double.parseDouble(colValue);
                                break;
                            case "DATETIME":
                                // mysql中的DATETIME类型一律对应kudu的DOUBLE类型
                                column_kudu_type = Type.STRING;
                                kudu_colValue = String.valueOf(colValue);
                                break;
                            default:
                                break;
                        }
//                        System.out.println("colName is:"+colName+" and colValue is:"+kudu_colValue+" and column_kudu_type is:"+column_kudu_type+" and isKey is:"+isKey);
//                        将key和value写入kudu表当中
                        log.info("colName is:{} and colValue is:{} and column_kudu_type is:{} and isKey:{}", colName, kudu_colValue, column_kudu_type, isKey);
                        KuduColumn c01 = new KuduColumn();
                        if (isKey) {
                            c01.setColumnName(colName).setColumnValue(kudu_colValue).setColumnType(column_kudu_type).setPrimaryKey(true).setUpdate(true).setNullEnble(false);
                        } else {
                            c01.setColumnName(colName).setColumnValue(kudu_colValue).setColumnType(column_kudu_type).setPrimaryKey(false).setUpdate(true);
                        }
                        row.add(c01);
                    }
                    KuduRow myrows01 = new KuduRow();
                    myrows01.setRows(row);
                    log.info("更新前的数据为:{}",myrows01);
                    if(eventBatchModel.getEventType().equals("INSERT")||eventBatchModel.getEventType().equals("UPDATE")){
                        agent.upsert("impala::"+MYSQL_DB_NAME+"."+MYSQL_TABLE_NAME, agent.getKdClient(), myrows01);
                    }else if(eventBatchModel.getEventType().equals("DELETE")){
                        agent.delete("impala::"+MYSQL_DB_NAME+"."+MYSQL_TABLE_NAME, agent.getKdClient(), myrows01);
                    }
                }
            }
        }
    }
    @Override
    public void afterPropertiesSet() throws Exception {

    }
}

代码只是提供一个思路,并不能直接使用。

好啦,以上就是全部内容,原创作品,转载请注明出处!

谢谢!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章