通过Spark操作Hudi(增、删、改、查、增量查)

一、概览

Hudi数据湖框架,基于spark计算引擎,对数据进行CRUD操作,使用官方模拟生成出租车出行数据
 任务一:模拟数据,插入Hudi表,采用COW模式
 任务二:快照方式查询(Snapshot Query),采用DSL方式
 任务三:更新(update)数据
 任务四:增量查询数据(Incremental Query),采用SQL方式
 任务五:删除(Delete)数据

 

二、cow类型表的操作

1.写入

  写入数据时,如果该rowkey已经存在,则复制原字段,会保留历史版本

2.查询

2.1快照查询Snapshot[默认]

默认加载最近一个版本的数据,通过参数 hoodie.datasource.query.type可以进行设置

val QUERY_TYPE_OPT_KEY = "hoodie.datasource.query.type"
val QUERY_TYPE_SNAPSHOT_OPT_VAL = "snapshot"
val QUERY_TYPE_READ_OPTIMIZED_OPT_VAL = "read_optimized"
val QUERY_TYPE_INCREMENTAL_OPT_VAL = "incremental"
val DEFAULT_QUERY_TYPE_OPT_VAL: String = QUERY_TYPE_SNAPSHOT_OPT_VAL

2.2增量查询Incremental

指定一个时间戳,查询时间戳之后的增量数据,如果是incremental增量查询,需要指定时间戳,当hudi表中数据满足:instant_time > beginTime时,数据将会被读取;此外,可设置某个时间范围:endTime > instant_time > beginTime

val BEGIN_INSTANTTIME_OPT_KEY = "hoodie.datasource.read.begin.instanttime"
val END_INSTANTTIME_OPT_KEY = "hoodie.datasource.read.end.instanttime"

3.删除数据

需要设置属性参数:hoodie.datasource.write.operation : delete

val OPERATION_OPT_KEY = "hoodie.datasource.write.operation"
val BULK_INSERT_OPERATION_OPT_VAL = WriteOperationType.BULK_INSERT.value
val INSERT_OPERATION_OPT_VAL = WriteOperationType.INSERT.value
val UPSERT_OPERATION_OPT_VAL = WriteOperationType.UPSERT.value
val DELETE_OPERATION_OPT_VAL = WriteOperationType.DELETE.value
val BOOTSTRAP_OPERATION_OPT_VAL = WriteOperationType.BOOTSTRAP.value
val INSERT_OVERWRITE_OPERATION_OPT_VAL = WriteOperationType.INSERT_OVERWRITE.value
val INSERT_OVERWRITE_TABLE_OPERATION_OPT_VAL = WriteOperationType.INSERT_OVERWRITE_TABLE.value
val DEFAULT_OPERATION_OPT_VAL = UPSERT_OPERATION_OPT_VAL

 

三、参考代码

package com.zhen.hudi.spark

import org.apache.hudi.QuickstartUtils.DataGenerator
import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}


/**
  * @Author FengZhen
  * @Date 2/17/22 9:09 PM 
  * @Description
  *  Hudi数据湖框架,基于spark计算引擎,对数据进行CRUD操作,使用官方模拟生成出租车出行数据
  *  任务一:模拟数据,插入Hudi表,采用COW模式
  *  任务二:快照方式查询(Snapshot Query),采用DSL方式
  *  任务三:更新(update)数据
  *  任务四:增量查询数据(Incremental Query),采用SQL方式
  *  任务五:删除(Delete)数据
  *
  *  cow类型表
  *   写入数据时,如果该rowkey已经存在,则复制原字段
  *   查询
  *     快照查询Snapshot[默认]:
  *       默认加载最近一个版本的数据
  *       通过参数 hoodie.datasource.query.type可以进行设置
  *       val QUERY_TYPE_OPT_KEY = "hoodie.datasource.query.type"
  *       val QUERY_TYPE_SNAPSHOT_OPT_VAL = "snapshot"
  *       val QUERY_TYPE_READ_OPTIMIZED_OPT_VAL = "read_optimized"
  *       val QUERY_TYPE_INCREMENTAL_OPT_VAL = "incremental"
  *       val DEFAULT_QUERY_TYPE_OPT_VAL: String = QUERY_TYPE_SNAPSHOT_OPT_VAL
  *     增量查询Incremental:
  *       指定一个时间戳,查询时间戳之后的增量数据
  *       如果是incremental增量查询,需要指定时间戳,
  *       当hudi表中数据满足:instant_time > beginTime时,数据将会被夹在读取
  *       此外,可设置某个时间范围:endTime > instant_time > beginTime
  *       val BEGIN_INSTANTTIME_OPT_KEY = "hoodie.datasource.read.begin.instanttime"
  *       val END_INSTANTTIME_OPT_KEY = "hoodie.datasource.read.end.instanttime"
  *
  *   删除数据
  *     需要设置属性参数:hoodie.datasource.write.operation : delete
  *     val OPERATION_OPT_KEY = "hoodie.datasource.write.operation"
  *     val BULK_INSERT_OPERATION_OPT_VAL = WriteOperationType.BULK_INSERT.value
  *     val INSERT_OPERATION_OPT_VAL = WriteOperationType.INSERT.value
  *     val UPSERT_OPERATION_OPT_VAL = WriteOperationType.UPSERT.value
  *     val DELETE_OPERATION_OPT_VAL = WriteOperationType.DELETE.value
  *     val BOOTSTRAP_OPERATION_OPT_VAL = WriteOperationType.BOOTSTRAP.value
  *     val INSERT_OVERWRITE_OPERATION_OPT_VAL = WriteOperationType.INSERT_OVERWRITE.value
  *     val INSERT_OVERWRITE_TABLE_OPERATION_OPT_VAL = WriteOperationType.INSERT_OVERWRITE_TABLE.value
  *     val DEFAULT_OPERATION_OPT_VAL = UPSERT_OPERATION_OPT_VAL
  *
  *
  */
object HudiSparkDemo {


  def main(args: Array[String]): Unit = {

    //创建SparkSession实例对象,设置属性
    val spark: SparkSession = {
      SparkSession.builder()
        .appName(this.getClass.getSimpleName.stripSuffix("$"))
        .master("local[2]")
        //设置序列化方式:Kryo
        .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
        .getOrCreate()
    }
    
    //定义变量,表名称、保存路径
    val tableName = "tbl_trips_cow"
    val tablePath = "/hudi-warehouse/tbl_trips_cow"
    

    //任务一:模拟数据,插入Hudi表,采用COW模式
//    insertData(spark, tableName, tablePath)

    //任务二:快照方式查询(Snapshot Query)数据,采用DSL方式
    queryData(spark, tablePath)
    //根据时间查询数据
//    queryDataByTime(spark, tablePath)

    /**
      * 任务三:更新数据update。
      *   第一步:模拟产生数据;
      *   第二步:模拟产生数据,针对第一步数据字段值更新;
      *   第三步:将数据更新到hudi表
      * hudi数据湖框架最大优势就是支持对数据的Upsert操作(插入或更新)
      */
//    val dataGen : DataGenerator = new DataGenerator()
//    insertData(spark, tableName, tablePath, dataGen)
//    updateData(spark, tableName, tablePath, dataGen)

    //任务四:增量查询数据(Incremental Query),采用SQL方式
//    incrementalQueryData(spark, tablePath)

    //任务五:删除(Delete)数据
    deleteData(spark, tableName, tablePath)


    //应用结束,关闭资源
    spark.stop()
  }

  /**
    * 官方案例:模拟产生数据,插入Hudi表,采用COW模式
    * @param spark
    * @param table
    * @param path
    */
  def insertData(spark: SparkSession, table: String, path: String): Unit = {
    import spark.implicits._
    //1.模拟乘车数据
    //构建数据生成器,模拟产生业务数据
    import org.apache.hudi.QuickstartUtils._
    val dataGen : DataGenerator = new DataGenerator()
    //模拟生成100条,并转成String类型
    val inserts = convertToStringList(dataGen.generateInserts(100))

    import scala.collection.JavaConverters._
    val insertDF =spark.read.json(
      spark.sparkContext.parallelize(inserts.asScala, 2).toDS()
    )

    insertDF.printSchema()
    insertDF.show(10, false)

    //2.插入数据到Hudi表
    import org.apache.hudi.DataSourceWriteOptions._
    import org.apache.hudi.config.HoodieWriteConfig._
    insertDF.write
      .mode(SaveMode.Append)
      .format("hudi")
      .option("hoodie.insert.shuffle.parallelism", "2")
      .option("hoodie.upsert.shuffle.parallelism", "2")
      //hudi表的属性值的设置
      //预合并
      .option(PRECOMBINE_FIELD.key(), "ts")
      //主键
      .option(RECORDKEY_FIELD.key(), "uuid")
      //分区
      .option(PARTITIONPATH_FIELD.key(), "partitionpath")
      //表名
      .option(TBL_NAME.key(), table)
      .save(path)

  }

  /**
    * 采用Snapshot Query快照方式查询表的数据
    * @param spark
    * @param path
    * @return
    */
  def queryData(spark: SparkSession, path: String): Unit= {
    import spark.implicits._
    val tripsDF = spark.read.format("hudi").load(path)
//    tripsDF.printSchema()
//    tripsDF.show(10, false)

    //查询费用大于20,小于50的乘车数据
    tripsDF
      .filter($"fare" >= 20 && $"fare" <= 50)
      .select($"driver", $"rider", $"fare", $"begin_lat", $"begin_lon", $"partitionpath", $"_hoodie_commit_time")
      .orderBy($"fare".desc, $"_hoodie_commit_time".desc)
      .show(20, false)
  }

  /**
    * 根据时间点查询数据
    * @param spark
    * @param path
    */
  def queryDataByTime(spark: SparkSession, path: String): Unit = {
    import org.apache.spark.sql.functions._

    //方式一:指定字符串,按照日期时间过滤获取数据
    val df1 = spark.read
      .format("hudi")
      .option("as.of.instant", "20220222214552250")
      .load(path)
      .sort(col("_hoodie_commit_time").desc)

    df1.printSchema()
    df1.show(5, false)

    //方式二:指定字符串,按照日期时间过滤获取数据
    val df2 = spark.read
      .format("hudi")
      .option("as.of.instant", "2022-02-22 21:45:52.250")
      .load(path)
      .sort(col("_hoodie_commit_time").desc)
    df2.printSchema()
    df2.show(5, false)
  }


  /**
    * 官方案例:模拟产生数据,插入Hudi表,采用COW模式
    * @param spark
    * @param table
    * @param path
    * @param dataGen
    */
  def insertData(spark: SparkSession, table: String, path: String, dataGen: DataGenerator): Unit = {
    import spark.implicits._
    //1.模拟乘车数据
    //构建数据生成器,模拟产生业务数据
    import org.apache.hudi.QuickstartUtils._
    //模拟生成100条,并转成String类型
    val inserts = convertToStringList(dataGen.generateInserts(100))

    import scala.collection.JavaConverters._
    val insertDF =spark.read.json(
      spark.sparkContext.parallelize(inserts.asScala, 2).toDS()
    )

    //    insertDF.printSchema()
    //    insertDF.show(10, false)

    //2.插入数据到Hudi表
    import org.apache.hudi.DataSourceWriteOptions._
    import org.apache.hudi.config.HoodieWriteConfig._
    insertDF.write
      .mode(SaveMode.Overwrite)
      .format("hudi")
      .option("hoodie.insert.shuffle.parallelism", "2")
      .option("hoodie.upsert.shuffle.parallelism", "2")
      //hudi表的属性值的设置
      //预合并
      .option(PRECOMBINE_FIELD.key(), "ts")
      //主键
      .option(RECORDKEY_FIELD.key(), "uuid")
      //分区
      .option(PARTITIONPATH_FIELD.key(), "partitionpath")
      //表名
      .option(TBL_NAME.key(), table)
      .save(path)

  }


  /**
    * 模拟产生hudi表中更新数据,将其更新到Hudi表中
    * @param spark
    * @param table
    * @param path
    * @param dataGen
    * @return
    */
  def updateData(spark: SparkSession, table: String, path: String, dataGen: DataGenerator): Unit = {

    import spark.implicits._
    //1.模拟乘车数据
    //构建数据生成器,模拟产生业务数据
    import org.apache.hudi.QuickstartUtils._
    //模拟产生100条更新数据,并转成String类型
    val updates = convertToStringList(dataGen.generateUpdates(100))

    import scala.collection.JavaConverters._
    val updateDF =spark.read.json(
      spark.sparkContext.parallelize(updates.asScala, 2).toDS()
    )

//    updateDF.printSchema()
//    updateDF.show(10, false)

    //2.插入数据到Hudi表
    import org.apache.hudi.DataSourceWriteOptions._
    import org.apache.hudi.config.HoodieWriteConfig._
    updateDF.write
      .mode(SaveMode.Append)
      .format("hudi")
      .option("hoodie.insert.shuffle.parallelism", "2")
      .option("hoodie.upsert.shuffle.parallelism", "2")
      //hudi表的属性值的设置
      //预合并
      .option(PRECOMBINE_FIELD.key(), "ts")
      //主键
      .option(RECORDKEY_FIELD.key(), "uuid")
      //分区
      .option(PARTITIONPATH_FIELD.key(), "partitionpath")
      //表名
      .option(TBL_NAME.key(), table)
      .save(path)


  }

  /**
    * 采用incremental query增量方式查询数据,需要指定时间戳
    * @param spark
    * @param path
    */
  def incrementalQueryData(spark: SparkSession, path: String): Unit = {

    import spark.implicits._

    val QUERY_TYPE_OPT_KEY = "hoodie.datasource.query.type"
    val QUERY_TYPE_SNAPSHOT_OPT_VAL = "snapshot"
    val QUERY_TYPE_READ_OPTIMIZED_OPT_VAL = "read_optimized"
    val QUERY_TYPE_INCREMENTAL_OPT_VAL = "incremental"

    val BEGIN_INSTANTTIME_OPT_KEY = "hoodie.datasource.read.begin.instanttime"
    val END_INSTANTTIME_OPT_KEY = "hoodie.datasource.read.end.instanttime"

    //1.加载hudi表数据,获取commit time时间,作为增量查询数据阈值
    spark.read
      .format("hudi")
      .load(path)
      .createOrReplaceTempView("view_temp_hudi_trips")
    val commits: Array[String] = spark
      .sql(
        """
          |SELECT
          | DISTINCT(_hoodie_commit_time) AS commitTime
          |FROM
          | view_temp_hudi_trips
          |ORDER BY
          | commitTime DESC
          |""".stripMargin
      )
      .map(row => row.getString(0))
      .take(50)
    val beginTime = commits(commits.length - 1)
    println(s"beginTime = ${beginTime}")

    //2.设置hudi数据commit time时间阈值,进行增量数据查询
    val tripsIncrementalDF = spark.read
      .format("hudi")
      //设置查询数据模式为:incremental,增量读取
      .option(QUERY_TYPE_OPT_KEY, QUERY_TYPE_INCREMENTAL_OPT_VAL)
      //设置增量读取数据时开始时间
      .option(BEGIN_INSTANTTIME_OPT_KEY, beginTime)
      .load(path)

    //3.将增量查询数据注册为临时视图,查询费用大于20的数据
    tripsIncrementalDF.createOrReplaceTempView("hudi_trips_incremental")
    spark
      .sql(
        """
          |SELECT
          | `_hoodie_commit_time`, fare, begin_lon, begin_lat, ts
          |FROM
          | hudi_trips_incremental
          |WHERE
          | fare > 20.0
          |""".stripMargin
      )
      .show(10, false)

  }

  /**
    * 删除hudi表数据,依据主键UUID进行删除,如果是分区表,需要指定分区路径
    * @param spark
    * @param table
    * @param path
    */
  def deleteData(spark: SparkSession, table: String, path: String):Unit = {
    import spark.implicits._

    //1.加载hudi表数据,获取条目数
    val tripsDF: DataFrame = spark.read
      .format("hudi")
      .load(path)
    println(s"Raw Count = ${tripsDF.count()}")

    //2.模拟要删除的数据,从hudi表中加载数据,获取几条数据,转换为要删除的数据集合
    val dataFrame = tripsDF.limit(2).select($"uuid", $"partitionpath")
    import org.apache.hudi.QuickstartUtils._

    val dataGenerator = new DataGenerator()
    val deletes = dataGenerator.generateDeletes(dataFrame.collectAsList())

    import scala.collection.JavaConverters._
    val deleteDF = spark.read.json(spark.sparkContext.parallelize(deletes.asScala, 2))

    //3.保存数据到hudi表中,设置操作类型为DELETE
    import org.apache.hudi.DataSourceWriteOptions._
    import org.apache.hudi.config.HoodieWriteConfig._
    deleteDF.write
      .mode(SaveMode.Append)
      .format("hudi")
      .option("hoodie.insert.shuffle.parallelism", 2)
      .option("hoodie.upsert.shuffle.parallelism", 2)
      //设置数据操作类型为delete,默认值为upsert
      .option(OPERATION.key(), "delete")
      .option(PRECOMBINE_FIELD.key(), "ts")
      .option(RECORDKEY_FIELD.key(), "uuid")
      .option(PARTITIONPATH_FIELD.key(), "partitionpath")
      .option(TBL_NAME.key(), table)
      .save(path)

    //4.再次加载hudi表数据,统计条目数,查看是否减少2条数据
    val verifyDF: DataFrame = spark.read
      .format("hudi")
      .load(path)
    println(s"After Delete Raw Count = ${verifyDF.count()}")

  }
}

 

今天俄罗斯进攻了乌克兰。

 

posted on 2022-02-24 21:35  嘣嘣嚓  阅读(3184)  评论(0编辑  收藏  举报

导航