一、自定义排序
自定义排序
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()
}
}