SparkCore代码笔记03——自定义排序、自定义分区、累加器、广播变量

一、自定义排序

自定义排序
    Spark对简单的数据类型可以直接排序,但是对于一些复杂的条件就需要用自定义排序来实现了

 

1.第一种定义方法:

用到了隐式转换 

package scalaBase.day11

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

case class Girl(name:String,faceValue:Int,age:Int)

object myOrder{
  implicit val girlOrdering=new Ordering[Girl]{
    override def compare(x: Girl, y: Girl): Int = {
      if(x.faceValue!=y.faceValue){
        y.faceValue-x.faceValue//降序
      }
      else {
        x.age-y.age  //升序
      }
    }
  }
}

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


      val conf = new SparkConf().setMaster("local").setAppName("mypartitioner")
      val sc = new SparkContext(conf)
      val rdd1: RDD[(String, Int, Int)] =
       sc.parallelize(List(("lucy",1,30),("tom",6,40),("vivi",6,30),("nancy",5,80)))
      val sorted1 = rdd1.sortBy(_._2,false)
      println(sorted1.collect().toBuffer)

     import myOrder.girlOrdering
     val sorted2 = rdd1.sortBy(x=>new Girl(x._1,x._2,x._3))
     println(sorted2.collect.toBuffer)

  }
}

2.第二种自定义排序方法:

package scalaBase.day11

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD


case class girl(name:String,faceValue:Int,age:Int) extends  Ordered[girl]{
  override def compare(that: girl): Int = {
    if(this.faceValue!=that.faceValue){
      that.faceValue-this.faceValue
    }
    else
      this.age-that.age
  }
}

object MyOrderDemo2 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local").setAppName("mypartitioner")
    val sc = new SparkContext(conf)
    val rdd1: RDD[(String, Int, Int)] =
    sc.parallelize(List(("lucy",1,30),("tom",6,40),("vivi",6,30),("nancy",5,80)))
    val sorted: RDD[(String, Int, Int)] = rdd1.sortBy(x=>girl(x._1,x._2,x._3))
    println(sorted.collect().toBuffer)
  }
}

 

 

二、自定义分区

自定义分区
    当spark的分区器不满足实际的业务的时候,就需要我们自定义分区器来实现分区,
    自定义分区器必须要继承Partitioner抽象类。

键值对RDD分区
    Spark目前支持的Hash分区和Range分区,用户也可以自定义分区,Hash分区是当前默认的分区器。
    Spark的分区器决定了RDD中分区的个数,RDD中每条数据经过shuffle过程决定了数据属于哪个分区和reduce的个数。
    
    不管什么分区器,都需要继承Partitoner抽象类,需要实现numPartitions(获取分区数)和getPartition(获取分区号)方法
    
    HashPartitoner:
    该分区器允许数据的key为null,则直接返回分区号为:0
    如果key不为null,则取key的hashCode值模余分区数来获取分区号
    
    RangerPartitioner:
    该分区方式有极少的场景可以应用到
    该分区方式是通过两个步骤来实现的:
        1、先从整个RDD中抽取样本数据,将样本数据排序,计算出每个每个分区的最大key值,
            这样就形成一个Array[key]类型的数组变量rangeBounds。
        2、判断rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标,
            该分区器要求RDD中的key类型必须是可以排序的。

应用场景:

按照业务需求,对数据进行分类存储;

遇到数据倾斜的时候,当某一分区的数据过于庞大,举例连衣裙的数据过多,我们给它加上一个10以内的随机数,rand.nextInt(10),减小数据倾斜

 

实例:

按照科目分到不同的文件中 ,定义一个新的分区器,继承自Partitioner,然后使用partitionBy算子,传入新定义的分区器的对象

package scalaBase.day11

import java.net.URL

import org.apache.spark.rdd.RDD
import org.apache.spark.{HashPartitioner, Partitioner, SparkConf, SparkContext}
import scala.collection.mutable


class myPartitioner(subjects:Array[String]) extends Partitioner{
  override def numPartitions: Int = subjects.length
  val submap = new mutable.HashMap[String,Int]()
  var x=0
  for(i<-subjects){
    submap+=(i->x)
    x+=1
  }

  override def getPartition(key: Any): Int = {
    submap.getOrElse(key.toString,0)
  }
}


object MyPartitionerDemo {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local").setAppName("mypartitioner")
    val sc = new SparkContext(conf)
    val rdd: RDD[String] = sc.textFile("F://bigdata/data/access.txt")
    val rdd1: RDD[(String, Int)] = rdd.map(x => {
      val f = x.split("\t")
      (f(1), 1)
    })
    val rdd2: RDD[(String, Int)] = rdd1.reduceByKey(_+_)
    val rdd3: RDD[(String, (String, Int))] = rdd2.map(x => {
      val host = new URL(x._1).getHost
      (host, x)
    })

    val p1 = new HashPartitioner(5)
    //val rdd4 = rdd3.partitionBy(p1)
   val subjects: Array[String] = rdd3.keys.distinct().collect()
    val rdd4 = rdd3.partitionBy(new myPartitioner(subjects))


    rdd4.saveAsTextFile("f://bigdata/out/p2")
    println(rdd4.collect().toBuffer)
    sc.stop()
  }
}

三、累加器


累加器
    Spark提供了用于做聚合工作的一个功能:Accumulator,可以使得Executor进行累加,最终将累加的值返回给Driver端
    注意,Executor只能做累加,不能查看其值,只有Driver端能查看该值。
    实现该功能,需要用到AccumulatorV2对象,并实现多个方法


    Spark的累加器,提供了一些常用的累加操作,比如LongAccumulator、DoubleAccumulator等,可以直接调用并进行累加即可。
    
    总结:
    累计器的创建:
    1、创建累计器的实例
    2、通过sc.register()注册累加器
    3、通过累加器实例的add方法来累加数据
    4、通过累计器实例的value方法来获取累计器的值

1.没有累加器导致的问题:

原因:因为初始化数据在driver端,计算在executor上,分布式计算,数据分布在不同机器上,例如mini1机器上有1,3,5,6,mini2机器上有2,4,7,8,9

  val conf = new SparkConf().setMaster("local").setAppName("mypartitioner")
    val sc = new SparkContext(conf)
    val list = sc.parallelize(List(1,2,3,4,5,6,7,8,9),2)
    var sum=0;
    list.foreach(x=>sum+=x)
    println(sum)

//运行结果:0

2.使用自带的累加器LongAccumulator

  val conf = new SparkConf().setMaster("local").setAppName("mypartitioner")
    val sc = new SparkContext(conf)
    val list = sc.parallelize(List(1,2,3,4,5,6,7,8,9),2)
   // var sum=0;
   // list.foreach(x=>sum+=x)
    //println(sum)

    val longacc: LongAccumulator = new LongAccumulator()
    sc.register(longacc,"LongAccumulator1")
    list.foreach(x=>longacc.add(x))
    println(longacc.value)

//运行结果:45

3.1自定义累加器,实现以上功能

自定义的累加器:

package com.qf.gp1921.day11

import org.apache.spark.util.AccumulatorV2

/**
  * 自定义Accumulator,需要实现多个方法
  */
class MyAccumulator extends AccumulatorV2[Int, Int]{
  // 创建一个输出值的变量
  private var sum: Int = _

  // 初始方法,检测是否为空
  override def isZero: Boolean = sum == 0

  // 拷贝一个新的累加器
  override def copy(): AccumulatorV2[Int, Int] = {
    // 需要创建当前自定义累加器对象
    val myacc = new MyAccumulator
    // 需要将当前数据拷贝到新的累加器里面
    // 就是将原有累加器数据复制到新的累计器里面
    // 主要是为了数据的更新迭代
    myacc.sum = this.sum
    myacc
  }
  // 重置一个累加器,将累计器中的数据清零
  override def reset(): Unit = sum = 0
  // 局部累加方法,将每个分区进行分区内累加
  override def add(v: Int): Unit = {
    // v: 分区中的一个元素
    sum += v
  }
  // 全局聚合方法,将每个分区的结果进行累加
  override def merge(other: AccumulatorV2[Int, Int]): Unit = {
    // other:分区的累加值
    sum += other.value
  }
  // 输出值--最终累加值
  override def value: Int = sum
}

调用:

package com.qf.gp1921.day11

import org.apache.spark.util.{DoubleAccumulator, LongAccumulator}
import org.apache.spark.{SparkConf, SparkContext}

object AccumulatorDemo3 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("AccumulatorDemo3").setMaster("local[2]")
    val sc = new SparkContext(conf)

    val numbers = sc.parallelize(Array(1,2,3,4,5,6), 2)

    // 实例化自定义Accumulator
    val acc = new MyAccumulator()

    // 将Accumulator进行注册
    sc.register(acc, "acc")

    // 开始累加
    numbers.foreach(x => acc.add(x))
    println(acc.value)

    sc.stop()
  }
}

 

3.2自定义累加器,实现wordcount

package com.qf.gp1921.day11

import org.apache.spark.util.AccumulatorV2

import scala.collection.mutable

/**
  * 实现单词计数的自定义Accumulator
  */
class MyAccumuletor2 extends AccumulatorV2[String, mutable.HashMap[String, Int]]{
  private val hashAcc = new mutable.HashMap[String, Int]()

  // 检测是否为空
  override def isZero: Boolean = hashAcc.isEmpty
  // 拷贝新的累加器
  override def copy(): AccumulatorV2[String, mutable.HashMap[String, Int]] = {
    val newAcc = new MyAccumuletor2
    hashAcc.synchronized {
      newAcc.hashAcc ++= hashAcc
    }
    newAcc
  }
  // 重置累加器
  override def reset(): Unit = hashAcc.clear()
  // 局部累加方法
  override def add(v: String): Unit = {
    hashAcc.get(v) match {
      case None => hashAcc += ((v, 1))
      case Some(x) => hashAcc += ((v, x + 1))
    }
  }
  // 全局累加
  override def merge(other: AccumulatorV2[String, mutable.HashMap[String, Int]]): Unit = {
    other match {
      case o: AccumulatorV2[String, mutable.HashMap[String, Int]] => {
        for ((k, v) <- o.value) {
          hashAcc.get(k) match {
            case None => hashAcc += ((k, v))
            case Some(x) => hashAcc += ((k, x + v))
          }
        }
      }
    }
  }
  // 输出值
  override def value: mutable.HashMap[String, Int] = hashAcc
}

 

调用:

package com.qf.gp1921.day11

import org.apache.spark.{SparkConf, SparkContext}

/**
  * 用累加器实现单词计数
  */
object AccumulatorDemo4 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("AccumulatorDemo4").setMaster("local[2]")
    val sc = new SparkContext(conf)

    val hashAcc = new MyAccumuletor2
    sc.register(hashAcc, "wc")

    val data = sc.textFile("hdfs://node01:9000/files").flatMap(_.split(" "))

    data.foreach(word => hashAcc.add(word))

    println(hashAcc.value)

    sc.stop()
  }
}

 

 

四、广播变量


    概念:将Driver端的一个变量以广播的方式发送给相应的Executor,每个节点只会广播一次。
    广播变量的优点:不是每个task一份变量副本,而是每个节点的Executor一份副本,
        这样的话,就可以让变量产生的副本减少很多。
        
    广播变量的过程:
        1、通过对一个类型T的对象调用sc.broadcast来创建出一个Broadcast[T]对象,并且任何可序列化的类型都可以这么实现。
        2、通过value属性访问该对象的值。
        3、变量只会被广播到各节点一次,而且是作为只读值处理。
        
    如果想将RDD广播出去行不行?不行
        因为RDD只是个数据的描述,没有存储实际的数据只有元数据,所以不能将RDD广播出去。倒是可以将RDD的结果拿到Driver端后再进行广播。
    广播变量只能在Driver端定义,不能再Executor端定义。
    
    应用场景:Driver端的变量在任务的执行的过程中,Executor端需要多次用到,
    此时最好将该变量以广播的方式分发到每个Executor端,这样可以有效减少网络io和占用的内存的容量。
    
    如果这个Driver端的变量太大,也不适合进行广播,更不适合以task的方式进行传输。

 

练习:

没有广播变量的话:

package com.qf.gp1921.day11

import org.apache.spark.{SparkConf, SparkContext}

/**
  * 以下代码会出现一个问题:
  * list是在Driver端创建的,但是因为需要在Executor端使用,
  * 所以Driver会将list以task的形式发送到Executor端
  * 也就是相当于在Executor端需要复制一份,
  * 如果有很多很多个task,就会有很多的task在Executor端携带很多个list
  * 如果这个list数据非常大的时候,就可能会造成oom
  */
object BroadcastDemo1 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("BroadcastDemo1").setMaster("local[2]")
    val sc = new SparkContext(conf)

    // 该变量的值是在Driver端
    val list = List("hello", "hanmeimei", "mimi")

    val lines = sc.textFile("hdfs://node01:9000/files")
    val filtered = lines.filter(list.contains(_))

    filtered.foreach(println)



    sc.stop()
  }
}

使用广播变量之后:

package com.qf.gp1921.day11

import org.apache.spark.broadcast.Broadcast
import org.apache.spark.{SparkConf, SparkContext}

/**
  * 使用广播变量
  */
object BroadcastDemo2 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("BroadcastDemo1").setMaster("local[2]")
    val sc = new SparkContext(conf)

    val list = List("hello")

    // 封装广播变量,相当于进行广播, 在Driver端执行
    val broadcast: Broadcast[List[String]] = sc.broadcast(list)

    val lines = sc.textFile("hdfs://node01:9000/files")
    // 在Executor端执行, broadcast.value就是从本地拿取的广播过来的值
    val filtered = lines.filter(broadcast.value.contains(_))
    filtered.foreach(println)


    sc.stop()
  }
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值