Spark自定义读hbase数据源

背景

我想以简单的形式在Spark中读取Hbase数据,但是Spark并不支持读取Hbase数据后简单使用。思考能否自己实现这个读取的过程?Hbase的读写API,结果数据往往需要处理后使用。我们是否可以将Hbase结果数据通过转化,直接转化为DataFrame的形式,方便我们使用。如果可行的话,总体思路可以分为几个步骤。1验证下Spark如何读取数据源,2Hbase的数据结构,3如何转化为Spark的数据结构(DataFrame)

读数据简述

1.现有字段信息描述 student_info (id String,name String,age String)
2.在hbase中的存储是没有数据类型的,储存形式为:

ROWCOL+ROW
1000column=cf:age, timestamp=1581349656278, value=18
1000column=cf:id, timestamp=1581349680439, value=1
1000column=cf:name, timestamp=1581349639526, value=gq
1001column=cf:age, timestamp=1581349697669, value=18
1001column=cf:id, timestamp=1581349691325, value=2
1001column=cf:name, timestamp=1581349706198, value=zs

3.Spark中查询形式为,我们最终的最终效果

package com.asiainfo.hbase

import com.asiainfo.utils.GlobalConfigUtils
import org.apache.spark.sql.{DataFrame, SparkSession}

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

    val session: SparkSession = SparkSession.builder().appName("Spark Example").master("local")
      .getOrCreate()

    val frame: DataFrame = session.read.
    //定义参数 sparkField 就是(id String,name String,age String)
      options(Map("sparkField" -> GlobalConfigUtils.getProp("sparkField"))).
      //加载我们的自定义实现的类,相当于把自己的逻辑传输给spark
      format("com.asiainfo.custom.hbase.DefaultSource").load()
    frame.show()
  }
}

源码分析

看下Spark是怎么读取Text文件的,最终就能返回DF了

    val frame: DataFrame = session.read.text("123.txt")

点击去发现,归根结底调用的是DataFrameReader的load()方法

  def load(paths: String*): DataFrame = {
    sparkSession.baseRelationToDataFrame(
      DataSource.apply(
        sparkSession,
        paths = paths,
        userSpecifiedSchema = userSpecifiedSchema,
        className = source,
        options = extraOptions.toMap).resolveRelation())
  }

里层调用了resolveRelation()方法返回一个BaseRelation对象,外层调用了baseRelationToDataFrame()返回一个DataFrame 那么我们先看看这个resolveRelation干啥了

def resolveRelation(checkFilesExist: Boolean = true): BaseRelation = {
	//providingClass.newInstance() 就是我们的提供的数据源类,如果是hbase 需要实现SchemaRelationProvider 中的createRelation方法
    val relation = (providingClass.newInstance(), userSpecifiedSchema) match {
 	  
      case (dataSource: SchemaRelationProvider, Some(schema)) =>
        dataSource.createRelation(sparkSession.sqlContext, caseInsensitiveOptions, schema)
      case (dataSource: RelationProvider, None) =>
        dataSource.createRelation(sparkSession.sqlContext, caseInsensitiveOptions)
      case (_: SchemaRelationProvider, None) =>
        throw new AnalysisException(s"A schema needs to be specified when using $className.")
      case (dataSource: RelationProvider, Some(schema)) =>
        val baseRelation =
          dataSource.createRelation(sparkSession.sqlContext, caseInsensitiveOptions)
        if (baseRelation.schema != schema) {
          throw new AnalysisException(s"$className does not allow user-specified schemas.")
        }
        baseRelation
     ......

看到其实就是返回了一个baseRelation对象,而且是由providingClass.newInstance()来提供的这个对象的创建方法,providingClass这个对象我们通过调用可以知道,其实它可以是我们自己定义的类,需要实现RelationProvider方法,提供baseRelation对象。所以我这里有思路了,我们需要自己定义一个类A,实现RelationProvider方法,那我看返回的baseRelation被哪里使用到了。顺着代码可以跟踪到,这个对象被传入到了LogicalRelation构造方法中。

  override val output: Seq[AttributeReference] = {
    val attrs = relation.schema.toAttributes
    expectedOutputAttributes.map { expectedAttrs =>
      assert(expectedAttrs.length == attrs.length)
      attrs.zip(expectedAttrs).map {
        // We should respect the attribute names provided by base relation and only use the
        // exprId in `expectedOutputAttributes`.
        // The reason is that, some relations(like parquet) will reconcile attribute names to
        // workaround case insensitivity issue.
        case (attr, expected) => attr.withExprId(expected.exprId)
      }
    }.getOrElse(attrs)
  }

并且以val attrs = relation.schema.toAttributes的形式使用到了这个方法。那我们看看BaseRelation方法schema属性是怎么被赋值的。看到类型是一个StructType。构造方法如下

case class StructType(fields: Array[StructField]) {}....

而StructField构造方法。从这里我们知道了。这个schame应该就是有一个个StructField对象封装出来的。而从StructField的构造方法来看,这个对象代表的是SparkSql中的一行行的数据。我们从hbase中取出的数据,只需要封装这个方法即可。

case class StructField(
    name: String,
    dataType: DataType,
    nullable: Boolean = true,
    metadata: Metadata = Metadata.empty) {

那我们需要自定义一个类,实现RelationProvider方法,提供一个baseRelation 这个relation 需要我们自己定义,实现baseRelation 类。

具体实现如下

SparkSchema.scala

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.sql.sources.{BaseRelation, RelationProvider, TableScan}
import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}

import scala.collection.mutable.ArrayBuffer


case class SparkSchema(fieldType: String, fieldName: String) extends Serializable

class SelfHbaseRelation(@transient val sqlContext: SQLContext,
                        @transient val options: Map[String, String]) extends BaseRelation with TableScan with Serializable {


  //1.带有字段类型的 "(id String,name String,age Int)"
  private val sparkFieldConf: String = options.getOrElse("sparkField", sys.error("找不到sparkField"))
  private val sparkField: Array[SparkSchema] = extractSparkField(sparkFieldConf)


  //提前通过已有的结构,定义好schema。其实就是封装好的字段信息 给到spark
  override def schema: StructType = {
    val rows: Array[StructField] = sparkField.map(field => {
      val structField: StructField = field.fieldType.toLowerCase match {
        case "string" => StructField(field.fieldName, StringType)
        case "int" => StructField(field.fieldName, IntegerType)
      }
      structField
    })
    new StructType(rows)
  }


  //这个方法,会被spark调用,去取的数据,然后与schema结合起来,共同映射为一张表
  override def buildScan(): RDD[Row] = {
    val hbaseConf: Configuration = HBaseConfiguration.create()
    hbaseConf.set("hbase.zookeeper.quorum","jfdacp03:2181,jfdacp04:2181,jfdacp05:2181")//指定zookeeper的地址
    hbaseConf.set(TableInputFormat.INPUT_TABLE, "student_info")//指定要查询表名
    hbaseConf.set(TableInputFormat.SCAN_CACHEDROWS , "10000")//指定查询时候,缓存多少数据
    hbaseConf.set(TableInputFormat.SHUFFLE_MAPS , "1000")
    hbaseConf.set("zookeeper.znode.parent","/hbase-unsecure")//hdp 跟cm的zk路径不一致

    //通过newAPIHadoopRDD查询
    val hbaseRdd: RDD[(ImmutableBytesWritable, Result)] = sqlContext.sparkContext.newAPIHadoopRDD(
      hbaseConf,
      classOf[TableInputFormat],
      classOf[ImmutableBytesWritable],
      classOf[Result]
    )
    val resultRdd: RDD[Result] = hbaseRdd.map(_._2)

    //我现在有字段信息了。就应该遍历这个字段信息,然后在hbase中取出相应的值来,然后组装成RDD[row]
    //因为要返回的是一个RDD[row] 所以只能使用map
    val value: RDD[Row] = resultRdd.map(result => {
      val arrayBuffer = new ArrayBuffer[Any]()
      //遍历没一个字段,在一个row的result中取出所有字段的值来。最终拼接成一行数据.
      //这样一遍 就是一个ROW。ROW其实就是每行的数据 {1,2,3,4,5}
      //每一行的数据 使用一个arrayBuffer来接收
      sparkField.foreach(field=>{
          field.fieldType.toLowerCase() match {
            case "string" => arrayBuffer += Bytes.toString(result.getValue(Bytes.toBytes("cf"), Bytes.toBytes(field.fieldName)))
            case "int" =>  arrayBuffer += Bytes.toInt(result.getValue(Bytes.toBytes("cf"), Bytes.toBytes(field.fieldName)))
          }
      })
      Row.fromSeq(arrayBuffer)
    })
    value
  }

  private def extractSparkField(sparkFieldConf: String): Array[SparkSchema] = {
    val fieldArray: Array[String] = sparkFieldConf.trim.drop(1).dropRight(1).split(",")
    val schemas: Array[SparkSchema] = fieldArray.map(field => {
      val fieldStr: Array[String] = field.split(" ")
      SparkSchema(fieldStr(1), fieldStr(0))
    })
    schemas
  }
}

DefaultSource.scala

import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.sources.{BaseRelation, RelationProvider}

class DefaultSource extends RelationProvider{
  override def createRelation(sqlContext: SQLContext, parameters: Map[String, String]): BaseRelation = {
    new SelfHbaseRelation(sqlContext,parameters)
  }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值