课堂笔记

实时数仓Day04

昨日回顾

维度数据同步:

  • 离线同步维度数据到Redis.

  • 实时同步维度数据到Redis.

    数据过滤

    进行数据落地:

    判断MySQL中的操作: 增加/修改/删除.

    执行的时候,不要忘记在APP主程序中调用ETL程序.

点击流数据处理

LogParser日志解析工具: 帮助我们快速的从日志数据中获取我们需要的字段信息.

使用方式:

定义一个封装数据的实体类.

实体类里面必须提供对成员变量设置值的set方法.

Parser<实体类型> parser = new HttpdLoglinParser(实体类字节码对象, 格式化字符串)

parser.addParseTarget(set方法名称, path路径)

实体类 变量名 = parser.parse(日志原始数据)

创建点击流日志实体类.

今日课程介绍

点击流日志ETL处理.

订单数据ETL处理.

订单明细ETL处理.

点击流日志ETL处理

业务介绍:

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

代码开发步骤

当前处理类要负责的任务:

  1. 使用LogParser将日志原始数据转换为对象: String => ClickLogBean

  2. 对ClickLogBean对象的地域信息进行拉宽操作: ClickLogBean => ClickLogWideBean(地域信息/时间信息)

    根据IP获取地域信息:

    1. 可以找一个公共的接口.将IP地址信息发送到这个接口,之后获取响应结果(Json),之后我们分析响应的Json中的数据内容,(这种方式类似于前面离线业务中生成时间维度数据中假日信息的做法)

    2. 自己弄一套IP地址库,自己在库里面进行查询即可.

      IP地址库有多种: 纯真IP地址库/淘宝IP地址库…

  3. 将ClickLogWideBean => Json字符串

  4. 将json发送到Kafka的DWD层: dwd_click_log

IP地址获取

方案一: 使用公共API进行获取.

curl cip.cc

方案二: 使用在线网站查询: https://tool.lu/ip/

方案三: 我们手动使用纯真IP地址库,手动解析: qqwry.dat

开发步骤:

  1. 将地址库和解析的工具类拷贝到当前项目中.

    1. 拷贝地址库: 将实时数仓Day04\资料\2.纯真ip库\qqwry.dat拷贝到父工程的data文件夹下.

    2. 拷贝解析工具类:将实时数仓Day04\资料\2.纯真ip库\工具类拷贝到ETL模块cn.itcast.shop.realtime.etl.bean.ip包下.

    3. 修改application.conf配置文件中IP地址库的文件地址为自己的路径:

      # ip库本地文件路径
      ip.file.path="C:/My_Data/IDEA_WorkSpace/itcast_shop_parent_bs/data/qqwry.dat"
      

      注意路径中斜线的方向.

  2. 如果公共的数据量比较大,每个task都去获取数据,会造成很大的内存开销.

    解决方案:

    1. 使用广播: 将数据放入TaskManager的公共内存中.

    2. 使用分布式缓存: 将数据拷贝到TaskManager的磁盘中.

      我们项目中此处使用分布式环境解决数据加载问题.

    3. 使用RichMapFunction进行业务处理.

代码开发

  1. 在APP主程序中注册分布式缓存文件

    //进行业务开发之前,先注册分布式缓存文件
    env.registerCachedFile(GlobalConfigUtil.`ip.file.path`, "qqwry.dat")
    
  2. 编写日志ETL主程序代码

    package cn.itcast.shop.realtime.etl.process
    
    import java.io.File
    import java.util.Locale
    
    import cn.itcast.shop.realtime.etl.bean.ip.IPSeeker
    import cn.itcast.shop.realtime.etl.bean.{ClickLogBean, ClickLogWideBean}
    import cn.itcast.shop.realtime.etl.process.base.MQBaseETL
    import cn.itcast.shop.realtime.etl.util.{GlobalConfigUtil, KafkaProps}
    import com.alibaba.fastjson.JSON
    import com.alibaba.fastjson.serializer.SerializerFeature
    import com.google.gson.JsonObject
    import nl.basjes.parse.httpdlog.HttpdLoglineParser
    import org.apache.commons.lang3.time.FastDateFormat
    import org.apache.flink.api.common.functions.RichMapFunction
    import org.apache.flink.api.common.serialization.SimpleStringSchema
    import org.apache.flink.configuration.Configuration
    import org.apache.flink.streaming.api.scala._
    import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer011
    
    /**
     * 点击流业务处理的ETL程序
     */
    class ClickLogETL(env: StreamExecutionEnvironment) extends MQBaseETL(env) {
      /**
       * 后续所有的ETL操作都需要将功能实现放在Process方法中.
       */
      override def process(): Unit = {
        // 获取点击流日志数据
        val clickSource: DataStream[String] = getDataSource(GlobalConfigUtil.`input.topic.click_log`)
        //1. 使用LogParser将日志原始数据转换为对象: String => ClickLogBean
        // map转换,操作方式有2种:
        // 1. 使用函数
        //val parser = createClickLogParser()
        //ClickLogBean(parser, line)
        // 2. 使用对象
        val clickLogBeanStream: DataStream[ClickLogBean] = clickSource.map(new RichMapFunction[String, ClickLogBean] {
    
          var parser: HttpdLoglineParser[ClickLogBean] = _
    
          override def open(parameters: Configuration): Unit = {
            //在open方法中,初始化LogParser对象
            parser = ClickLogBean.createClickLogParser()
          }
    
          override def map(log: String): ClickLogBean = {
            //开始进行数据解析转换
            val bean: ClickLogBean = parser.parse(log)
            //使用apply方法创建
            //        val bean1: ClickLogBean = ClickLogBean(parser, log)
            bean
          }
        })
        //    clickLogBeanStream.print()
    
        //2. 对ClickLogBean对象的地域信息进行拉宽操作: ClickLogBean => ClickLogWideBean(地域信息/时间信息)
        //   根据IP获取地域信息:
        //   1. 可以找一个公共的接口.将IP地址信息发送到这个接口,之后获取响应结果(Json),之后我们分析响应的Json中的数据内容,(这种方式类似于前面离线业务中生成时间维度数据中假日信息的做法)
        //   2. 自己弄一套IP地址库,自己在库里面进行查询即可.
        //      IP地址库有多种: 纯真IP地址库/淘宝IP地址库....
        val clickLogWideStream: DataStream[ClickLogWideBean] = clickLogBeanStream.map(new RichMapFunction[ClickLogBean, ClickLogWideBean] {
          var seeker: IPSeeker = _
          override def open(parameters: Configuration): Unit = {
            // 获取分布式缓存文件
            val file: File = getRuntimeContext.getDistributedCache.getFile("qqwry.dat")
            // 创建一个扫描器
            seeker = new IPSeeker(file)
          }
          override def map(logBean: ClickLogBean): ClickLogWideBean = {
            // ClickLogBean => ClickLogWideBean
            // 北京市大兴区
            val wideBean: ClickLogWideBean = ClickLogWideBean(logBean)
            val country: String = seeker.getCountry(wideBean.ip)
            val arr: Array[String] = country.split("省")
            //定义省份和城市
            var province = ""
            var city = ""
            //判断数组的长度
            if (arr.length > 1) {
              // 切开了, 有省份和城市 河北省廊坊市
              province = arr(0) + "省"
              city = arr(1)
            } else {
              // 直辖市 上海市 北京市 上海 闵行区  北京市大兴区
              val temArr: Array[String] = country.split("市")
              if (temArr.length > 1) {
                province = temArr(0)
                city = arr(1)
              }
            }
            // 将省份和城市赋值给宽表
            wideBean.setProvince(province)
            wideBean.setCity(city)
            //时间戳 05/Sep/2010:11:27:50 +0200
            // 将时间字符串转换为时间戳.
            // FastDateFormat
            val timeStamp: Long = FastDateFormat
              .getInstance("dd/MMM/yyyy:HH:mm:ss Z", Locale.ENGLISH)
              .parse(logBean.getRequestTime)
              .getTime
            wideBean.setTimestamp(timeStamp)
            // 返回宽表数据
            wideBean
          }
        })
        clickLogWideStream.print("宽表数据:")
        //3. 将ClickLogWideBean => Json字符串
        val jsonStream: DataStream[String] = clickLogWideStream.map(line => JSON.toJSONString(line, SerializerFeature.DisableCircularReferenceDetect))
        //4. 将json发送到Kafka的DWD层: `dwd_click_log`
        val kafkaSink = new FlinkKafkaProducer011[String](
          GlobalConfigUtil.`output.topic.clicklog`,
          new SimpleStringSchema(),
          KafkaProps.getKafkaProps()
        )
        jsonStream.addSink(kafkaSink)
      }
    }
    

测试程序

创建日志数据的topic

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

启动日志的生产者和消费者

# 生产者
/export/servers/kafka_2.11-1.0.0/bin/kafka-console-producer.sh --broker-list node1:9092,node2:9092,node3:9092 --topic ods_itcast_click_log
# 消费者
/export/servers/kafka_2.11-1.0.0/bin/kafka-console-consumer.sh --zookeeper node1:2181 --topic dwd_click_log

模拟数据

2001:980:91c0:1:8d31:a232:25e5:85d 175.188.159.62 - [05/Sep/2010:11:27:50 +0200] "GET /images/my.jpg HTTP/1.1" 404 23617 "http://www.angularjs.cn/A00n" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; nl-nl) AppleWebKit/533.17.8 (KHTML, like Gecko) Version/5.0.1 Safari/533.17.8"

启动APP主程序

别忘记在APP主程序中,引用ClickLogETL

测试:

在生产者中发送数据,在消费者中,看明细数据是否能够获取到.

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

订单数据ETL处理

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

代码开发步骤:

  1. 编写订单的实体类对象: OrderDBEntity

  2. 获取数据源

  3. 对数据进行过滤操作:

    1. 我们只要订单表数据: itcast_orders

    2. 我们只要新增的数据: insert

      下完订单后,如果订单状态发生改变,会产生多余数据此处会影响到后面的指标统计,比如订单数量/周环比分析等业务.

      实际开发中,要结合业务,如果需要更新和删除事件,那么此处就不用过滤了.

  4. 将RowData数据=>OrderDBEntity

  5. 将OrderDBEntity=>Json

  6. 将数据发送给Kafka.

编写订单实体类

package cn.itcast.shop.realtime.etl.bean

import cn.itcast.shop.bean.RowData

import scala.beans.BeanProperty

/**
 * 订单数据样例类
 */
case class OrderDBEntity(
                          @BeanProperty orderId: Long, //订单id
                          @BeanProperty orderNo: String, //订单编号
                          @BeanProperty userId: Long, //用户id
                          @BeanProperty orderStatus: Int, //订单状态 -3:用户拒收-2:未付款的订单-1:用户取消 0:待发货 1:配送中 2:用户确认收货
                          @BeanProperty goodsMoney: Double, //商品金额
                          @BeanProperty deliverType: Int, //收货方式0:送货上门1:自提
                          @BeanProperty deliverMoney: Double, //运费
                          @BeanProperty totalMoney: Double, //订单金额(包括运费)
                          @BeanProperty realTotalMoney: Double, //实际订单金额(折扣后金额)
                          @BeanProperty payType: Int, //支付方式
                          @BeanProperty isPay: Int, //是否支付0:未支付1:已支付
                          @BeanProperty areaId: Int, //区域最低一级
                          @BeanProperty areaIdPath: String, //区域idpath
                          @BeanProperty userName: String, //收件人姓名
                          @BeanProperty userAddress: String, //收件人地址
                          @BeanProperty userPhone: String, //收件人电话
                          @BeanProperty orderScore: Int, //订单所得积分
                          @BeanProperty isInvoice: Int, //是否开发票1:需要0:不需要
                          @BeanProperty invoiceClient: String, //发票抬头
                          @BeanProperty orderRemarks: String, //订单备注
                          @BeanProperty orderSrc: Int, //订单来源0:商城1:微信2:手机版3:安卓App4:苹果App
                          @BeanProperty needPay: Double, //需缴费用
                          @BeanProperty payRand: Int, //货币单位
                          @BeanProperty orderType: Int, //订单类型
                          @BeanProperty isRefund: Int, //是否退款0:否1:是
                          @BeanProperty isAppraise: Int, //是否点评0:未点评1:已点评
                          @BeanProperty cancelReason: Int, //取消原因ID
                          @BeanProperty rejectReason: Int, //用户拒绝原因ID
                          @BeanProperty rejectOtherReason: String, //用户拒绝其他原因
                          @BeanProperty isClosed: Int, //订单是否关闭
                          @BeanProperty goodsSearchKeys: String,
                          @BeanProperty orderunique: String, //订单流水号
                          @BeanProperty receiveTime: String, //收货时间
                          @BeanProperty deliveryTime: String, //发货时间
                          @BeanProperty tradeNo: String, //在线支付交易流水
                          @BeanProperty dataFlag: Int, //订单有效标志 -1:删除 1:有效
                          @BeanProperty createTime: String, //下单时间
                          @BeanProperty settlementId: Int, //是否结算,大于0的话则是结算ID
                          @BeanProperty commissionFee: Double, //订单应收佣金
                          @BeanProperty scoreMoney: Double, //积分抵扣金额
                          @BeanProperty useScore: Int, //花费积分
                          @BeanProperty orderCode: String,
                          @BeanProperty extraJson: String, //额外信息
                          @BeanProperty orderCodeTargetId: Int,
                          @BeanProperty noticeDeliver: Int, //提醒发货 0:未提醒 1:已提醒
                          @BeanProperty invoiceJson: String, //发票信息
                          @BeanProperty lockCashMoney: Double, //锁定提现金额
                          @BeanProperty payTime: String, //支付时间
                          @BeanProperty isBatch: Int, //是否拼单
                          @BeanProperty totalPayFee: Int, //总支付金额
                          @BeanProperty isFromCart: String //是否来自购物车 0:直接下单  1:购物车
                        )

object OrderDBEntity {
  def apply(rowData: RowData): OrderDBEntity = {
    OrderDBEntity(
      rowData.getColumns.get("orderId").toLong,
      rowData.getColumns.get("orderNo"),
      rowData.getColumns.get("userId").toLong,
      rowData.getColumns.get("orderStatus").toInt,
      rowData.getColumns.get("goodsMoney").toDouble,
      rowData.getColumns.get("deliverType").toInt,
      rowData.getColumns.get("deliverMoney").toDouble,
      rowData.getColumns.get("totalMoney").toDouble,
      rowData.getColumns.get("realTotalMoney").toDouble,
      rowData.getColumns.get("payType").toInt,
      rowData.getColumns.get("isPay").toInt,
      rowData.getColumns.get("areaId").toInt,
      rowData.getColumns.get("areaIdPath"),
      rowData.getColumns.get("userName"),
      rowData.getColumns.get("userAddress"),
      rowData.getColumns.get("userPhone"),
      rowData.getColumns.get("orderScore").toInt,
      rowData.getColumns.get("isInvoice").toInt,
      rowData.getColumns.get("invoiceClient"),
      rowData.getColumns.get("orderRemarks"),
      rowData.getColumns.get("orderSrc").toInt,
      rowData.getColumns.get("needPay").toDouble,
      rowData.getColumns.get("payRand").toInt,
      rowData.getColumns.get("orderType").toInt,
      rowData.getColumns.get("isRefund").toInt,
      rowData.getColumns.get("isAppraise").toInt,
      rowData.getColumns.get("cancelReason").toInt,
      rowData.getColumns.get("rejectReason").toInt,
      rowData.getColumns.get("rejectOtherReason"),
      rowData.getColumns.get("isClosed").toInt,
      rowData.getColumns.get("goodsSearchKeys"),
      rowData.getColumns.get("orderunique"),
      rowData.getColumns.get("receiveTime"),
      rowData.getColumns.get("deliveryTime"),
      rowData.getColumns.get("tradeNo"),
      rowData.getColumns.get("dataFlag").toInt,
      rowData.getColumns.get("createTime"),
      rowData.getColumns.get("settlementId").toInt,
      rowData.getColumns.get("commissionFee").toDouble,
      rowData.getColumns.get("scoreMoney").toDouble,
      rowData.getColumns.get("useScore").toInt,
      rowData.getColumns.get("orderCode"),
      rowData.getColumns.get("extraJson"),
      rowData.getColumns.get("orderCodeTargetId").toInt,
      rowData.getColumns.get("noticeDeliver").toInt,
      rowData.getColumns.get("invoiceJson"),
      rowData.getColumns.get("lockCashMoney").toDouble,
      rowData.getColumns.get("payTime"),
      rowData.getColumns.get("isBatch").toInt,
      rowData.getColumns.get("totalPayFee").toInt,
      rowData.getColumns.get("isFromCart")
    )
  }
}

在BaseETL添加数据落地方法

  /**
   * 定义数据落地到Kafka的操作
   * @param topic
   * @return
   */
  def sink2Kafka(topic: String): FlinkKafkaProducer011[String] = {
    val kafkaSink = new FlinkKafkaProducer011[String](
      topic,
      new SimpleStringSchema(),
      KafkaProps.getKafkaProps()
    )
    kafkaSink
  }

编写ETL主程序

package cn.itcast.shop.realtime.etl.process

import cn.itcast.shop.bean.RowData
import cn.itcast.shop.realtime.etl.bean.OrderDBEntity
import cn.itcast.shop.realtime.etl.process.base.MySQLBaseETL
import cn.itcast.shop.realtime.etl.util.{GlobalConfigUtil, KafkaProps}
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.serializer.SerializerFeature
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer011

/**
 * 订单的ETL处理类
 * @param env
 */
class OrderETL(env: StreamExecutionEnvironment) extends MySQLBaseETL(env){
  /**
   * 后续所有的ETL操作都需要将功能实现放在Process方法中.
   */
  override def process(): Unit = {
    //1. 获取数据源
    val mysqlSource: DataStream[RowData] = getDataSource()
    //2. 对数据进行过滤操作:
    //   1. 我们只要订单表数据: `itcast_orders`
    val orderDBEntiryStream: DataStream[OrderDBEntity] = mysqlSource
      .filter(_.getTableName == "itcast_orders")
      //   2. 我们只要新增的数据: `insert`
      //      下完订单后,如果订单状态发生改变,会产生多余数据此处会影响到后面的指标统计,比如订单数量/周环比分析等业务.
      //      实际开发中,要结合业务,如果需要更新和删除事件,那么此处就不用过滤了.
      .filter(_.getEventType.equalsIgnoreCase("insert"))
      //3. 将RowData数据=>OrderDBEntity
      .map(OrderDBEntity(_))

    //4. 将OrderDBEntity=>Json
    val jsonStream: DataStream[String] = orderDBEntiryStream.map(line => JSON.toJSONString(line, SerializerFeature.DisableCircularReferenceDetect))
    //测试数据打印
    jsonStream.printToErr("新生成的订单数据=>")
    //5. 将数据发送给Kafka.
//    val kafkaSink = new FlinkKafkaProducer011[String](
//      GlobalConfigUtil.`output.topic.order`,
//      new SimpleStringSchema(),
//      KafkaProps.getKafkaProps()
//    )
    jsonStream.addSink(sink2Kafka(GlobalConfigUtil.`output.topic.order`))
  }
}

测试订单数据处理

创建Topic:

启动订单明细的消费者

/export/servers/kafka_2.11-1.0.0/bin/kafka-console-consumer.sh --zookeeper node1:2181 --topic dwd_order

在MySQL中新增订单数据

INSERT INTO `itcast_shop`.`itcast_orders`(`orderId`, `orderNo`, `userId`, `orderStatus`, `goodsMoney`, `deliverType`, `deliverMoney`, `totalMoney`, `realTotalMoney`, `payType`, `isPay`, `areaId`, `userAddressId`, `areaIdPath`, `userName`, `userAddress`, `userPhone`, `orderScore`, `isInvoice`, `invoiceClient`, `orderRemarks`, `orderSrc`, `needPay`, `payRand`, `orderType`, `isRefund`, `isAppraise`, `cancelReason`, `rejectReason`, `rejectOtherReason`, `isClosed`, `goodsSearchKeys`, `orderunique`, `isFromCart`, `receiveTime`, `deliveryTime`, `tradeNo`, `dataFlag`, `createTime`, `settlementId`, `commissionFee`, `scoreMoney`, `useScore`, `orderCode`, `extraJson`, `orderCodeTargetId`, `noticeDeliver`, `invoiceJson`, `lockCashMoney`, `payTime`, `isBatch`, `totalPayFee`, `modifiedTime`) VALUES (355, '123456', 137, 2, 0.12, 0, 0.00, 3029.00, 0.00, 3, 0, 123, 409, NULL, '徐牧', '江苏省 无锡市 北塘区', NULL, 0, 0, NULL, NULL, 0, 0.00, 1, 0, 0, 0, 0, 0, NULL, 0, NULL, '8021250f-0e82-48dd-bc23-d9cb2908bfc0', 0, NULL, NULL, NULL, 1, '2019-09-15 08:17:58', 0, 0.00, 0.00, 0, 'order', NULL, 0, 0, NULL, 0.00, '2019-09-15 08:21:52', 0, 0, '2020-01-07 23:08:23');

运行结果:

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

订单明细ETL处理

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

到目前为止,我们已经做到了将维度数据同步到Redis中,接下来我们就可以处理订单明细数据了,每来一条订单明细数据,我们就将该数据进行实时拉宽处理,主要就是为了获取这条订单明细的商家/商品分类/地域信息等内容.

开发步骤梳理

  1. 定义实体类

    定义订单明细宽表实体类: OrderGoodsWideEntity

  2. 加载数据源

  3. 过滤订单明细数据

    过滤订单明细表数据: itcast_order_goods

    过滤事件: insert.因为订单明细是一个一般性事实表.只要产生后,就不会发生改变.

  4. 对数据进行拉宽处理,如何拉宽?

    比如来一个map操作,在map中获取Redis中维度数据,然后将数据组装成宽表数据(同步的操作)

    使用异步IO获取Redis中的维度数据(异步的操作)

  5. 将拉宽后的数据转换为json

  6. 将json数据发送到Kafka中

  7. 将拉宽后的数据保存到HBase

异步IO

比如现在我们去食堂吃饭:

同步: 人太多,需要排队,一个一个上,前面的通过点菜,付款,做好饭之后端着自己的饭走了,然后下一个继续点菜/付款…程序的处理时间等同于最后一个任务完成时间.

异步: 人太多,需要排队,大家伙一块上.点餐/付款,付款之后服务员给个小票(票号),当我们的饭做好之后,由服务员叫号.

如果采用异步操作,我们就不会阻塞线程,可以实现资源的最大化利用.

*方式* *说明*
同步、提高并行度(MapFunction) 非常高的资源成本;高并行度MapFunction意味着更多的subtask,线程,网络连接,数据库连接,缓冲区等等
Async I/O 与数据库的异步交互意味着一个并行函数实例可以同时处理多个请求并同时接收响应(资源复用),这样等待时间可以与发送其他请求和接收响应重叠,至少等待时间是在多个请求上平摊的,这在大多数据情况下会导致更高的流吞吐量

Flink中已经内置了异步IO的支持库,所以我们可以直接使用,不需要依赖第三方客户端.

订单明细拉宽开发步骤:

  1. 先获取能够直接拿到数据.
  2. 根据商品的ID获取商品的维度数据
    1. 使用商品ID查询Redis中的商品维度数据
    2. 将json转换为商品维度实体类
    3. 从商品对象中获取商品名称/3级分类ID/商家ID等信息
  3. 根据3级分类ID获取维度数据
    1. 将json转换为对象
    2. 获取3级分类名称,2级的ID
  4. 根据2级分类ID获取维度数据
    1. 将json转换为对象
    2. 获取2级分类名称,1级的ID
  5. 根据1级分类ID获取维度数据
    1. 将json转换为对象
    2. 获取1级分类名称
  6. 根据商家ID获取商家的信息(商家的名字/公司名称/区域ID)
  7. 根据区域ID获取组织机构的城市信息(城市名称/父级ID)
  8. 根据城市的父级ID获取大区相关信息(大区名称)
  9. 将上方构建的所有数据封装为订单明细宽表数据.

异步IO代码编写

表关系梳理:

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

需要对维度实体类进行修改,添加将json字符串转换为对象的apply方法:

object DimGoodsDBEntity{
  def apply(json: String): DimGoodsDBEntity = {
    //使用FastJson将json字符串转换为对象
    JSON.parseObject(json, classOf[DimGoodsDBEntity])
  }
}
package cn.itcast.shop.realtime.etl.async

import cn.itcast.shop.bean.RowData
import cn.itcast.shop.realtime.etl.bean.{DimGoodsCatDBEntity, DimGoodsDBEntity, DimOrgDBEntity, DimShopsDBEntity, OrderGoodsWideEntity}
import cn.itcast.shop.realtime.etl.util.{GlobalConfigUtil, RedisUtil}
import org.apache.flink.configuration.Configuration
import org.apache.flink.runtime.concurrent.Executors
import org.apache.flink.streaming.api.scala.async.{AsyncFunction, ResultFuture, RichAsyncFunction}
import redis.clients.jedis.Jedis

import scala.concurrent.{ExecutionContext, Future}

/**
 * 对订单明细进行拉宽处理,此处使用异步IO实现.
 * 从Redis中获取维度数据,将当前的订单明细数据进行拉宽
 * 最后返回拉宽的明细数据.
 */
class AsyncOrderGoodsDetailRedisRequest extends RichAsyncFunction[RowData, OrderGoodsWideEntity] {
  var jedis: Jedis = _

  /**
   * 当线程创建的时候调用一次,一般执行一些初始化的任务
   *
   * @param parameters
   */
  override def open(parameters: Configuration): Unit = {
    //获取Redis连接
    jedis = RedisUtil.getJedis()
  }

  /**
   * 如果超时了,要做什么事儿.
   *
   * @param rowData
   * @param resultFuture
   */
  override def timeout(rowData: RowData, resultFuture: ResultFuture[OrderGoodsWideEntity]): Unit = {
    println("有数据超时了: " + rowData)
  }

  // 使用Future的时候,需要进行隐式参数导包操作
  implicit lazy val executor = ExecutionContext.fromExecutor(Executors.directExecutor())
  /**
   * 实现异步IO具体操作的内容,比如获取Redis中的维度数据,进行ETL处理.
   *
   * @param rowData 本次要处理的数据
   * @param resultFuture
   */
  override def asyncInvoke(rowData: RowData, resultFuture: ResultFuture[OrderGoodsWideEntity]): Unit = {
    Future{
      //1. 先获取能够直接拿到数据.
      val ogId: String = rowData.getColumns.get("ogId")
      val orderId: String = rowData.getColumns.get("orderId")
      val goodsId: String = rowData.getColumns.get("goodsId")
      val goodsNum: String = rowData.getColumns.get("goodsNum")
      val goodsPrice: String = rowData.getColumns.get("goodsPrice")
      //2. 根据商品的ID获取商品的维度数据
      //   1. 使用商品ID查询Redis中的商品维度数据
      val goodsJson: String = jedis.hget(GlobalConfigUtil.`redis.key.goods`, goodsId)
      //   2. 将json转换为商品维度实体类
      val goodsDBEntity: DimGoodsDBEntity = DimGoodsDBEntity(goodsJson)
      //   3. 从商品对象中获取商品名称/3级分类ID/商家ID等信息
      val goodsName: String = goodsDBEntity.getGoodsName
      val goodsThirdCatId: String = goodsDBEntity.getGoodsCatId.toString
      val shopId: String = goodsDBEntity.getShopId.toString
      //3. 根据3级分类ID获取维度数据
      val cat3Json: String = jedis.hget(GlobalConfigUtil.`redis.key.goods_cats`, goodsThirdCatId)
      //   1. 将json转换为对象
      val cat3Entity: DimGoodsCatDBEntity = DimGoodsCatDBEntity(cat3Json)
      //   2. 获取3级分类名称,2级的ID
      val goodsThirdCatName: String = cat3Entity.getCatName
      val goodsSecondCatId: String = cat3Entity.getParentId.toString
      //4. 根据2级分类ID获取维度数据
      val cat2Json: String = jedis.hget(GlobalConfigUtil.`redis.key.goods_cats`, goodsSecondCatId)
      //   1. 将json转换为对象
      val cat2Entity: DimGoodsCatDBEntity = DimGoodsCatDBEntity(cat2Json)
      //   2. 获取2级分类名称,1级的ID
      val goodsSecondCatName: String = cat2Entity.getCatName
      val goodsFirstCatId: String = cat2Entity.getParentId.toString
      //5. 根据1级分类ID获取维度数据
      val cat1Json: String = jedis.hget(GlobalConfigUtil.`redis.key.goods_cats`, goodsFirstCatId)
      //   1. 将json转换为对象
      val cat1Entity: DimGoodsCatDBEntity = DimGoodsCatDBEntity(cat1Json)
      //   2. 获取2级分类名称,1级的ID
      val goodsFirstCatName: String = cat1Entity.getCatName
      //6. 根据商家ID获取商家的信息(商家的名字/公司名称/区域ID)
      val shopJson: String = jedis.hget(GlobalConfigUtil.`redis.key.shops`, shopId)
      val shopEntity: DimShopsDBEntity = DimShopsDBEntity(shopJson)
      val areaId: String = shopEntity.getAreaId.toString
      val shopName: String = shopEntity.getShopName
      val shopCompany: String = shopEntity.getShopCompany
      //7. 根据区域ID获取组织机构的城市信息(城市名称/父级ID)
      val cityJson: String = jedis.hget(GlobalConfigUtil.`redis.key.org`, areaId)
      val cityEntity: DimOrgDBEntity = DimOrgDBEntity(cityJson)
      val cityId: String = cityEntity.getOrgId.toString
      val cityName: String = cityEntity.getOrgName
      val regionId: String = cityEntity.getParentId.toString
      //8. 根据城市的父级ID获取大区相关信息(大区名称)
      val regionJson: String = jedis.hget(GlobalConfigUtil.`redis.key.org`, regionId)
      val regionEntity: DimOrgDBEntity = DimOrgDBEntity(regionJson)
      val regionName: String = regionEntity.getOrgName
      //9. 将上方构建的所有数据封装为订单明细宽表数据.
      val wideEntity: OrderGoodsWideEntity = OrderGoodsWideEntity(
        ogId.toLong, //订单明细ID
        orderId.toLong, //订单ID
        goodsId.toLong, // 商品ID
        goodsNum.toLong, // 商品数量
        goodsPrice.toDouble, // 商品的价格
        goodsName, // 商品的名称
        shopId.toLong, // 商家id
        goodsThirdCatId.toInt, // 3级分类ID
        goodsThirdCatName, // 3级分类的名称
        goodsSecondCatId.toInt, // 2级分类ID
        goodsSecondCatName, // 2级分类的名称
        goodsFirstCatId.toInt, // 1级分类ID
        goodsFirstCatName, // 1级分类的名称
        areaId.toInt, // 商家区域ID
        shopName, // 商家的名称
        shopCompany, // 商家所属公司
        cityId.toInt, // 城市ID
        cityName, // 城市的名称
        regionId.toInt, // 大区的ID
        regionName) // 大区的名称

      //我们数据处理完了之后,要告诉外界,我们执行完了,可以那结果了.
      //complete需要向外传递的是一个迭代器对象,需要用List将我们的数据包起来.
      resultFuture.complete(List(wideEntity))
    }

  }

  /**
   * 当线程关闭的时候调用,一般执行一些释放资源的操作
   */
  override def close(): Unit = super.close()
}

编写订单明细主程序

导入HBase操作的工具类: 实时数仓Day04\资料\2.hbase连接池工具类

package cn.itcast.shop.realtime.etl.process

import java.util.concurrent.TimeUnit

import cn.itcast.shop.bean.RowData
import cn.itcast.shop.realtime.etl.async.AsyncOrderGoodsDetailRedisRequest
import cn.itcast.shop.realtime.etl.bean.OrderGoodsWideEntity
import cn.itcast.shop.realtime.etl.process.base.MySQLBaseETL
import cn.itcast.shop.realtime.etl.util.{GlobalConfigUtil, HbaseUtil}
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.serializer.SerializerFeature
import org.apache.flink.api.common.functions.MapFunction
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction
import org.apache.flink.streaming.api.scala._
import org.apache.hadoop.hbase.TableName
import org.apache.hadoop.hbase.client.{Connection, Put, Table}

/**
 * 订单明细ETL处理程序
 * @param env
 */
class OrderGoodsETL(env: StreamExecutionEnvironment) extends MySQLBaseETL(env){
  /**
   * 后续所有的ETL操作都需要将功能实现放在Process方法中.
   */
  override def process(): Unit = {
    //1. 加载数据源
    val sourceSteam: DataStream[RowData] = getDataSource()
    //2. 过滤订单明细数据
    //   过滤订单明细表数据: `itcast_order_goods`
    val orderGoodsSourceStream: DataStream[RowData] = sourceSteam
      .filter(_.getTableName == "itcast_order_goods")
      //   过滤事件: `insert`.因为订单明细是一个一般性事实表.只要产生后,就不会发生改变.
      .filter(_.getEventType.equalsIgnoreCase("insert"))
    //3. 对数据进行拉宽处理,如何拉宽?
    //   比如来一个map操作,在map中获取Redis中维度数据,然后将数据组装成宽表数据(同步的操作)
    //   使用异步IO获取Redis中的维度数据(异步的操作)
//    orderGoodsSourceStream.map(new MapFunction[RowData, OrderGoodsWideEntity] {})
    //参数1: 要使用异步IO进行处理的数据源(Stream)
    //参数2: 要进行异步IO的具体操作(类似于上面的Map)
    //参数3: 超时时间,等待结果需要多久.
    //参数4: 时间单位
    //参数5: 异步IO的并行度是多少
    val wideStream: DataStream[OrderGoodsWideEntity] = AsyncDataStream.unorderedWait(
      orderGoodsSourceStream,
      new AsyncOrderGoodsDetailRedisRequest,
      1,
      TimeUnit.SECONDS,
      100
    )
    //4. 将拉宽后的数据转换为json
    val jsonStream: DataStream[String] = wideStream.map(line => JSON.toJSONString(line, SerializerFeature.DisableCircularReferenceDetect))
    jsonStream.printToErr("订单明细宽表数据: =>")

    //5. 将json数据发送到Kafka中
    jsonStream.addSink(sink2Kafka(GlobalConfigUtil.`output.topic.order_detail`))
    //6. 将拉宽后的数据保存到HBase
//    order_detail_1261 {}
    wideStream.addSink(new RichSinkFunction[OrderGoodsWideEntity] {

      var table: Table = _

      override def open(parameters: Configuration): Unit = {
        //获取HBase连接对象
        val connection: Connection = HbaseUtil.getPool().getConnection
        //获取表对象(如果HBase中没有我们要操作的表,需要先手动创建, create "dwd_order_detail","detail")
        table = connection.getTable(TableName.valueOf(GlobalConfigUtil.`hbase.table.orderdetail`))
      }

      override def invoke(wideEntity: OrderGoodsWideEntity): Unit = {
        //向Hbase中保存数据
        val put = new Put(wideEntity.ogId.toString.getBytes) //以订单明细ID作为RowKey
        //向Put中添加列信息
        //定义列簇名字
        val familyName: Array[Byte] = GlobalConfigUtil.`hbase.table.family`.getBytes
        put.addColumn(familyName, "ogId".getBytes, wideEntity.ogId.toString.getBytes)
        put.addColumn(familyName, "orderId".getBytes, wideEntity.orderId.toString.getBytes)
        put.addColumn(familyName, "goodsId".getBytes, wideEntity.goodsId.toString.getBytes)
        put.addColumn(familyName, "goodsNum".getBytes, wideEntity.goodsNum.toString.getBytes)
        put.addColumn(familyName, "goodsPrice".getBytes, wideEntity.goodsPrice.toString.getBytes)
        put.addColumn(familyName, "goodsName".getBytes, wideEntity.goodsName.toString.getBytes)
        put.addColumn(familyName, "shopId".getBytes, wideEntity.shopId.toString.getBytes)
        put.addColumn(familyName, "goodsThirdCatId".getBytes, wideEntity.goodsThirdCatId.toString.getBytes)
        put.addColumn(familyName, "goodsThirdCatName".getBytes, wideEntity.goodsThirdCatName.toString.getBytes)
        put.addColumn(familyName, "goodsSecondCatId".getBytes, wideEntity.goodsSecondCatId.toString.getBytes)
        put.addColumn(familyName, "goodsSecondCatName".getBytes, wideEntity.goodsSecondCatName.toString.getBytes)
        put.addColumn(familyName, "goodsFirstCatId".getBytes, wideEntity.goodsFirstCatId.toString.getBytes)
        put.addColumn(familyName, "goodsFirstCatName".getBytes, wideEntity.goodsFirstCatName.toString.getBytes)
        put.addColumn(familyName, "areaId".getBytes, wideEntity.areaId.toString.getBytes)
        put.addColumn(familyName, "shopName".getBytes, wideEntity.shopName.toString.getBytes)
        put.addColumn(familyName, "shopCompany".getBytes, wideEntity.shopCompany.toString.getBytes)
        put.addColumn(familyName, "cityId".getBytes, wideEntity.cityId.toString.getBytes)
        put.addColumn(familyName, "cityName".getBytes, wideEntity.cityName.toString.getBytes)
        put.addColumn(familyName, "regionId".getBytes, wideEntity.regionId.toString.getBytes)
        put.addColumn(familyName, "regionName".getBytes, wideEntity.regionName.toString.getBytes)
        table.put(put)
      }
    })
  }
}

运行测试:

先创建订单明细的Topic:

启动订单明细的消费者:

/export/servers/kafka_2.11-1.0.0/bin/kafka-console-consumer.sh --zookeeper node1:2181 --topic dwd_order_detail

启动Canal客户端/APP主程序.

修改数据库观察数据变更信息:

INSERT INTO `itcast_shop`.`itcast_order_goods`(`ogId`, `orderId`, `goodsId`, `goodsNum`, `goodsPrice`, `payPrice`, `goodsSpecId`, `goodsSpecNames`, `goodsName`, `goodsImg`, `extraJson`, `goodsType`, `commissionRate`, `goodsCode`, `promotionJson`, `createtime`) VALUES (1262, 352, 111285, 5, 268.00, 1340.00, 33, '', '', '', NULL, 0, 0.02, NULL, NULL, '2019-09-15 08:17:58');

结果验证:

Kafka中的数据

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

HBase中的数据

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

注意事项

在运行的时候报错:

Caused by: java.nio.file.FileSystemException: C:\Users\焦海峰\AppData\Local\Temp\blobStore-4ff11bfd-0862-4f6c-9598-2b37e80915d7\job_b7a840c415c235e4ecb5bcb328ed0c77\blob_p-0128b1b0c5245148327c9fbe79b74c0741c441db-014a42fda3467c3f7bd962ca29b5b88f: 另一个程序正在使用此文件,进程无法访问。

这是因为我们使用了分布式缓存,并且程序配置了checkpoint和重启策略,如果程序中有错误,程序会重启,这个时候会引起文件的使用权占用问题.

其实这个错误的根本原因还是我们程序中有报错,可以尝试将日志级别调整为info,看控制台中有么有别的报错信息,或者关闭checkpoint和重启策略.

你可能感兴趣的:(实时数仓)