2021.5.3数仓之歌曲影响力指数分析

看一下项目结构:
base包放的一些用到的基本类
common包是一些工具
dm是DM层
eds是EDS层
ods是ODS层
每一层处理的数据文件都放在了对应的包底下,对应的每个包底下还有有对应的主题,有基于机器的主题,基于用户的主题,基于内容的主题。
ods大部分处理数据都是使用sqoop去导入数据的。
由于ods没有做过多的数据清洗,所以底下的代码非常少,只有一个清洗客户端的日志,ProduceClientLog,这么一个类,还有一些的流式的任务就写在streaming下面

看一下ProduceClientLog是怎么处理数据的:

package com.msbjy.scala.musicproject.ods

import com.alibaba.fastjson.{JSON, JSONObject}
import com.msbjy.scala.musicproject.base.PairRDDMultipleTextOutputFormat
import com.msbjy.scala.musicproject.common.ConfigUtils
import org.apache.spark.{HashPartitioner, SparkContext}
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession

/**
  * 读取 运维人员每天上传到服务器上的客户端日志,进行解析,并加载到 ODS层 的表
  *     加载的ODS表如下:
  *
  *  注意:运行此类需要指定参数是 指定当前日期,格式:20201231 ,
  *   第二个参数是指定对应的日志保存的目录,
  *     例如:hdfs://mycluster/logdata/currentday_clientlog.tar.gz ,
  *     本地直接指定:./MusicProject/data/currentday_clientlog.tar.gz
  */
object ProduceClientLog {

  private val localrun: Boolean = ConfigUtils.LOCAL_RUN
  private val hiveMetaStoreUris = ConfigUtils.HIVE_METASTORE_URIS
  private val hiveDataBase = ConfigUtils.HIVE_DATABASE
  private var sparkSession : SparkSession = _
  private var sc: SparkContext = _
  private val hdfsclientlogpath : String = ConfigUtils.HDFS_CLIENT_LOG_PATH
  private var clientLogInfos : RDD[String] = _

  def main(args: Array[String]): Unit = {
    /**
      * 先判断有没有传递一个参数
      *  指定当前log数据的日期时间 格式:20201231
      *  2.指定当前log的上传路径:例如 : ./MusicProject/data/currentday_clientlog.tar.gz
      */
    if(args.length<1){
      println(s"需要指定 数据日期")
      System.exit(1)
    }
    val logDate = args(0) // 日期格式 : 年月日 20201231

    if(localrun){
      sparkSession = SparkSession.builder()
        .master("local")
        .appName("ProduceClientLog")
        //在本地读取hive数据的时候,想连接到hive必须配置hive metastore,找到hive元数据的地方
        .config("hive.metastore.uris",hiveMetaStoreUris).enableHiveSupport().getOrCreate()
      sc = sparkSession.sparkContext
      clientLogInfos = sc.textFile(
        "L:\\BJMSB\\DWProject\\MusicProject\\data\\currentday_clientlog.tar.gz")
    }else{
      sparkSession = SparkSession.builder().appName("ProduceClientLog").enableHiveSupport().getOrCreate()
      sc = sparkSession.sparkContext
      clientLogInfos = sc.textFile(s"${hdfsclientlogpath}/currentday_clientlog.tar.gz")
    }

    //组织K,V格式的数据 : (客户端请求类型,对应的info信息)
    val tableNameAndInfos = clientLogInfos.map(line => line.split("&"))
      .filter(item => item.length == 6)
      .map(line => (line(2), line(3)))

    //获取当日出现的所有的“请求类型”
//    val allTableNames = tableNameAndInfos.keys.distinct().collect()

    //转换数据,将数据分别以表名的方式存储在某个路径中
    tableNameAndInfos.map(tp=>{
      val tableName = tp._1//客户端请求类型
      val tableInfos = tp._2//请求的json string
      if("MINIK_CLIENT_SONG_PLAY_OPERATE_REQ".equals(tableName)){
        val jsonObject: JSONObject = JSON.parseObject(tableInfos)
        val songid = jsonObject.getString("songid") //歌曲ID
        val mid = jsonObject.getString("mid") //机器ID
        val optrateType = jsonObject.getString("optrate_type") //0:点歌, 1:切歌,2:歌曲开始播放,3:歌曲播放完成,4:录音试听开始,5:录音试听切歌,6:录音试听完成
        val uid = jsonObject.getString("uid") //用户ID(无用户则为0)
        val consumeType = jsonObject.getString("consume_type")//消费类型:0免费;1付费
        val durTime = jsonObject.getString("dur_time") //总时长单位秒(operate_type:0时此值为0)
        val sessionId = jsonObject.getString("session_id") //局数ID
        val songName = jsonObject.getString("songname") //歌曲名
        val pkgId = jsonObject.getString("pkg_id")//套餐ID类型
        val orderId = jsonObject.getString("order_id") //订单号
        (tableName,songid+"\t"+mid+"\t"+optrateType+"\t"+uid+"\t"+consumeType+"\t"+durTime+"\t"+sessionId+"\t"+songName+"\t"+pkgId+"\t"+orderId)
      }else{
        //将其他表的infos 信息直接以json格式保存到目录中
        tp
      }
    }).saveAsHadoopFile(
      s"${hdfsclientlogpath}/all_client_tables/${logDate}",
      classOf[String],
      classOf[String],
      classOf[PairRDDMultipleTextOutputFormat]
    )


    /**
      * 在Hive中创建 ODS层的 TO_CLIENT_SONG_PLAY_OPERATE_REQ_D 表
      */
    sparkSession.sql(s"use $hiveDataBase ")
    sparkSession.sql(
      """
        |CREATE EXTERNAL TABLE IF NOT EXISTS `TO_CLIENT_SONG_PLAY_OPERATE_REQ_D`(
        | `SONGID` string,  --歌曲ID
        | `MID` string,     --机器ID
        | `OPTRATE_TYPE` string,  --操作类型
        | `UID` string,     --用户ID
        | `CONSUME_TYPE` string,  --消费类型
        | `DUR_TIME` string,      --时长
        | `SESSION_ID` string,    --sessionID
        | `SONGNAME` string,      --歌曲名称
        | `PKG_ID` string,        --套餐ID
        | `ORDER_ID` string       --订单ID
        |)
        |partitioned by (data_dt string)
        |ROW FORMAT DELIMITED  FIELDS TERMINATED BY '\t'
        |LOCATION 'hdfs://mycluster/user/hive/warehouse/data/song/TO_CLIENT_SONG_PLAY_OPERATE_REQ_D'
      """.stripMargin)

    sparkSession.sql(
      s"""
        | load data inpath
        | '${hdfsclientlogpath}/all_client_tables/${logDate}/MINIK_CLIENT_SONG_PLAY_OPERATE_REQ'
        | into table TO_CLIENT_SONG_PLAY_OPERATE_REQ_D partition (data_dt='${logDate}')
      """.stripMargin)

    println("**** all finished ****")
  }
}

再看ConfigUtils在common包下面:

package com.msbjy.scala.musicproject.common

import com.typesafe.config.{Config, ConfigFactory}
object ConfigUtils {
  /**
    *  ConfigFactory.load() 默认加载classpath下的application.conf,application.json和application.properties文件。
    *
    */
  lazy val load: Config = ConfigFactory.load()  
  // 一般在Scala中加载配置文件,都会用ConfigFactory这个类,加载resources资源目录底下的配置文件
  val LOCAL_RUN = load.getBoolean("local.run")
  val HIVE_METASTORE_URIS = load.getString("hive.metastore.uris")
  val HIVE_DATABASE = load.getString("hive.database")
  val HDFS_CLIENT_LOG_PATH = load.getString("clientlog.hdfs.path")
  val MYSQL_URL = load.getString("mysql.url")
  val MYSQL_USER = load.getString("mysql.user")
  val MYSQL_PASSWORD = load.getString("mysql.password")
  val TOPIC = load.getString("kafka.userloginInfo.topic")
  val USER_PLAY_SONG_TOPIC = load.getString("kafka.userplaysong.topic")
  val KAFKA_BROKERS = load.getString("kafka.cluster")
  val REDIS_HOST = load.getString("redis.host")
  val REDIS_PORT = load.getInt("redis.port")
  val REDIS_OFFSET_DB = load.getInt("redis.offset.db")
  val REDIS_DB = load.getInt("redis.db")
}

ConfigFactory在xml中的相关依赖:

<!--导入Scala 配置 使用到的类 -->
<dependency>
    <groupId>com.typesafe</groupId>
    <artifactId>config</artifactId>
    <version>1.3.1</version>
</dependency>

再看一下resources目录下的application.conf配置文件:

#是否在本地运行
local.run="true"
#hive uris
hive.metastore.uris="thrift://192.168.179.13:9083"
#数据在Hive的哪个库中
hive.database="default"
#运维人员上传 clientlog 日志数据所在的HDFS路径
clientlog.hdfs.path="hdfs://mycluster/logdata"
#DM 层 结果保存在MySQL中的地址和库
mysql.url = "jdbc:mysql://192.168.179.14:3306/songresult?useUnicode=true&characterEncoding=UTF-8"
#mysql登录账号
mysql.user = "root"
#mysql登录账号密码
mysql.password = "123456"
#kafka 集群
kafka.cluster = "192.168.179.13:9092,192.168.179.14:9092,192.168.179.15:9092"
#kafka log topic
kafka.userloginInfo.topic = "logtopic"
#kafka userplaysonglog topic
kafka.userplaysong.topic = "songinfo"
#redis host -redis 节点
redis.host = "mynode4"
#redis port -redis的端口
redis.port = "6379"
#redis offset db -维护offset的redis库
redis.offset.db = "6"
#result redis db -结果存放在Redis中的库
redis.db = "7"

1.11 Sqoop安装
由于目中使用到了Sqoop将mysql中的数据导入到Hive中,所以这里,首先安装Sqoop。
Sqoop是一款开源的工具,主要用于在Hadoop(Hive)与传统的数据库(mysql、postgresql…)间进行数据的传递,可以将一个关系型数据库(例如 : MySQL ,Oracle ,Postgres等)中的数据导进到HDFS中,也可以将HDFS的数据导进到关系型数据库中。Sqoop的导入导出数据原理非常简单,就是将导入或导出命令翻译成 mapreduce 程序来实现。
Sqoop安装:
1.下载Sqoop
登录sqoop官网:http://sqoop.apache.org/ ,下载Sqoop,这里选择版本1.4.7版本进行下载。
2.上传解压
将下载好的Sqoop,上传到集群中的某个节点上,这里选择mynode3,进行解压。节点必须有Hadoop环境,并且配置好hadoop环境变量。
3.在mynode3节点上配置sqoop的环境变量
在/etc/profile追加Sqoop的环境变量,保存后,执行source /etc/profile使环境变量生效。
1.export SQOOP_HOME=/software/sqoop-1.4.7.bin__hadoop-2.6.0/
2.export PATH=$PATH:$SQOOP_HOME/bin
4.拷贝mysql的驱动包到Sqoop lib下
将“mysql-connector-java-5.1.47.jar”包上传到“$SQOOP_HOME/lib”下。当导入MySQL数据到HDFS时,需要使用MySQL驱动包。
5.拷贝hive相应的jar包到Sqoop lib下
当将数据导入到Hive时,需要使用到Hive对应的jar包,需要将“$HIVE_HOME/lib”目录下的“hive-exec-1.2.1.jar”与“hive-common-1.2.1.jar”两个jar包复制到“$SQOOP_HOME/lib”下。
6.执行命令:sqoop help ,查看是否配置好Sqoop

3.抽取MySQL中song数据到Hive ODS

将MySQL中的“songdb”库中的“song”表通过sqoop抽取到对应的ODS层表“TO_SONG_INFO_D”中,这里需要安装sqoop工具,每天定时全量覆盖更新到Hive中。
首先在安装mysql的mynode2节点登录mysql,使用navicat工具连接mysql,创建songdb库,在“songdb”库中导入“songdb.sql”文件,将“song”数导入到MySQL中。
创建“songdb”库:

create database songdb default character set utf8;

其次,在mynode3上执行sqoop导入数据脚本“ods_mysqltohive_to_song_info_d.sh”,将mysql中的song表数据导入到Hive 数仓“TO_SONG_INFO_D”表中,脚本内容如下:

sqoop import \
--connect  jdbc:mysql://mynode2:3306/songdb?dontTrackOpenResources=true\&defaultFetchSize=10000\&useCursorFetch=true\&useUnicode=yes\&characterEncoding=utf8 \
--username root \
--password 123456 \
--table song \
--target-dir /user/hive/warehouse/data/song/TO_SONG_INFO_D/ \
--delete-target-dir \
--num-mappers 1 \
--fields-terminated-by '\t' 

sqoop后可以指定的参数,解释如下:
 --connect :连接jdbc url
dontTrackOpenResources=true:你关闭所有的statement和resultset,但是如果你没有关闭connection的话,connection就会引用到statement和resultset上,从而导致GC不能释放这些资源(statement和resultset),当设置参数dontTrackOpenResources=true时,在statement关闭后,resultset也会被关闭,达到节省内存目的。
defaultFetchSize:设置每次拉取数据量。
useCursorFetch=true :设置连接属性useCursorFetch=true (5.0版驱动开始支持),表示采用服务器端游标,每次从服务器取fetch_size条数据。
useUnicode=yes&characterEncoding=utf8 : 设置编码格式UTF8
 --username : 数据库用户名
 --password : 数据库密码
 --table : import出的数据库表
 --target-dir : 指定数据导入到HDFS中的路径。
 --delete-target-dir :如果数据输出目录已存在则先删除
 --num-mappers :当数据导入HDFS中时生成的MR任务使用几个map任务。
 --hive-import :将数据从关系数据库中导入到 hive 表中,自动将数据导入到hive中,生成对应–hive-table 指定的表,表中的字段是直接从MySql中映射过来的字段。
 --hive-database : 数据导入Hive中使用的Hive 库。
 --hive-overwrite :覆盖掉在 Hive 表中已经存在的数据,与append只能使用一个。
 --fields-terminated-by : 指定字段列分隔符,默认分隔符是’\001’,建议指定分隔符。
 --hive-table : 导入的Hive表。

问题:sqoop可以将mysql的数据直接导入的hive的一张表中,那么为什么这里要导入到hdfs的路径呢?而且hive的表也不是分区表。
原因:我们要导入的外部表,如果直接使用sqoop的方法会有什么问题?
我们对应的这个模型文件里面,可以看到song这张表的字段,有一个source_id和name,如果要是真的按照sqoop直接将MySQL的表导入到hive的一张表里面,它能不能按照建表的字段去设置?一定是不行的。
它会直接将MySQL的字段导入到hive里面,生成一张表,表名是指定的,但字段名就和MySQL数据库中的字段名一模一样,实际我们在构建数据仓库的时候,我们有做统一字段名的处理,在ODS层里面,已经把字段做了规范化的处理,NBR就对应source_id,NAME就对应name,用sqoop直接导入的时候,做不到这个东西,字段名会保持一致。
所以先导入到hdfs一个路径里面,这个路径就映射我们创建的这张表,所以会自动将这些数据字段映射到指定的字段底下。字段不对应会有一致性的问题。
假设一定在hive中创建好了一张表,又指定–hive-import和–table-name,这时候会直接覆盖元数据,这里创建的字段全都没了,成为了一个内部表,里面的字段就是MySQL数据库的字段。

特别注意:在将代码复制到notepad++的时候,换行符默认是CR LF的形式,这时候要转换成Unix的格式LF
编辑 -> 文档格式转换 -> 转为Unix(LF),然后再复制到Linux中,否则脚本可能不执行或者会出现错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值