04_Hudi 集成 Spark、保存数据至Hudi、集成Hive查询、MergeInto 语句

news2024/11/28 11:55:52

本文来自"黑马程序员"hudi课程

4.第四章 Hudi 集成 Spark
4.1 环境准备
4.1.1 安装MySQL 5.7.31
4.1.2 安装Hive 2.1
4.1.3 安装Zookeeper 3.4.6
4.1.4 安装Kafka 2.4.1
4.2 滴滴运营分析
4.2.1 需求说明
4.2.2 环境准备
4.2.2.1 工具类SparkUtils
4.2.2.2 日期转换星期
4.2.3 数据ETL保存
4.2.3.1 开发步骤
4.2.3.2 加载CSV数据
4.2.3.3 数据ETL转换
4.2.3.4 保存数据至Hudi
4.2.3.5 Hudi 表存储结构
4.2.4 指标查询分析
4.2.4.1 开发步骤
4.2.4.2 加载Hudi表数据
4.2.4.3 指标一:订单类型统计
4.2.4.4 指标二:订单时效性统计
4.2.4.5 指标三:订单交通类型统计
4.2.4.6 指标四:订单价格统计
4.2.4.7 指标五:订单距离统计
4.2.4.8 指标六:订单星期统计
4.2.5 集成Hive查询
4.2.5.1 创建表及查询
4.2.5.2 HiveQL 分析
4.3 结构化流写入Hudi
4.3.1 模拟交易订单
4.3.2 流式程序开发
4.3.3 Spark 查询分析
4.3.4 DeltaStreamer 工具类
4.4 集成 SparkSQL
4.4.1 启动spark-sql
4.4.2 快速入门
4.4.2.1 创建表
4.4.2.2 插入数据
4.4.2.3 查询数据
4.4.2.4 更新数据
4.4.2.5 删除数据
4.4.3 DDL 创建表
4.4.4 MergeInto 语句
4.4.4.1 Merge Into Insert
4.4.4.2 Merge Into Update
4.4.4.3 Merge Into Delete

4. 第四章 Hudi 集成 Spark

数据湖框架Hudi,从诞生之初支持Spark进行操作,后期支持Flink,接下来先看看与Spark整合使用,并且在0.9.0版本中,提供SparkSQL支持,编写DDL和DML操作数据。

4.1 环境准备

Hudi数据湖框架,开始与Spark分析引擎框架整合,通过Spark保存数据到Hudi表,使用Spark加载Hudi表数据进行分析,不仅支持批处理和流计算,还可以集成Hive进行数据分析,安装大数据其他框架:MySQL、Hive、Zookeeper及Kafka,便于案例集成整合使用。
在这里插入图片描述

4.1.1 安装MySQL 5.7.31

采用tar方式安装MySQL数据库,具体命令和相关说明如下

# 1. 检查系统是否安装过mysql 
rpm -qa|grep mysql

# 2. 卸载CentOS7系统自带mariadb
rpm -qa|grep mariadb
rpm -e --nodeps mariadb-libs.xxxxxxx

# 3. 删除etc目录下的my.cnf ,一定要删掉,等下再重新建
rm /etc/my.cnf

# 4. 创建mysql 用户组和用户
groupadd mysql
useradd -r -g mysql mysql

# 5. 下载安装,从官网安装下载,位置在/usr/local/
wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.31-linux-glibc2.12-x86_64.tar.gz


# 6. 解压安装mysql
tar -zxvf mysql-5.7.31-linux-glibc2.12-x86_64.tar.gz -C /usr/local/
cd /usr/local/
mv mysql-5.7.31-linux-glibc2.12-x86_64 mysql

# 7. 进入mysql/bin/目录,编译安装并初始化mysql,务必记住数据库管理员临时密码
cd mysql/bin/
./mysqld --initialize --user=mysql --datadir=/usr/local/mysql/data --basedir=/usr/local/mysql

# 8. 编写配置文件 my.cnf ,并添加配置
vi /etc/my.cnf
    [mysqld]
    datadir=/usr/local/mysql/data
    port = 3306
    sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
    symbolic-links=0
    max_connections=400
    innodb_file_per_table=1
    lower_case_table_names=1

# 9. 启动mysql 服务器
/usr/local/mysql/support-files/mysql.server start

# 10. 添加软连接,并重启mysql 服务
ln -s /usr/local/mysql/support-files/mysql.server /etc/init.d/mysql
ln -s /usr/local/mysql/bin/mysql /usr/bin/mysql
service mysql restart

# 11. 登录mysql ,密码就是初始化时生成的临时密码
mysql -u root -p

# 12、修改密码,因为生成的初始化密码难记
set password for root@localhost = password('123456');

# 13、开放远程连接
use mysql;
update user set user.Host='%' where user.User='root';
flush privileges;

# 14. 设置开机自启
cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
chmod +x /etc/init.d/mysqld
chkconfig --add mysqld
chkconfig --list

最后使用MySQL数据库客户端远程链接数据库,测试是否成功。

4.1.2 安装Hive 2.1

直接解压Hive框架tar包,配置HDFS依赖及元数据存储MySQL数据库信息,最后启动元数据服务Hive MetaStore和HiveServer2服务。

# 1. 上传,解压
[root@node1 ~]# cd /export/server/
[root@node1 server]# rz
[root@node1 server]# chmod u+x apache-hive-2.1.0-bin.tar.gz      
[root@node1 server]# tar -zxf apache-hive-2.1.0-bin.tar.gz
[root@node1 server]# mv apache-hive-2.1.0-bin hive-2.1.0-bin
[root@node1 server]# ln -s hive-2.1.0-bin hive

# 2. 配置环境变量
[root@node1 server]# cd hive/conf/
[root@node1 conf]# mv hive-env.sh.template hive-env.sh
[root@node1 conf]# vim hive-env.sh 
    HADOOP_HOME=/export/server/hadoop
    export HIVE_CONF_DIR=/export/server/hive/conf
    export HIVE_AUX_JARS_PATH=/export/server/hive/lib
   
# 3. 创建HDFS目录
[root@node1 ~]# hadoop-daemon.sh start namenode
[root@node1 ~]# hadoop-daemon.sh start datanode

[root@node1 ~]# hdfs dfs -mkdir -p /tmp
[root@node1 ~]# hdfs dfs -mkdir -p /usr/hive/warehouse
[root@node1 ~]# hdfs dfs -chmod g+w /tmp
[root@node1 ~]# hdfs dfs -chmod g+w /usr/hive/warehouse

# 4. 配置文件hive-site.xml
[root@node1 ~]# cd /export/server/hive/conf
[root@node1 conf]# vim hive-site.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
	<property>
		<name>javax.jdo.option.ConnectionURL</name>
		<value>jdbc:mysql://node1.itcast.cn:3306/hive_metastore?createDatabaseIfNotExist=true</value>
	</property>
	<property>
		<name>javax.jdo.option.ConnectionDriverName</name>
		<value>com.mysql.jdbc.Driver</value>
	</property>
	<property>
		<name>javax.jdo.option.ConnectionUserName</name>
		<value>root</value>
	</property>
	<property>
		<name>javax.jdo.option.ConnectionPassword</name>
		<value>123456</value>
	</property>
	<property>
		<name>hive.metastore.warehouse.dir</name>
		<value>/usr/hive/warehouse</value>        
	</property>
	<property>
		<name>hive.metastore.uris</name>
		<value>thrift://node1.itcast.cn:9083</value>
	</property>
	<property>
		<name>hive.mapred.mode</name>
		<value>strict</value>
	</property>
	<property>
		<name>hive.exec.mode.local.auto</name>
		<value>true</value>
	</property>
	<property>
		<name>hive.fetch.task.conversion</name>
		<value>more</value>
	</property>
	    <property>
        <name>hive.server2.thrift.client.user</name>
        <value>root</value>
    </property>
    <property>
        <name>hive.server2.thrift.client.password</name>
        <value>123456</value>
    </property>
</configuration>

# 5. 添加用户权限配置
[root@node1 ~]# cd /export/server/hadoop/etc/hadoop
[root@node1 hadoop] vim core-site.xml
<property>
        <name>hadoop.proxyuser.root.hosts</name>
        <value>*</value>
    </property>
    <property>
        <name>hadoop.proxyuser.root.groups</name>
        <value>*</value>
    </property>

# 6. 初始化数据库
[root@node1 ~]# cd /export/server/hive/lib
[root@node1 lib]# rz
	 mysql-connector-java-5.1.48.jar

[root@node1 ~]# cd /export/server/hive/bin
[root@node1 bin]# ./schematool -dbType mysql -initSchema

# 7. 启动HiveMetaStore服务
[root@node1 ~]# cd /export/server/hive
[root@node1 hive]# nohup bin/hive --service metastore >/dev/null &

# 8. 启动HiveServer2服务
[root@node1 ~]# cd /export/server/hive
[root@node1 hive]# bin/hive --service hiveserver2 >/dev/null &

# 9. 启动beeline命令行
[root@node1 ~]# cd /export/server/hive
[root@node1 hive]# bin/beeline -u jdbc:hive2://node1.itcast.cn:10000 -n root -p 123456

服务启动成功后,使用beeline客户端连接,创建数据库和表,导入数据与查询测试。

4.1.3 安装Zookeeper 3.4.6

上传Zookeeper软件至安装目录,解压和配置环境,命令如下所示:

# 上传软件
[root@node1 ~]# cd /export/software
[root@node1 software]# rz
	zookeeper-3.4.6.tar.gz

# 给以执行权限
[root@node1 software]# chmod u+x zookeeper-3.4.6.tar.gz

# 解压tar包
[root@node1 software]# tar -zxf zookeeper-3.4.6.tar.gz -C /export/server

# 创建软链接
[root@node1 ~]# cd /export/server
[root@node1 server]# ln -s zookeeper-3.4.6 zookeeper

# 配置zookeeper
[root@node1 ~]# cd /export/server/zookeeper/conf
[root@node1 conf]# mv zoo_sample.cfg zoo.cfg
[root@node1 conf]# vim zoo.cfg
	修改内容:
	dataDir=/export/server/zookeeper/datas
[root@node1 conf]# mkdir -p /export/server/zookeeper/datas

# 设置环境变量
[root@node1 ~]# vim /etc/profile
添加内容:
        export ZOOKEEPER_HOME=/export/server/zookeeper
        export PATH=$PATH:$ZOOKEEPER_HOME/bin
[root@node1 ~]# source /etc/profile
启动Zookeeper服务,查看状态,命令如下:
# 启动服务
[root@node1 ~]# cd /export/server/zookeeper/
[root@node1 zookeeper]# bin/zkServer.sh start 
JMX enabled by default
Using config: /export/server/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

[root@node1 zookeeper]# bin/zkServer.sh status
JMX enabled by default
Using config: /export/server/zookeeper/bin/../conf/zoo.cfg
Mode: standalone

4.1.4 安装Kafka 2.4.1

上传Kafka软件至安装目录,解压和配置环境,命令如下所示:

# 上传软件
[root@node1 ~]# cd /export/software
[root@node1 software~]#  rz
	kafka_2.12-2.4.1.tgz
[root@node1 software]# chmod u+x kafka_2.12-2.4.1.tgz 

# 解压tar包
[root@node1 software]# tar -zxf kafka_2.12-2.4.1.tgz -C /export/server
[root@node1 ~]# cd /export/server
[root@node1 server]# ln -s kafka_2.12-2.4.1 kafka

# 配置kafka
[root@node1 ~]# cd /export/server/kafka/config
[root@node1 conf]# vim server.properties
	修改内容:
	listeners=PLAINTEXT://node1.itcast.cn:9092		log.dirs=/export/server/kafka/kafka-logs
	zookeeper.connect=node1.itcast.cn:2181/kafka
# 创建存储目录		
[root@node1 ~]# mkdir -p /export/server/kafka/kafka-logs

# 设置环境变量
[root@node1 ~]# vim /etc/profile
添加内容:
        export KAFKA_HOME=/export/server/kafka
        export PATH=$PATH:$KAFKA_HOME/bin
[root@node1 ~]# source /etc/profile
启动Kafka服务,查看状态,命令如下:
# 启动服务
[root@node1 ~]# cd /export/server/kafka
[root@node1 kafka]# bin/kafka-server-start.sh -daemon config/server.properties
[root@node1 kafka]# jps
2188 QuorumPeerMain
2639 Kafka

4.2 滴滴运营分析

以滴滴为首的互联网叫车平台的出现,在重构线下叫车市场的同时,也为市场其他闲置资源提供了更多盈利的可能性。自与快的合并和并购Uber中国以后,滴滴牢牢占据着国内出行市场第一的位置,在飞速发展的同时也不断向广大用户提供多元化的服务,不断优化社会汽车出行方面的资源配置问题。本次样本为随机抽取2017年5月至10月海口市每天的滴滴订单数据,共14160162条
在这里插入图片描述

海口市是南方的旅游大城,滴滴公司在此的业务发展由来已久,积累了大量的业务订单数据,在此利用其2017年下半年的订单数据,做一些简单的统计分析,来看在那段时间内滴滴公司在海口市的业务发展情况并尝试揭示海口市用户的部分出行特征。

  • 快车出行为滴滴运营过程中的主流订单类型;
  • 滴滴出行订单中,预约用车市占率极低,仍以实时预约为主;
  • 接送机订单仅占总订单量的4%;
  • 绝大多数订单距离集中在0-15公里,价格集中在0-100元;
  • 工作日期间,居民对网约车的出行需求降低,而在周末时较为旺盛

4.2.1 需求说明

滴滴出行数据为2017年5月1日-10月31日(半年)海口市每天的订单数据,包含订单的起终点经纬度以及订单类型、出行品类、乘车人数的订单属性数据。具体字段含义说明如下所示:
在这里插入图片描述

依据海口滴滴出行数据,按照如下需求统计分析:
在这里插入图片描述

4.2.2 环境准备

基于前面Maven Project,创建相关目录和包,结构如下图所示:
在这里插入图片描述

其中滴滴出行数据放在Maven Project工程【datas】本地文件系统目录下。对滴滴出行分析,程序分为两个部分:数据存储Hudi表【DidiStorageSpark】和指标计算统计分析【DidiAnalysisSpark】。

4.2.2.1 工具类SparkUtils

无论数据ETL保存,还是数据加载统计,都需要创建SparkSession实例对象,所以编写工具类SparkUtils,创建方法【createSparkSession】构建实例,代码如下:

package cn.itcast.hudi.didi

import org.apache.spark.sql.SparkSession

/**
 * SparkSQL操作数据(加载读取和保存写入)时工具类,比如获取SparkSession实例对象等
 */
object SparkUtils {
   
   /**
    * 构建SparkSession实例对象,默认情况下本地模式运行
    */
   def createSparkSession(clazz: Class[_],
                          master: String = "local[4]", partitions: Int = 4): SparkSession = {
      SparkSession.builder()
         .appName(clazz.getSimpleName.stripSuffix("$"))
         .master(master)
         .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
         .config("spark.sql.shuffle.partitions", partitions)
         .getOrCreate()
   }
   
}

4.2.2.2 日期转换星期

查询分析指标中,需要将日期时间字段值,转换为星期,方便统计工作日和休息日滴滴出行情况,测试代码如下,传递日期时间字符串,转换为星期。

package cn.itcast.hudi.test

import java.util.{Calendar, Date}

import org.apache.commons.lang3.time.FastDateFormat

/**
 * 将日期转换星期,例如输入:2021-06-24  -> 星期四
 *      https://www.cnblogs.com/syfw/p/14370793.html
 */
object DayWeekTest {
   
   def main(args: Array[String]): Unit = {
      
      val dateStr: String = "2021-06-24"
      
      val format: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd")
      val calendar: Calendar = Calendar.getInstance()
      
      val date: Date = format.parse(dateStr)
      calendar.setTime(date)
      
      val dayWeek: String = calendar.get(Calendar.DAY_OF_WEEK) match {
         case 1 => "星期日"
         case 2 => "星期一"
         case 3 => "星期二"
         case 4 => "星期三"
         case 5 => "星期四"
         case 6 => "星期五"
         case 7 => "星期六"
      }
      
      println(dayWeek)
   }
   
}

解析编写代码,本地文件系统加载滴滴出行数据,存储至Hudi表,最后按照指标统计分析。

4.2.3 数据ETL保存

从本地文件系统LocalFS加载海口市滴滴出行数据,进行相应ETL转换,最终存储Hudi表。

4.2.3.1 开发步骤

编写SparkSQL程序,实现数据ETL转换保存,分为如下5步:

  • step1. 构建SparkSession实例对象(集成Hudi和HDFS)
  • step2. 加载本地CSV文件格式滴滴出行数据
  • step3. 滴滴出行数据ETL处理
  • stpe4. 保存转换后数据至Hudi表
  • step5. 应用结束关闭资源
    数据ETL转换保存程序:DidiStorageSpark,其中MAIN方法代码如下:
package cn.itcast.hudi.didi

import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}
import org.apache.spark.sql.functions._

/**
 * 滴滴海口出行运营数据分析,使用SparkSQL操作数据,先读取CSV文件,保存至Hudi表。
 *    -1. 数据集说明
 *        2017年5月1日-10月31日海口市每天的订单数据,包含订单的起终点经纬度以及订单类型、出行品类、乘车人数的订单属性数据。
 *        数据存储为CSV格式,首行为列名称
 *    -2. 开发主要步骤
 *      step1. 构建SparkSession实例对象(集成Hudi和HDFS)
 *      step2. 加载本地CSV文件格式滴滴出行数据
 *      step3. 滴滴出行数据ETL处理
 *      stpe4. 保存转换后数据至Hudi表
 *      step5. 应用结束关闭资源
 */
object DidiStorageSpark {
   
   // 滴滴数据路径
   val datasPath: String = "datas/didi/dwv_order_make_haikou_2.txt"
   
   // Hudi中表的属性
   val hudiTableName: String = "tbl_didi_haikou"
   val hudiTablePath: String = "/hudi-warehouse/tbl_didi_haikou"
   
   def main(args: Array[String]): Unit = {
      // step1. 构建SparkSession实例对象(集成Hudi和HDFS)
      val spark: SparkSession = SparkUtils.createSparkSession(this.getClass)
      import spark.implicits._
      
      // step2. 加载本地CSV文件格式滴滴出行数据
      val didiDF: DataFrame = readCsvFile(spark, datasPath)
      // didiDF.printSchema()
      // didiDF.show(10, truncate = false)
      
      // step3. 滴滴出行数据ETL处理并保存至Hudi表
      val etlDF: DataFrame = process(didiDF)
      //etlDF.printSchema()
      //etlDF.show(10, truncate = false)
      
      // stpe4. 保存转换后数据至Hudi表
      saveToHudi(etlDF, hudiTableName, hudiTablePath)
      
      // stpe5. 应用结束,关闭资源
      spark.stop()
   }

分别实现MAIN中三个方法:加载csv数据、数据etl转换和保存数据。

4.2.3.2 加载CSV数据

编写方法,封装SparkSQL加载CSV格式滴滴出行数据,具体代码如下:

/**
 * 读取CSV格式文本文件数据,封装到DataFrame数据集
 */
def readCsvFile(spark: SparkSession, path: String): DataFrame = {
   spark.read
      // 设置分隔符为逗号
      .option("sep", "\\t")
      // 文件首行为列名称
      .option("header", "true")
      // 依据数值自动推断数据类型
      .option("inferSchema", "true")
      // 指定文件路径
      .csv(path)
}

4.2.3.3 数据ETL转换

编写方法,对滴滴出行数据ETL转换,添加字段【ts】和【partitionpath】,方便保存数据至Hudi表时,指定字段名称。具体代码如下:

/**
 * 对滴滴出行海口数据进行ETL转换操作:指定ts和partitionpath 列
 */
def process(dataframe: DataFrame): DataFrame = {
   dataframe
      // 添加分区列:三级分区 -> yyyy/MM/dd
       .withColumn(
          "partitionpath",  // 列名称
          concat_ws("/", col("year"), col("month"), col("day")) //
       )
      // 删除列:year, month, day
      .drop("year", "month", "day")
      // 添加timestamp列,作为Hudi表记录数据与合并时字段,使用发车时间
       .withColumn(
          "ts",
          unix_timestamp(col("departure_time"), "yyyy-MM-dd HH:mm:ss")
       )
}

4.2.3.4 保存数据至Hudi

编写方法,将ETL转换后数据,保存到Hudi表中,采用COW模式,具体代码如下:

/**
 * 将数据集DataFrame保存值Hudi表中,表的类型:COW
 */
def saveToHudi(dataframe: DataFrame, table: String, path: String): Unit = {
   // 导入包
   import org.apache.hudi.DataSourceWriteOptions._
   import org.apache.hudi.config.HoodieWriteConfig._
   
   // 保存数据
   dataframe.write
      .mode(SaveMode.Overwrite)
      .format("hudi") // 指定数据源为Hudi
      .option("hoodie.insert.shuffle.parallelism", "2")
      .option("hoodie.upsert.shuffle.parallelism", "2")
      // Hudi 表的属性设置
      .option(RECORDKEY_FIELD_OPT_KEY, "order_id")
      .option(PRECOMBINE_FIELD_OPT_KEY, "ts")
      .option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath")
      // 表的名称和路径
      .option(TABLE_NAME, table)
      .save(path)
}

4.2.3.5 Hudi 表存储结构

运行Spark程序,读取CSV格式数据,ETL转换后,保存至Hudi表,查看HDFS目录结构如下:
在这里插入图片描述

4.2.4 指标查询分析

按照查询分析指标,从Hudi表加载数据,进行分组聚合统计,分析结果,给出结论。
在这里插入图片描述

4.2.4.1 开发步骤

创建对象DidiAnalysisSpark,编写MAIN方法,先从Hudi表加载数据,再依据指标分组聚合。

package cn.itcast.hudi.didi

import java.util.{Calendar, Date}

import org.apache.commons.lang3.time.FastDateFormat
import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.sql.functions._

/**
 * 滴滴海口出行运营数据分析,使用SparkSQL操作数据,从加载Hudi表数据,按照业务需求统计。
 *    -1. 数据集说明
 *        海口市每天的订单数据,包含订单的起终点经纬度以及订单类型、出行品类、乘车人数的订单属性数据。
 *        数据存储为CSV格式,首行为列名称
 *    -2. 开发主要步骤
 *      step1. 构建SparkSession实例对象(集成Hudi和HDFS)
 *      step2. 依据指定字段从Hudi表中加载数据
 *      step3. 按照业务指标进行数据统计分析
 *      step4. 应用结束关闭资源
 */
object DidiAnalysisSpark {
   
   // Hudi中表的属性
   val hudiTablePath: String = "/hudi-warehouse/tbl_didi_haikou"
   
   def main(args: Array[String]): Unit = {
      // step1. 构建SparkSession实例对象(集成Hudi和HDFS)
      val spark: SparkSession = SparkUtils.createSparkSession(this.getClass, partitions = 8)
      import spark.implicits._
      
      // step2. 依据指定字段从Hudi表中加载数据
      val hudiDF: DataFrame = readFromHudi(spark, hudiTablePath)
      
      // step3. 按照业务指标进行数据统计分析
      // 指标1:订单类型统计
      // reportProduct(hudiDF)
      // 指标2:订单时效统计
      // reportType(hudiDF)
      // 指标3:交通类型统计
      //reportTraffic(hudiDF)
      // 指标4:订单价格统计
      //reportPrice(hudiDF)
      // 指标5:订单距离统计
      //reportDistance(hudiDF)
      // 指标6:日期类型:星期,进行统计
      //reportWeek(hudiDF)
      
      // step4. 应用结束关闭资源
      spark.stop()
   }

其中将加载Hudi表数据和各个指标统计,分别封装到不同的方法中,便于测试。

4.2.4.2 加载Hudi表数据

编写方法,封装SparkSQL从Hudi表加载数据,其中过滤获取指标统计时所需字段,代码如下:

/**
 * 从Hudi表加载数据,指定数据存在路径
 */
def readFromHudi(spark: SparkSession, path: String): DataFrame = {
   // a. 指定路径,加载数据,封装至DataFrame
   val didiDF: DataFrame = spark.read.format("hudi").load({path)
   
   // b. 选择字段
   didiDF
      // 选择字段
           .select(
          "order_id", "product_id", "type", "traffic_type", //
            "pre_total_fee", "start_dest_distance", "departure_time" //
       )
}

4.2.4.3 指标一:订单类型统计

对海口市滴滴出行数据,按照订单类型统计,使用字段:product_id,其中值【1滴滴专车, 2滴滴企业专车, 3滴滴快车, 4滴滴企业快车】,封装方法:reportProduct,代码如下:

/**
 *  订单类型统计,字段:product_id
 */
def reportProduct(dataframe: DataFrame): Unit = {
   // a. 按照产品线ID分组统计
   val reportDF: DataFrame = dataframe.groupBy("product_id").count()
   
   // b. 自定义UDF函数,转换名称
   val to_name = udf(
      // 1滴滴专车, 2滴滴企业专车, 3滴滴快车, 4滴滴企业快车
      (productId: Int) => {
         productId match {
            case 1 =>  "滴滴专车"
            case 2 =>  "滴滴企业专车"
            case 3 =>  "滴滴快车"
            case 4 =>  "滴滴企业快车"
         }
      }
   )
   
   // c. 转换名称,应用函数
   val resultDF: DataFrame = reportDF.select(
      to_name(col("product_id")).as("order_type"), //
      col("count").as("total") //
   )
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

将统计结果使用柱状图展示,可以看出快车出行为2017年海口市滴滴运营过程中的主流订单类型
在这里插入图片描述

根据统计样本所提供的滴滴订单数据显示,几乎全部的订单都来自于滴滴快车产品线,滴滴专车只占了很小一部分订单量,企业专车产品线的订单量与之相比更是无足轻重。滴滴快车作为滴滴的传统招牌业务,是滴滴公司的支柱。而在14年底就推出的滴滴专车(后于18年改名“礼橙专车”),从17年的数据看来,至少在海口市的使用率不算太高。这也是情有可原的,毕竟滴滴专车的目标受众是更小的高端商务出行人群,旨在为商务出行提供优质服务的产品,其更高的价格与滴滴快车相比,无法成为普罗大众的首选。

4.2.4.4 指标二:订单时效性统计

依据用户下单的时效型:type,分组聚合统计,代码如下:

/**
 *  订单时效性统计,字段:type
 */
def reportType(dataframe: DataFrame): Unit = {
   // a. 按照产品线ID分组统计
   val reportDF: DataFrame = dataframe.groupBy("type").count()
   
   // b. 自定义UDF函数,转换名称
   val to_name = udf(
      // 0实时,1预约
      (realtimeType: Int) => {
         realtimeType match {
            case 0 =>  "实时"
            case 1 =>  "预约"
         }
      }
   )
   
   // c. 转换名称,应用函数
   val resultDF: DataFrame = reportDF.select(
      to_name(col("type")).as("order_realtime"), //
      col("count").as("total") //
   )
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

使用柱状图展示结果,可以看出17年海口市滴滴出行订单中,预约用车市占率极低,仍以实时预约为主。
在这里插入图片描述

根据统计样本所提供的滴滴订单数据显示,尽管滴滴早已推出预约用车的业务,实时需求依旧是绝大部分用车订单的情景,但这并不意味着预约用车没有存在的价值。对于消费者而言,实时用车具有更高的灵活性,但预约用车提供了预先安排、避免特殊情况下打不到车的选择,让消费者能够将乘车出行融入生活的日程表中。

4.2.4.5 指标三:订单交通类型统计

对海口市滴滴出行数据,按照交通类型:traffic_type,分组聚合统计,代码如下:

/**
 *  交通类型统计,字段:traffic_type
 */
def reportTraffic(dataframe: DataFrame): Unit = {
   // a. 按照产品线ID分组统计
   val reportDF: DataFrame = dataframe.groupBy("traffic_type").count()
   
   // b. 自定义UDF函数,转换名称
   val to_name = udf(
      // 1企业时租,2企业接机套餐,3企业送机套餐,4拼车,5接机,6送机,302跨城拼车
      (trafficType: Int) => {
         trafficType match {
case 0 =>  "普通散客"
            case 1 =>  "企业时租"
            case 2 =>  "企业接机套餐"
            case 3 =>  "企业送机套餐"
            case 4 =>  "拼车"
            case 5 =>  "接机"
            case 6 =>  "送机"
            case 302 =>  "跨城拼车"
            case _ => "未知"
         }
      }
   )
   
   // c. 转换名称,应用函数
   val resultDF: DataFrame = reportDF.select(
      to_name(col("traffic_type")).as("traffic_type"), //
      col("count").as("total") //
   )
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

使用柱状图展示结果,可以看出接送机订单仅占总订单量的4%
在这里插入图片描述

机场运送的衔接是滴滴出行的一个潜力市场,在统计样本记录的订单里,17年5月到11月,海口市散客的接送机订单加起来占了总订单量的4%,约56万订单。但企业接送机业务则没有记录。

4.2.4.6 指标四:订单价格统计

对滴滴出行订单数据,依据价格划分不同级别,分组聚合统计,代码如下:

/**
 * 订单价格统计,将价格分阶段统计,字段:pre_total_fee
 */
def reportPrice(dataframe: DataFrame): Unit = {
   val resultDF: DataFrame = dataframe
          .agg(
          // 价格:0 ~ 15
          sum(
             when(
                col("pre_total_fee").between(0, 15), 1
             ).otherwise(0)
          ).as("0~15"),
          // 价格:16 ~ 30
          sum(
             when(
                col("pre_total_fee").between(16, 30), 1
             ).otherwise(0)
          ).as("16~30"),
          // 价格:31 ~ 50
          sum(
             when(
                col("pre_total_fee").between(31, 50), 1
             ).otherwise(0)
          ).as("31~50"),
          // 价格:50 ~ 100
          sum(
             when(
                col("pre_total_fee").between(51, 100), 1
             ).otherwise(0)
          ).as("51~100"),
          // 价格:100+
          sum(
             when(
                col("pre_total_fee").gt(100), 1
             ).otherwise(0)
          ).as("100+")
       )
   
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

其中使用when条件函数和sum累加函数,巧妙的进行条件判断和累加统计。此外,通过结果可以看出【价格集中在0-50元】。

4.2.4.7 指标五:订单距离统计

对滴滴出行数据,按照每次订单行程距离,划分不同分段范围,分组聚合统计,代码如下:

/**
 * 订单距离统计,将价格分阶段统计,字段:start_dest_distance
 */
def reportDistance(dataframe: DataFrame): Unit = {
   val resultDF: DataFrame = dataframe
      .agg(
         // 价格:0 ~ 15
         sum(
            when(
               col("start_dest_distance").between(0, 10000), 1
            ).otherwise(0)
         ).as("0~10km"),
         // 价格:16 ~ 30
         sum(
            when(
               col("start_dest_distance").between(10001, 20000), 1
            ).otherwise(0)
         ).as("10~20km"),
         // 价格:31 ~ 50
         sum(
            when(
               col("start_dest_distance").between(200001, 30000), 1
            ).otherwise(0)
         ).as("20~30km"),
         // 价格:50 ~ 100
         sum(
            when(
               col("start_dest_distance").between(30001, 5000), 1
            ).otherwise(0)
         ).as("30~50km"),
         // 价格:100+
         sum(
            when(
               col("start_dest_distance").gt(50000), 1
            ).otherwise(0)
         ).as("50+km")
      )
   
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

此指标与指标四类似,使用when条件函数和sum求和函数进行统计。

4.2.4.8 指标六:订单星期统计

转换日期为星期,分组聚合统计,查看工作日和休息,滴滴出情况,代码如下:

/**
 *  订单星期分组统计,字段:departure_time
 */
def reportWeek(dataframe: DataFrame): Unit = {
   
   // a. 自定义UDF函数,转换日期为星期
   val to_week: UserDefinedFunction = udf(
      // 0实时,1预约
      (dateStr: String) => {
         val format: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd")
         val calendar: Calendar = Calendar.getInstance()
         
         val date: Date = format.parse(dateStr)
         calendar.setTime(date)
         
         val dayWeek: String = calendar.get(Calendar.DAY_OF_WEEK) match {
            case 1 => "星期日"
            case 2 => "星期一"
            case 3 => "星期二"
            case 4 => "星期三"
            case 5 => "星期四"
            case 6 => "星期五"
            case 7 => "星期六"
         }
         // 返回星期
         dayWeek
      }
   )
   
   // b. 转换日期为星期,并分组和统计
   val resultDF: DataFrame = dataframe
      .select(
         to_week(col("departure_time")).as("week")
      )
           .groupBy(col("week")).count()
           .select(
          col("week"), col("count").as("total") //
       )
   resultDF.printSchema()
   resultDF.show(10, truncate = false)
}

看出结果,可以看出【工作日期间,海口市居民对出租车的出行需求降低,而在周末时较为旺盛】。

4.2.5 集成Hive查询

前面将滴滴出行数据存储在Hudi表中,使用SparkSQL读取数据,接下来集成Hive表数据,从Hudi表读取数据。
在这里插入图片描述

4.2.5.1 创建表及查询

在Hive中创建表,关联至Hudi表,需要将集成JAR包:hudi-hadoop-mr-bundle-0.9.0.jar,放入至**$HIVE_HOME/lib**目录下。

[root@node1 ~]# cp hudi-hadoop-mr-bundle-0.9.0.jar /export/server/hive/lib/

拷贝依赖包到 Hive 路径是为了 Hive 能够正常读到 Hudi 的数据,至此服务器环境准备完毕。
前面Spark 将滴滴出行数据写到Hudi表,想要通过Hive访问到这块数据,就需要创建一个Hive外部表,因为 Hudi 配置了分区,所以为了能读到所有的数据,此时外部表也得分区,分区字段名可随意配置。

# 1. 创建数据库
create database db_hudi ;

# 2. 使用数据库
use db_hudi ;

# 3. 创建外部表
CREATE EXTERNAL TABLE tbl_hudi_didi(
    order_id bigint          ,
    product_id int           ,
    city_id int              ,
    district int             ,
    county int               ,
    type int                 ,
    combo_type int           ,
    traffic_type int         ,
    passenger_count int      ,
    driver_product_id int    ,
    start_dest_distance int  ,
    arrive_time string       ,
    departure_time string    ,
    pre_total_fee double     ,
    normal_time string       ,
    bubble_trace_id string   ,
    product_1level int       ,
    dest_lng double          ,
    dest_lat double          ,
    starting_lng double      ,
    starting_lat double      ,
    partitionpath string     ,
    ts bigint                
)
PARTITIONED BY ( 
  `yarn_str` string, `month_str` string, `day_str` string)
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe' 
STORED AS INPUTFORMAT 
  'org.apache.hudi.hadoop.HoodieParquetInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat'
LOCATION
  '/hudi-warehouse/tbl_didi_haikou' ;
  
# 5. 添加分区  
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='22') location '/hudi-warehouse/tbl_didi_haikou/2017/5/22' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='23') location '/hudi-warehouse/tbl_didi_haikou/2017/5/23' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='24') location '/hudi-warehouse/tbl_didi_haikou/2017/5/24' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='25') location '/hudi-warehouse/tbl_didi_haikou/2017/5/25' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='26') location '/hudi-warehouse/tbl_didi_haikou/2017/5/26' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='27') location '/hudi-warehouse/tbl_didi_haikou/2017/5/27' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='28') location '/hudi-warehouse/tbl_didi_haikou/2017/5/28' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='29') location '/hudi-warehouse/tbl_didi_haikou/2017/5/29' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='30') location '/hudi-warehouse/tbl_didi_haikou/2017/5/30' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='5', day_str='31') location '/hudi-warehouse/tbl_didi_haikou/2017/5/31' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='1') location '/hudi-warehouse/tbl_didi_haikou/2017/6/1' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='2') location '/hudi-warehouse/tbl_didi_haikou/2017/6/2' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='3') location '/hudi-warehouse/tbl_didi_haikou/2017/6/3' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='4') location '/hudi-warehouse/tbl_didi_haikou/2017/6/4' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='5') location '/hudi-warehouse/tbl_didi_haikou/2017/6/5' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='6') location '/hudi-warehouse/tbl_didi_haikou/2017/6/6' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='7') location '/hudi-warehouse/tbl_didi_haikou/2017/6/7' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='8') location '/hudi-warehouse/tbl_didi_haikou/2017/6/8' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='9') location '/hudi-warehouse/tbl_didi_haikou/2017/6/9' ;
alter table db_hudi.tbl_hudi_didi add if not exists partition(yarn_str='2017', month_str='6', day_str='10') location '/hudi-warehouse/tbl_didi_haikou/2017/6/10' ;

# 查看分区信息
show partitions tbl_hudi_didi ;

上述命令执行完成以后,Hive表数据与Hudi表数据关联成功,可以在Hive中编写SQL语句分析Hudi数据,SELECT语句查询表的数据。

# 设置非严格模式
set hive.mapred.mode = nonstrict ;

# SQL查询前10条数据
select order_id, product_id, type, traffic_type, pre_total_fee, start_dest_distance, departure_time 
from db_hudi.tbl_hudi_didi limit 10 ;

显示结果如下所示:
在这里插入图片描述

4.2.5.2 HiveQL 分析

在Hive框架beeline命令行中编写HiveQL语句,对前面5.4节指标统计分析。

# 设置Hive本地模式
set hive.exec.mode.local.auto=true;

set hive.exec.mode.local.auto.tasks.max=10;
set hive.exec.mode.local.auto.inputbytes.max=50000000;

指标一:订单类型统计

WITH tmp AS (
  SELECT product_id, COUNT(1) AS total FROM db_hudi.tbl_hudi_didi GROUP BY product_id
)
SELECT 
  CASE product_id
    WHEN 1 THEN "滴滴专车"
    WHEN 2 THEN "滴滴企业专车"
    WHEN 3 THEN "滴滴快车"
    WHEN 4 THEN "滴滴企业快车"
  END AS order_type,
  total
FROM tmp ;

分析结果(仅仅导入小部分滴滴出行数据至Hudi表),如下图所示:
在这里插入图片描述

指标二:订单时效性统计

WITH tmp AS (
  SELECT type AS order_realtime, COUNT(1) AS total FROM db_hudi.tbl_hudi_didi GROUP BY type
)
SELECT 
  CASE order_realtime
    WHEN 0 THEN "实时"
    WHEN 1 THEN "预约"
  END AS order_realtime,
  total
FROM tmp ;

分析结果(仅仅导入小部分滴滴出行数据至Hudi表),如下图所示:
在这里插入图片描述

指标三:订单交通类型统计

WITH tmp AS (
  SELECT traffic_type, COUNT(1) AS total FROM db_hudi.tbl_hudi_didi GROUP BY traffic_type
)
SELECT 
  CASE traffic_type
   WHEN 0 THEN  "普通散客" 
   WHEN 1 THEN  "企业时租"
   WHEN 2 THEN  "企业接机套餐"
   WHEN 3 THEN  "企业送机套餐"
   WHEN 4 THEN  "拼车"
   WHEN 5 THEN  "接机"
   WHEN 6 THEN  "送机"
   WHEN 302 THEN  "跨城拼车"
   ELSE "未知"
  END AS traffic_type,
  total
FROM tmp ;

分析结果(仅仅导入小部分滴滴出行数据至Hudi表),如下图所示:
在这里插入图片描述

指标四:订单价格统计

SELECT 
  SUM(
    CASE WHEN pre_total_fee BETWEEN 1 AND 15 THEN 1 ELSE 0 END
  ) AS 0_15,
  SUM(
    CASE WHEN pre_total_fee BETWEEN 16 AND 30 THEN 1 ELSE 0 END
  ) AS 16_30,
  SUM(
    CASE WHEN pre_total_fee BETWEEN 31 AND 50 THEN 1 ELSE 0 END
  ) AS 31_150,
  SUM(
    CASE WHEN pre_total_fee BETWEEN 51 AND 100 THEN 1 ELSE 0 END
  ) AS 51_100,
  SUM(
    CASE WHEN pre_total_fee > 100 THEN 1 ELSE 0 END
  )  AS 100_
FROM 
  db_hudi.tbl_hudi_didi;

分析结果(仅仅导入小部分滴滴出行数据至Hudi表),如下图所示:
在这里插入图片描述

4.3 结构化流写入Hudi

整合Spark StructuredStreaming与Hudi,实时将流式数据写入Hudi表中,对每批次数据batch DataFrame,采用Spark DataSource方式写入数据。
在这里插入图片描述
属性参数说明:https://hudi.apache.org/docs/writing_data#datasource-writer

4.3.1 模拟交易订单

编程模拟生成交易订单数据,实时发送Kafka Topic,为了简单起见交易订单数据字段如下,封装到样例类OrderRecord中:

/**
 * 订单实体类(Case Class)
 *
 * @param orderId     订单ID
 * @param userId      用户ID
 * @param orderTime   订单日期时间
 * @param ip          下单IP地址
 * @param orderMoney  订单金额
 * @param orderStatus 订单状态
 */
case class OrderRecord(
                         orderId: String,
                         userId: String,
                         orderTime: String,
                         ip: String,
                         orderMoney: Double,
                         orderStatus: Int
                      )

编写程序【MockOrderProducer】,实时产生交易订单数据,使用Json4J类库转换数据为JSON字符,发送Kafka Topic中,代码如下:

import java.util.Properties

import org.apache.commons.lang3.time.FastDateFormat
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}
import org.apache.kafka.common.serialization.StringSerializer
import org.json4s.jackson.Json

import scala.util.Random

/**
 * 模拟生产订单数据,发送到Kafka Topic中
 *      Topic中每条数据Message类型为String,以JSON格式数据发送
 * 数据转换:
 *      将Order类实例对象转换为JSON格式字符串数据(可以使用json4s类库)
 */
object MockOrderProducer {
   
   def main(args: Array[String]): Unit = {
      
      var producer: KafkaProducer[String, String] = null
      try {
         // 1. Kafka Client Producer 配置信息
         val props = new Properties()
         props.put("bootstrap.servers", "node1.itcast.cn:9092")
         props.put("acks", "1")
         props.put("retries", "3")
         props.put("key.serializer", classOf[StringSerializer].getName)
         props.put("value.serializer", classOf[StringSerializer].getName)
         
         // 2. 创建KafkaProducer对象,传入配置信息
         producer = new KafkaProducer[String, String](props)
         
         // 随机数实例对象
         val random: Random = new Random()
         // 订单状态:订单打开 0,订单取消 1,订单关闭 2,订单完成 3
         val allStatus = Array(0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
         
         while (true) {
            // 每次循环 模拟产生的订单数目
            val batchNumber: Int = random.nextInt(1) + 5
            (1 to batchNumber).foreach { number =>
               val currentTime: Long = System.currentTimeMillis()
               val orderId: String = s"${getDate(currentTime)}%06d".format(number)
               val userId: String = s"${1 + random.nextInt(5)}%08d".format(random.nextInt(1000))
               val orderTime: String = getDate(currentTime, format = "yyyy-MM-dd HH:mm:ss.SSS")
               val orderMoney: String = s"${5 + random.nextInt(500)}.%02d".format(random.nextInt(100))
               val orderStatus: Int = allStatus(random.nextInt(allStatus.length))
               // 3. 订单记录数据
               val orderRecord: OrderRecord = OrderRecord(
                  orderId, userId, orderTime, getRandomIp, orderMoney.toDouble, orderStatus
               )
               // 转换为JSON格式数据
               val orderJson = new Json(org.json4s.DefaultFormats).write(orderRecord)
               println(orderJson)
               // 4. 构建ProducerRecord对象
               val record = new ProducerRecord[String, String]("order-topic", orderId, orderJson)
               // 5. 发送数据:def send(messages: KeyedMessage[K,V]*), 将数据发送到Topic
               producer.send(record)
            }
            Thread.sleep(random.nextInt(500))
         }
      } catch {
         case e: Exception => e.printStackTrace()
      } finally {
         if (null != producer) producer.close()
      }
   }
   
   /** =================获取当前时间================= */
   def getDate(time: Long, format: String = "yyyyMMddHHmmssSSS"): String = {
      val fastFormat: FastDateFormat = FastDateFormat.getInstance(format)
      val formatDate: String = fastFormat.format(time) // 格式化日期
      formatDate
   }
   
   /** ================= 获取随机IP地址 ================= */
   def getRandomIp: String = {
      // ip范围
      val range: Array[(Int, Int)] = Array(
         (607649792, 608174079), //36.56.0.0-36.63.255.255
         (1038614528, 1039007743), //61.232.0.0-61.237.255.255
         (1783627776, 1784676351), //106.80.0.0-106.95.255.255
         (2035023872, 2035154943), //121.76.0.0-121.77.255.255
         (2078801920, 2079064063), //123.232.0.0-123.235.255.255
         (-1950089216, -1948778497), //139.196.0.0-139.215.255.255
         (-1425539072, -1425014785), //171.8.0.0-171.15.255.255
         (-1236271104, -1235419137), //182.80.0.0-182.92.255.255
         (-770113536, -768606209), //210.25.0.0-210.47.255.255
         (-569376768, -564133889) //222.16.0.0-222.95.255.255
      )
      // 随机数:IP地址范围下标
      val random = new Random()
      val index = random.nextInt(10)
      val ipNumber: Int = range(index)._1 + random.nextInt(range(index)._2 - range(index)._1)
      
      // 转换Int类型IP地址为IPv4格式
      number2IpString(ipNumber)
   }
   
   /** =================将Int类型IPv4地址转换为字符串类型================= */
   def number2IpString(ip: Int): String = {
      val buffer: Array[Int] = new Array[Int](4)
      buffer(0) = (ip >> 24) & 0xff
      buffer(1) = (ip >> 16) & 0xff
      buffer(2) = (ip >> 8) & 0xff
      buffer(3) = ip & 0xff
      // 返回IPv4地址
      buffer.mkString(".")
   }
   
}

运行应用程序,模拟生成交易订单数据,格式化后:
在这里插入图片描述

4.3.2 流式程序开发

编写Structured Streaming Application应用:HudiStructuredDemo,实时从Kafka的【order-topic】消费JSON格式数据,经过ETL转换后,存储到Hudi表中。

package cn.itcast.hudi.streaming

import org.apache.spark.internal.Logging
import org.apache.spark.sql._
import org.apache.spark.sql.functions._
import org.apache.spark.sql.streaming.OutputMode

/**
 * 基于StructuredStreaming结构化流实时从Kafka消费数据,经过ETL转换后,存储至Hudi表
 */
object HudiStructuredDemo extends Logging{
   
   def main(args: Array[String]): Unit = {
      // step1、构建SparkSession实例对象
      val spark: SparkSession = createSparkSession(this.getClass)
      
      // step2、从Kafka实时消费数据
      val kafkaStreamDF: DataFrame = readFromKafka(spark, "order-topic")
      
      // step3、提取数据,转换数据类型
      val streamDF: DataFrame = process(kafkaStreamDF)
      
      // step4、保存数据至Hudi表中:COW(写入时拷贝)和MOR(读取时保存)
      saveToHudi(streamDF)
      
      // step5、流式应用启动以后,等待终止
      spark.streams.active.foreach(query => println(s"Query: ${query.name} is Running ............."))
      spark.streams.awaitAnyTermination()
   }
   
   /**
    * 创建SparkSession会话实例对象,基本属性设置
    */
   def createSparkSession(clazz: Class[_]): SparkSession = {
      SparkSession.builder()
         .appName(this.getClass.getSimpleName.stripSuffix("$"))
         .master("local[2]")
         // 设置序列化方式:Kryo
         .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
         // 设置属性:Shuffle时分区数和并行度
         .config("spark.default.parallelism", 2)
         .config("spark.sql.shuffle.partitions", 2)
         .getOrCreate()
   }
   
   /**
    * 指定Kafka Topic名称,实时消费数据
    */
   def readFromKafka(spark: SparkSession, topicName: String): DataFrame = {
      spark
         .readStream
         .format("kafka")
         .option("kafka.bootstrap.servers", "node1.itcast.cn:9092")
         .option("subscribe", topicName)
         .option("startingOffsets", "latest")
         .option("maxOffsetsPerTrigger", 100000)
         .option("failOnDataLoss", "false")
          .load()
   }
   
   /**
    * 对Kafka获取数据,进行转换操作,获取所有字段的值,转换为String,以便保存Hudi表
    */
   def process(streamDF: DataFrame): DataFrame = {
      /* 从Kafka消费数据后,字段信息如
         key -> binary,value -> binary
         topic -> string, partition -> int, offset -> long
         timestamp -> long, timestampType -> int
       */
      streamDF
         // 选择字段,转换类型为String
         .selectExpr(
            "CAST(key AS STRING) order_id", //
            "CAST(value AS STRING) message", //
            "topic", "partition", "offset", "timestamp"//
         )
         // 解析Message,提取字段内置
          .withColumn("user_id", get_json_object(col("message"), "$.userId"))
          .withColumn("order_time", get_json_object(col("message"), "$.orderTime"))
          .withColumn("ip", get_json_object(col("message"), "$.ip"))
          .withColumn("order_money", get_json_object(col("message"), "$.orderMoney"))
          .withColumn("order_status", get_json_object(col("message"), "$.orderStatus"))
         // 删除Message列
          .drop(col("message"))
         // 转换订单日期时间格式为Long类型,作为Hudi表中合并数据字段
          .withColumn("ts", to_timestamp(col("order_time"), "yyyy-MM-dd HH:mm:ss.SSSS"))
          // 订单日期时间提取分区日期:yyyyMMdd
          .withColumn("day", substring(col("order_time"), 0, 10))
   }
   
   /**
    * 将流式数据集DataFrame保存至Hudi表,分别表类型:COW和MOR
    */
   def saveToHudi(streamDF: DataFrame): Unit = {
      streamDF.writeStream
         .outputMode(OutputMode.Append())
          .queryName("query-hudi-streaming")
         // 针对每微批次数据保存
          .foreachBatch((batchDF: Dataset[Row], batchId: Long) => {
            println(s"============== BatchId: ${batchId} start ==============")
             writeHudiMor(batchDF) // TODO:表的类型MOR
          })
         .option("checkpointLocation", "/datas/hudi-spark/struct-ckpt-100")
          .start()
   }
   
   /**
    * 将数据集DataFrame保存到Hudi表中,表的类型:MOR(读取时合并)
    */
   def writeHudiMor(dataframe: DataFrame): Unit = {
      import org.apache.hudi.DataSourceWriteOptions._
      import org.apache.hudi.config.HoodieWriteConfig._
      import org.apache.hudi.keygen.constant.KeyGeneratorOptions._
      
      dataframe.write
         .format("hudi")
         .mode(SaveMode.Append)
         // 表的名称
         .option(TBL_NAME.key, "tbl_kafka_mor")
         // 设置表的类型
         .option(TABLE_TYPE.key(), "MERGE_ON_READ")
         // 每条数据主键字段名称
         .option(RECORDKEY_FIELD_NAME.key(), "order_id")
         // 数据合并时,依据时间字段
         .option(PRECOMBINE_FIELD_NAME.key(), "ts")
         // 分区字段名称
         .option(PARTITIONPATH_FIELD_NAME.key(), "day")
         // 分区值对应目录格式,是否与Hive分区策略一致
         .option(HIVE_STYLE_PARTITIONING_ENABLE.key(), "true")
         // 插入数据,产生shuffle时,分区数目
         .option("hoodie.insert.shuffle.parallelism", "2")
         .option("hoodie.upsert.shuffle.parallelism", "2")
         // 表数据存储路径
         .save("/hudi-warehouse/tbl_order_mor")
   }
   
}

上述代码中有两个细节,对于流式应用来说很关键:

  • 第一、从Kafka消费数据时,通过属性【maxOffsetsPerTrigger】,设置每批次最大数据量,实际生产项目需要结合流式数据波峰及应用运行资源综合考虑设置;
  • 第二、将ETL后数据保存至Hudi中,设置检查点位置Checkpoint Location,便于流式应用运行失败后,可以从Checkpoint恢复,继续上次消费数据,进行实时处理;

运行上述程序,查看HDFS上Hudi表存储交易订单数据存储目录结构:
在这里插入图片描述

4.3.3 Spark 查询分析

启动spark-shell命令行,查询Hudi表存储交易订单数据,命令如下:

spark-shell \
--master local[2] \
--jars /root/hudi-jars/hudi-spark3-bundle_2.12-0.9.0.jar,\
/root/hudi-jars/spark-avro_2.12-3.0.1.jar,/root/hudi-jars/spark_unused-1.0.0.jar \
--conf "spark.serializer=org.apache.spark.serializer.KryoSerializer"  

指定Hudi表数据存储目录,加载数据:

val orderDF = spark.read.format("hudi").load("/hudi-warehouse/tbl_order_mor")

查看Schema信息

orderDF.printSchema()

在这里插入图片描述

查看订单表前10条数据,选择订单相关字段:

orderDF.select("order_id", "user_id", "order_time", "ip", "order_money", "order_status", "day").show(false)

在这里插入图片描述

查看数据总条目数:

orderDF.count()

交易订单数据基本聚合统计:最大金额max、最小金额min、平均金额avg

spark.sql("""
  with tmp AS (
    SELECT CAST(order_money AS DOUBLE) FROM view_tmp_order WHERE order_status = '0'
  )
  select 
    max(order_money) as max_money, 
    min(order_money) as min_money, 
    round(avg(order_money), 2) as avg_money 
  from tmp 
""").show()

在这里插入图片描述

4.3.4 DeltaStreamer 工具类

HoodieDeltaStreamer工具 (hudi-utilities-bundle中的一部分) 提供了从DFS或Kafka等不同来源进行摄取的方式,并具有以下功能:

  • 从Kafka单次摄取新事件
  • 支持json、avro或自定义记录类型的传入数据
  • 管理检查点,回滚和恢复
  • 利用DFS或Confluent schema注册表的Avro模式
  • 支持自定义转换操作
    工具类:HoodieDeltaStreamer,本质上运行Spark 流式程序,实时从获取数据,存储奥Hudi表中,执行如下命令,查看帮助文档:
spark-submit --master local[2] \
--class org.apache.hudi.utilities.deltastreamer.HoodieDeltaStreamer \
/root/hudi-utilities-bundle_2.11-0.9.0.jar \
--help

注意:工具类所在jar包【hudi-utilities-bundle_2.11-0.9.0.jar】,将其添加CLASSPATH。
官方提供案例:实时消费Kafka中数据,数据格式为Avro,将其存储到Hudi表。
在这里插入图片描述

4.4 集成 SparkSQL

在Hudi最新版本0.9.0支持与SparkSQL集成,直接在spark-sql交互式命令行编写SQL语句,极大方便用户对Hudi表的DDL/DML操作。文档:https://hudi.apache.org/docs/quick-start-guide
在这里插入图片描述

4.4.1 启动spark-sql

Hudi表数据存储在HDFS文件系统,先启动NameNode和DataNode服务。

[root@node1 ~]# hadoop-daemon.sh start namenode
[root@node1 ~]# hadoop-daemon.sh start datanode

启动spark-sql交互式命令行,设置依赖jar包和相关属性参数。

spark-sql \
--master local[2] \
--jars /root/hudi-jars/hudi-spark3-bundle_2.12-0.9.0.jar,\
/root/hudi-jars/spark-avro_2.12-3.0.1.jar,/root/hudi-jars/spark_unused-1.0.0.jar \
--conf 'spark.serializer=org.apache.spark.serializer.KryoSerializer' \
--conf 'spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension'

Hudi默认upsert/insert/delete的并发度是1500,对于演示小规模数据集设置更小的并发度。

set hoodie.upsert.shuffle.parallelism = 1;
set hoodie.insert.shuffle.parallelism = 1;
set hoodie.delete.shuffle.parallelism = 1;

设置不同步Hudi表元数据:

set hoodie.datasource.meta.sync.enable=false;

4.4.2 快速入门

使用DDL和DML语句,创建表、删除表和对数据CURD操作。

4.4.2.1 创建表

编写DDL语句,创建Hudi表,表的类型:MOR和分区表,主键为id,分区字段为dt,合并字段默认为ts

create table test_hudi_table (
  id int,
  name string,
  price double,
  ts long,
  dt string
) using hudi
 partitioned by (dt)
 options (
  primaryKey = 'id',
  type = 'mor'
 )
location 'hdfs://node1.itcast.cn:8020/hudi-warehouse/test_hudi_table' ;

创建Hudi表后查看创建的Hudi表

show create table test_hudi_table 

在这里插入图片描述

4.4.2.2 插入数据

使用INSERT INTO 插入数据到Hudi表中:

insert into test_hudi_table select 1 as id, 'hudi' as name, 10 as price, 1000 as ts, '2021-11-01' as dt;

insert完成后查看Hudi表本地目录结构,生成的元数据、分区和数据与Spark Datasource写入均相同。
在这里插入图片描述

使用ISNERT INTO语句,多插入几条数据,命令如下:

insert into test_hudi_table select 2 as id, 'spark' as name, 20 as price, 1100 as ts, '2021-11-01' as dt;
insert into test_hudi_table select 3 as id, 'flink' as name, 30 as price, 1200 as ts, '2021-11-01' as dt;
insert into test_hudi_table select 4 as id, 'sql' as name, 40 as price, 1400 as ts, '2021-11-01' as dt;

4.4.2.3 查询数据

使用SQL查询Hudi表数据,全表扫描查询:

select * from test_hudi_table ;

在这里插入图片描述

查看表中字段结构,使用DESC语句:

desc test_hudi_table ;

在这里插入图片描述

指定查询字段,查询表中前几天数据:

SELECT _hoodie_record_key,_hoodie_partition_path, id, name, price, ts, dt FROM test_hudi_table ;

在这里插入图片描述

4.4.2.4 更新数据

使用update语句,更新id=1数据中price为100,语句如下:

update test_hudi_table set price = 100.0 where id = 1 ;

再次查询Hudi表数据,查看数据是否更新:

SELECT id, name, price, ts, dt FROM test_hudi_table WHERE id = 1;

在这里插入图片描述

4.4.2.5 删除数据

使用DELETE语句,将id=1的记录删除,命令如下:

delete from test_hudi_table where id = 1 ;

再次查询Hudi表数据,查看数据是否更新:

SELECT COUNT(1) AS total from test_hudi_table WHERE id = 1;

查询结果如下,可以看到已经查询不到任何数据了,表明Hudi表中已经不存在任何记录了。
在这里插入图片描述

4.4.3 DDL 创建表

在spark-sql中编写DDL语句,创建Hudi表数据,核心三个属性参数:
在这里插入图片描述

  • 指定Hudi表的类型:
    在这里插入图片描述

官方案例:创建COW类型Hudi表。
在这里插入图片描述

  • 管理表与外部表:创建表时,指定location存储路径,表就是外部表
    在这里插入图片描述

  • 创建表时设置为分区表:partitioned table
    在这里插入图片描述

  • 支持使用CTAS:Create table as select方式创建表
    在这里插入图片描述

在实际应用使用时,合理选择创建表的方式,建议创建外部及分区表,便于数据管理和安全。

4.4.4 MergeInto 语句

在Hudi中提供MergeInto语句,依据判断条件,决定对数据操作时,属于插入insert、更新update,还是删除delete,语法如下:
在这里插入图片描述

4.4.4.1 Merge Into Insert

当不满足条件时(关联条件不匹配),插入数据到Hudi表中

merge into test_hudi_table as t0
using (
 select 1 as id, 'hadoop' as name, 1 as price, 9000 as ts, '2021-11-02' as dt
) as s0
on t0.id = s0.id
when not matched then insert * ;

查询Hudi表数据,可以看到Hudi表中存在一条记录
在这里插入图片描述

4.4.4.2 Merge Into Update

当满足条件时(关联条件匹配),对数据进行更新操作:

merge into test_hudi_table as t0
using (
 select 1 as id, 'hadoop3' as name, 1000 as price, 9999 as ts, '2021-11-02' as dt
) as s0
on t0.id = s0.id
when matched then update set *

查询Hudi表,可以看到Hudi表中的分区已经更新
在这里插入图片描述

4.4.4.3 Merge Into Delete

当满足条件时(关联条件匹配),对数据进行删除操作:

merge into test_hudi_table t0
using (
 select 1 as s_id, 'hadoop3' as s_name, 8888 as s_price, 9999 as s_ts, '2021-11-02' as dt
) s0
on t0.id = s0.s_id
when matched and s_ts = 9999 then delete

查询结果如下,可以看到Hudi表中已经没有数据。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/866665.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

读《Flask Web开发实战》(狼书)笔记 | 第1、2章

前言 2023-8-11 以前对网站开发萌生了想法&#xff0c;又有些急于求成&#xff0c;在B站照着视频敲了一个基于flask的博客系统。但对于程序的代码难免有些囫囵吞枣&#xff0c;存在许多模糊或不太理解的地方&#xff0c;只会照葫芦画瓢。 而当自己想开发一个什么网站的时&…

限流在不同场景的最佳实践

目录导读 限流在不同场景的最佳实践1. 前言2. 为什么要限流3. 有哪些限流场景3.1 限流场景分类3.2 限流与熔断降级之间的关系3.3 非业务限流3.4 业务限流 4. 有哪些限流算法4.1 计数器限流算法4.2 漏桶限流算法4.3 令牌桶限流算法4.4 滑动时间窗限流算法4.5 限流算法选型 5. 限…

【数据结构与算法】稀疏数组

文章目录 一&#xff1a;为什么会使用稀疏数组1.1 先看一个实际的需求1.2 基本介绍1.2.1 稀疏数组的处理方法1.2.2 数组的举例说明1.2.3 应用实例1.2.4 整体思路分析二维数组转稀疏数组的思路稀疏数组转原始的二维数组的思路 二&#xff1a;代码实现2.1 创建一个原始的11*11二维…

​LeetCode解法汇总1572. 矩阵对角线元素的和

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 描述&#xff1a; 给你一个正…

探秘金和OA:解析任意文件读取漏洞的潜在威胁

是喜是悲&#xff0c;但可以慰藉的是&#xff0c;你总不枉在这世界上活了一场&#xff0c;有了这样的认识&#xff0c;你就会珍重生活&#xff0c;而不会玩世不恭&#xff1b;同时也会给人自身注入一种强大的内在力量…… 漏洞复现 访问url&#xff1a; 构造payload /C6/Jh…

【网络编程(二)】NIO快速入门

NIO Java NIO 三大核心组件 Buffer&#xff08;缓冲区&#xff09;&#xff1a;每个客户端连接都会对应一个Buffer&#xff0c;读写数据通过缓冲区读写。Channel&#xff08;通道&#xff09;&#xff1a;每个channel用于连接Buffer和Selector&#xff0c;通道可以进行双向读…

日常问题——使用Java转将long类型为date类型,日期是1970年

&#x1f61c;作 者&#xff1a;是江迪呀✒️本文关键词&#xff1a;日常BUG、BUG、问题分析☀️每日 一言 &#xff1a;存在错误说明你在进步&#xff01; 一、问题描述 long类型的日期为&#xff1a;1646718195 装换为date类型&#xff1a; Date date new Dat…

SQL | 使用函数处理数据

8-使用函数处理数据 8.1-函数 SQL可以用函数来处理数据。函数一般是在数据上执行的&#xff0c;为数据的转换和处理提供了方便。 8.1.1 函数带来的问题 每种DBMS都有特定的函数&#xff0c;只有很少一部分函数&#xff0c;是被所有主要的DBMS等同的支持。 虽然所有的类型的…

Linux 基础(五)常用命令-文件属性

文件属性 文件权限文件属性修改文件权限属性 文件所有者 文件权限 文件属性 Linux中文件权限 可以通过文件属性体现&#xff1b; 使用 ll 查看文件列表 最前面的 l d 表示文件类型 1 5 表示硬链接数 或者 子文件夹个数 所属用户 所属用户组 文件大小 创建/更新时间 文件&…

【前端二次开发框架关于关闭eslint】

前端二次开发框架关于关闭eslint 方法一方法二方法三方法四&#xff1a;以下是若想要关闭项目中的部分代码时&#xff1a; 方法一 在vue.config.js里面进行配置&#xff1a; module.exports {lintOnSave:false,//是否开启eslint保存检测 ,它的有效值为 true || false || err…

一个简单实用的线程池及线程池组的实现!

1.线程池简介 线程池&#xff0c;顾名思义&#xff0c;就是一个“池子”里面放有多个线程。为什么要使用线程池呢&#xff1f;当我们编写的代码需要并发异步处理很多任务时候&#xff0c;一般的处理办法是一个任务开启一个线程去处理&#xff0c;处理结束后释放线程。可是这样…

Docker安装 Kibana

目录 前言安装Kibana步骤1&#xff1a;准备1. 安装docker2. 搜索可以使用的镜像。3. 也可从docker hub上搜索镜像。4. 选择合适的redis镜像。 步骤2&#xff1a;拉取 kibana 镜像拉取镜像查看已拉取的镜像 步骤3&#xff1a;创建容器创建容器方式1&#xff1a;快速创建容器 步骤…

vue父页面给iframe子页面传值

在vue父页面有两个个参数 名称和图标&#xff0c;需要把这两个参数传到iframe的地图里面&#xff0c;在地图触发绘点事件的时候&#xff0c;获取到传来的参数并且展示 vue:传值给子页面iframe // 传值给子页面iframe(2个参数)handleIframeLoad() {const iframeWindow this.$re…

海康威视iVMS综合安防系统任意文件上传(0Day)

漏洞描述 攻击者通过请求/svm/api/external/report接口任意上传文件,导致获取服务器webshell权限,同时可远程进行恶意代码执行。 免责声明 技术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律,遵守公共秩序,尊重社会公德,不得利用网络从事危害国家安全、荣誉和…

安全第二次

一&#xff0c;iframe <iframe>标签用于在网页里面嵌入其他网页。 1&#xff0c;sandbox属性 如果嵌入的网页是其他网站的页面&#xff0c;因不了解对方会执行什么操作&#xff0c;因此就存在安全风险。为了限制<iframe>的风险&#xff0c;HTML 提供了sandb…

HCIA---动态路由---RIP协议

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 目录 前言 一.动态路由 二.动态路由协议分类 IGP&#xff1a;内部网关协议 EGP:外部网关协议 三.RIP协议概述 RIP版本分类&#xff1a; RIP三要素&#xff1a; 思维…

TypeScript 关于对【泛型】的定义使用解读

目录 概念导读泛型函数多个泛型参数泛型约束泛型别名泛型接口泛型类总结&#xff1a; 概念导读 泛型&#xff08;Generics&#xff09;是指在定义函数、接口或类的时候&#xff0c;不预先指定具体的类型&#xff0c;而在使用的时候再指定类型的一种特性。使用泛型 可以复用类型…

【非科班如何丝滑转码?】探索计算机领域跳槽之路

近年来&#xff0c;计算机领域的蓬勃发展吸引着越来越多非计算机科班出身的人士投身其中。本文将就如何顺利实现非科班转码&#xff0c;计算机岗位的发展前景&#xff0c;以及现阶段转码的建议&#xff0c;结合个人经验和观察&#xff0c;为您阐述详细全面的观点。 一、如何规划…

【雕爷学编程】Arduino动手做(201)---行空板开发环境之VSCode

37款传感器与模块的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&#x…

Kotlin 基础教程一

Kotlin 基本数据类型 Java | Kotlin byte Byte short Short int Int long Long float Float double Double boolean Boolean c…