大数据开发之Spark篇----Spark和HBase的结合(2)

Spark将数据写入到HBase上

上一篇博客,我已经介绍了使用put这种方法来将数据写入到HBase上了,如果你是在实时状态下这样写的话可能还好,但是如果是离线批处理的时候,我们要将数据批量地写入到HBase上的话,这么写的性能就非常地差了。
下面将介绍一种直接将数据写入到HFile的方法,数据将不经过HBase层了。这种写法的性能是put的好几倍哦。
下面也是先贴代码再作解释:(这里的依赖和上一篇博客中的依赖一样,我就不贴了)

package com.doudou.www.etl

import java.util.zip.CRC32

import org.apache.hadoop.fs.Path
import org.apache.hadoop.hbase.client.{ConnectionFactory, HTable, Put}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.{HFileOutputFormat2, LoadIncrementalHFiles, TableOutputFormat}
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.hbase._
import org.apache.hadoop.mapreduce.Job
import org.apache.log4j.{Level, Logger}
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable.ListBuffer

/**
  * 使用HFile这种方式来写入到HBase上面
  * 就是先写入到HDFS上面,再Bulkload到HBase上关联的表
  */
object ETLtoHFile {

  val logger:Logger = Logger.getLogger(ETLtoHFile.getClass)

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

    Logger.getRootLogger.setLevel(Level.WARN)

    /**
      * 同理先读取数据
      */
    val conf = new SparkConf().setAppName("etl_hfile").setMaster("local[3]").set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
    conf.registerKryoClasses(Array(classOf[ImmutableBytesWritable],classOf[KeyValue]))
    conf.set("spark.hadoop.dfs.client.use.datanode.hostname","true")
    val sc = new SparkContext(conf)

    val InfoRdd = sc.textFile(ETLConstantUtils.DATA_DIR,2)
    val personRDD = InfoRdd.mapPartitions(iter => {
      iter.map(x => {
        val info = x.split(",")
        info.size
        Person(info(0).toInt,info(1),info(2).toInt,info(3),info(4).toInt,info(5).toInt)
      })
    })

    logger.warn("" + personRDD.count())

    /**
      * 将数据转成对应的格式
      * 这里有点不同
      * 之前都是一条记录对应一个key-value的
      * 现在是一个(rowkey,columuName)-> KeyValue 这样为RDD上的一个元素
      *
      */
    val keyValueRDD = personRDD.mapPartitions(iter => {
      /**
        * 为什么要定义成这种模式,是因为要对RDD进行排序,按照rowkey和column来排序
        * 所以先定义成((String,String),KeyValue),则每一个元素都是(rowkey,column) -> KeyValue
        * //这里的坑就在与list里面是元组的话,需要用 -> 来关联元组的key和value
        * 需要对RDD进行排序
        * 排序后就可以将(rowkey,column)去掉
        */
      var listWithKeyValue = new ListBuffer[((String,String),KeyValue)]()
      iter.flatMap{case person => {
        listWithKeyValue.clear()
        val rowKey = createRowKey(person)
        listWithKeyValue += (rowKey,"id") -> createKeyValue(rowKey,"id",person.id.toString)
        listWithKeyValue += (rowKey,"name") -> createKeyValue(rowKey,"name",person.name)
        listWithKeyValue += (rowKey,"age") -> createKeyValue(rowKey,"age",person.age.toString)
        listWithKeyValue += (rowKey,"gender") -> createKeyValue(rowKey,"gender",person.gender)
        listWithKeyValue += (rowKey,"sameLevel") -> createKeyValue(rowKey,"sameLevel",person.sameLevel.toString)
        listWithKeyValue += (rowKey,"isWorked") -> createKeyValue(rowKey,"isWorked",person.isWorked.toString)
        listWithKeyValue.toList
      }}
    })
      .sortByKey(true).map(tuple => (new ImmutableBytesWritable(Bytes.toBytes(tuple._1._1)),tuple._2))

    logger.warn("" + keyValueRDD.count())


    /**
      * 同理构建HBase的相关工作
      */
    val hconf = HBaseConfiguration.create()
    hconf.set("hbase.zookeeper.quorum","doudou")
    hconf.set("hbase.zookeeper.property.clientPort","2181")
    hconf.set("hbase.rootdir","hdfs://doudou:8020/hbase")
    hconf.set("hbase.fs.tmp.dir","/tmp/hbase-staging")


    val con = ConnectionFactory.createConnection(hconf)
    val admin = con.getAdmin

    if(admin.tableExists(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))){
      admin.disableTable(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
      admin.deleteTable(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
      val htd = new HTableDescriptor(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
      val hcd = new HColumnDescriptor("info")
      htd.addFamily(hcd)
      admin.createTable(htd)
    }else{
      val htd = new HTableDescriptor(TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
      val hcd = new HColumnDescriptor(ETLConstantUtils.HCOLUMNS)
      htd.addFamily(hcd)
      admin.createTable(htd)
    }

    hconf.set(TableOutputFormat.OUTPUT_TABLE,ETLConstantUtils.HBASE_TABLE)

    /**
      * 这里就和一般的put方法不同了
      * 1)我们先要构建一个Job对象,且对象参数为HBase的conf配置
      * 2)new一个HTable出来,需要传入hconf
      * 3)一个特殊的方法,要使用这个方法记得idea上要有你hdfs-site.xml和core-site.xml这个两个配置文件
      */
    val job = Job.getInstance(hconf)
    val table = new HTable(hconf,TableName.valueOf(ETLConstantUtils.HBASE_TABLE))
    HFileOutputFormat2.configureIncrementalLoad(job,table.getTableDescriptor,table.getRegionLocator)

    /**
      * 定义数据初始的HDFS上的输出路径,以及其对应的path对象
      */
    val output_dir = "/doudou/person_3"
    val output_path = new Path(output_dir)

    /**
      * 同样使用saveAsNewAPIHadoopFile这个API来写入数据
      * 这里的classOf[HFileOutputFormat2]是使用api来修改过的
      * 就是上面的特殊方法
      */
    keyValueRDD.saveAsNewAPIHadoopFile(
      output_dir,
      classOf[ImmutableBytesWritable],
      classOf[KeyValue],
      classOf[HFileOutputFormat2],
      hconf
    )

    /**
      * 完成上面的动作后,在我们的指定路径上已经有了我们的数据文件了
      * 现在需要将数据刷到HBase的表上了
      */
    val load = new LoadIncrementalHFiles(job.getConfiguration)
    load.doBulkLoad(output_path, table)

    admin.close()
    con.close()
    sc.stop()
  }

  def createRowKey(person:Person):String = {

    val sBuilder = new StringBuilder()
    sBuilder.append(person.id)
    val crc32 = new CRC32()
    crc32.reset()
    if(person.name.length > 0){
      crc32.update(Bytes.toBytes(person.name))
    }
    if(person.age != 0){
      crc32.update(Bytes.toBytes(person.age.toString))
    }
    if(person.gender.size > 0){
      crc32.update(Bytes.toBytes(person.gender))
    }
    sBuilder.append(crc32.getValue)
    sBuilder.toString()
  }

  def createKeyValue(rowKey:String,column:String,value:String):KeyValue = {
    val keyValue = new KeyValue(
      Bytes.toBytes(rowKey),
      Bytes.toBytes(ETLConstantUtils.HCOLUMNS),
      Bytes.toBytes(column),
      Bytes.toBytes(value)
    )
    keyValue
  }

}

来这里的前半段和上一篇博文里的内容是一样的,我就不做多的介绍了,这种写法有一个最为不同的写法是,我们需要对每一条记录使用flatMap,将单个column的cell当作RDD中的一个元素。和一般的区别是什么呢?我们知道在HBase里面,我们在定义table的时候只需要定义一个columnfamily就可以了(我司一般是使用单个columnfamily,如果读者业务有需要可以定义多个columnfamily的),至于这个columnfamily里面有多少个column是在写入数据时以key-value这种方式定义的。所以,我们在使用put这种方法的时候,是将RDD中的一条记录一个put,而记录中的字段和值就是以key-value方法使用addColumn这个API来添加进去的。

但这里的做法就不是了,我们首先将一条记录的每一个KeyValue对象,放在一个List中,那么一个List对应着一条记录了,然后使用flatMap来代替map进行操作。细心的读者可能发现我的List中的类型是((String,String),KeyValue),而后我进行了一次排序就直接将KeyValue对象取出来丢掉了(String,String)这个部分。这是因为使用直接写入到HFile中是需要对每一个KeyValue对象按照rowKey和column进行排序的(按照String类型排序,别把rowkey和column转成Bytes后再排序哦)。代码末端有生成KeyValue对象的生成方法。

然后还有一段是特别的就是对Job对象的操作了,这段是固定写法,如果读者好奇为什么要这么写的话,可以翻下源码中ImportTsv这个类,就在的org.apache.hadoop.hbase.mapreduce这个包下面,里面就是这种写法。

最后使用完saveAsNewAPIHadoopFile将数据加载到HDFS上面后,记得把数据刷到HBase表上面去哦,使用bulkload进行load就可以了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值