Spark-Core分组求TOPN的六种不同的实现方式

案例:计算学科最受欢迎老师TopN

1.需求:根据网站的行为日志,统计每个学科最受欢迎老师的TopN,即按照学科分组,在每一个组内进行排序

2. 样例数据:

http://bigdata.51doit.cn/laozhang

http://bigdata.51doit.cn/laozhang

http://bigdata.51doit.cn/laozhao

http://bigdata.51doit.cn/laozhao

http://bigdata.51doit.cn/laozhao

http://bigdata.51doit.cn/laozhao

http://bigdata.51doit.cn/laozhao

http://bigdata.51doit.cn/laoduan

http://bigdata.51doit.cn/laoduan

http://javaee.51doit.cn/xiaozhang

http://javaee.51doit.cn/xiaozhang

http://javaee.51doit.cn/laowang

http://javaee.51doit.cn/laowang

http://javaee.51doit.cn/laowang

 

数据格式:http://学科.51doit.cn/老师名称

3.六种不同的实现方式:

3.1 第一种方法:调用groupBy按照学科进行分组,然后将value对应的迭代器toList,将数据全部加载到内存中,然后在调用List的sortBy方法进行排序,然后再调用take取TopN

package com.zxx.spark.day06

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

/**
 * 第一种方法是将迭代器中的数据都收集到内存中然后在内存中进行排序,
 * 这种方法只针对于少量数据适合都加载到内存中,
 * 如果是海量数据则有可能出现内存溢出的问题
 */
object FavoriteTeacherDemo1 {
  def main(args: Array[String]): Unit = {
    //求每一学科最受欢迎的老师TopN
    //先创建SparkContext和集群建立链接
    val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName)
    //添加一个判断,判断是否设置为集群或是,还是本地模式
    val flag: Boolean = args(0).toBoolean
    if (flag) {
      conf.setMaster("local[*]")
    }
    val sc: SparkContext = new SparkContext(conf)
    //创建RDD
    val rdd: RDD[String] = sc.textFile(args(1))
    //    http: / / bigdata.51doit.cn/laozhang
    //将读取到的数据进行切割和处理
    val subjectAndTeacherOne: RDD[((String, String), Int)] = rdd.map(e => {
      val sp: Array[String] = e.split("/")
      val url: Array[String] = sp(2).split("\\.")
      ((url(0), sp(3)), 1)
    })
    //subjectAndTeacherOne:((bigdata,laozhao),1), ((bigdata,laozhao),1), ((bigdata,laozhao),1),
    //将数据按照学科和老师进行联合作为key进行聚合
    val reduced: RDD[((String, String), Int)] = subjectAndTeacherOne.reduceByKey(_ + _)
    //然后在按照学科进行分组
    val grouped: RDD[(String, Iterable[((String, String), Int)])] = reduced.groupBy(_._1._1)
    //第一种方法是将迭代器中的数据都收集到内存中然后在内存中进行排序,这种方法只针对于少量数据适合都加载到内存中,如果是海量数据则有可能出现内存溢出的问题
    val res: RDD[(String, List[((String, String), Int)])] = grouped.mapValues(it => {
      val sorted: List[((String, String), Int)] = it.toList.sortBy(-_._2).take(3)
      sorted
    })
    println(res.collect().toBuffer)
  }
}

3.2 第二种方法:第二种方法是将每个学科过滤出来,然后单独对每一个学科进行排序,求topN,(不分组),这样就是将所有学科放在一个数组中,然后只对一个学科进行排序,这样就可以避免分组,减少一次shuffle,这样做虽然减少了一次shuffle,但是提交了3次job

package com.zxx.spark.day06

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

/**
 * 第二种方式,是将每个学科过滤出来,然后单独对每一个学科进行求topN(底层调用了有界优先队列,不需要排序),利用for循环,遍历所有学科
 * 需要将要过滤的所有学科放在一个数组中
 */
object FavoriteTeacherDemo2 {
  def main(args: Array[String]): Unit = {
    //求每一学科最受欢迎的老师TopN
    //先创建SparkContext和集群建立链接
    val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName)
    //添加一个判断,判断是否设置为集群或是,还是本地模式
    val flag: Boolean = args(0).toBoolean
    if (flag) {
      conf.setMaster("local[*]")
    }
    val sc: SparkContext = new SparkContext(conf)
    //创建RDD
    val rdd: RDD[String] = sc.textFile(args(1))
    //    http: / / bigdata.51doit.cn/laozhang
    //将读取到的数据进行切割和处理
    val subjectAndTeacherOne: RDD[((String, String), Int)] = rdd.map(e => {
      val sp: Array[String] = e.split("/")
      val url: Array[String] = sp(2).split("\\.")
      ((url(0), sp(3)), 1)
    })
    //将学科和老师为联合key进行聚合
    val reduced: RDD[((String, String), Int)] = subjectAndTeacherOne.reduceByKey(_ + _)
    //将所有学科放在一个数组中,然后只对一个学科进行排序,这样就可以避免分组,减少一次shuffle,这样做虽然减少了一次shuffle,但是提交了3次job
    val arr: Array[String] = Array("bigdata", "javaee", "php")
    for (elem <- arr) {
      //subjectAndTeacherOne:((bigdata,laozhao),1), ((bigdata,laozhao),1), ((bigdata,laozhao),1),
      //第二种排序方式,是将每个学科过滤出来,然后单独对每一个学科进行排序,求topN,
      val rdd2: RDD[((String, String), Int)] = reduced.filter(_._1._1.equals(elem))
      /*     //新的rdd中装的只有bigdata 学科对应的老师和数量
                val sorted: RDD[((String, String), Int)] = rdd2.sortBy(_._2,false)
                val res: Array[((String, String), Int)] = sorted.take(3)*/
      //调用top方法,来取topN,因为top方法底层是调用了takeOrdered方法,takeOrdered底层有一个隐式转化,
      //默认的排序规则是按照Ordering中的tupTwo进行排序的,不符合我们的需求,所以我们要自定义排序规则,用隐式参数的方法将数据传入top算子中
      implicit val ord: Ordering[((String, String), Int)] = Ordering[Int].on[((String, String), Int)](t => t._2)
      val res: Array[((String, String), Int)] = rdd2.top(3) // rdd2.top(3)(隐式参数)
      println(res.toBuffer)
    }
    //释放资源
    sc.stop()

  }

}

3.3 第三种方法是创建一个有TreeSet集合,这个集合会自动去重,并且会在集合内自动进行排序实现的功能就是和有界优先队列一样

package com.zxx.spark.day06

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

import scala.collection.mutable

/**
 * 第三种方法是创建一个有TreeSet集合,这个集合会自动去重,并且会在集合内自动进行排序
 * 实现的功能就是和有界优先队列一样
 */
object FavoriteTeacherDemo3 {
  def main(args: Array[String]): Unit = {

    //求每一学科最受欢迎的老师TopN
    //先创建SparkContext和集群建立链接
    val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName)
    //添加一个判断,判断是否设置为集群或是,还是本地模式
    val flag: Boolean = args(0).toBoolean
    if (flag) {
      conf.setMaster("local[*]")
    }
    val sc: SparkContext = new SparkContext(conf)
    //创建RDD
    val rdd: RDD[String] = sc.textFile(args(1))
    //    http: / / bigdata.51doit.cn/laozhang
    //将读取到的数据进行切割和处理
    val subjectAndTeacherOne: RDD[((String, String), Int)] = rdd.map(e => {
      val sp: Array[String] = e.split("/")
      val url: Array[String] = sp(2).split("\\.")
      ((url(0), sp(3)), 1)
    })
    //将学科和老师为联合key进行聚合
    val reduced: RDD[((String, String), Int)] = subjectAndTeacherOne.reduceByKey(_ + _)
    //按照学科进行分组
    val grouped: RDD[(String, Iterable[(String, Int)])] = reduced.map(e => {
      (e._1._1, (e._1._2, e._2))
    }).groupByKey()
    //遍历新rdd中的value迭代器,然后将数据迭代出来,每取一条数据都将数据加入到TreeSet集合中,实现的功能类似于有界 优先队列一样,这样就可以在
    //executor端进行并行的排序而且不会出现内存溢出的情况,是没取一条数据然后和集合中的元素进行对比
    //传入要求的topN参数
    val topN = args(2).toInt
    val res: RDD[(String, List[(String, Int)])] = grouped.mapValues(it => {
      //定义一个可排序的TreeSet集合,treeSet集合默认也有一个隐式参数,默认调用的也是ordering排序规则,为了实现需求,需要我们自定义排序规则
      implicit val ord: Ordering[(String, Int)] = Ordering[Int].on[(String, Int)](t => t._2).reverse
      val treeSet: mutable.TreeSet[(String, Int)] = new mutable.TreeSet[(String, Int)]() //treeSet后面也有一个隐式参数列表
      //遍历迭代器,将取出来的每条数据放入到treeSet集合中
      it.foreach(tp => {
        //将当前取出的每一条数据放入treeSet集合中
        treeSet.add(tp)
        //判断treeSet集合的长度,如果大于要求的topN,则移除最小的元素
        if (treeSet.size > topN) {
          val last: (String, Int) = treeSet.last
          treeSet -= last
        }
      })
      treeSet.toList
    })
    println(res.collect().toBuffer)
    //释放资源
    sc.stop()

  }

}

3.4 第四种方法,是按照自己定义的分区规则,重新分区,  目的是为了让每一个学科都能单独进入到一个分区中,一个分区只有一个学科

package com.zxx.spark.day06

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

import scala.collection.mutable

/**
 * 第四种方法,是按照自己定义的分区规则,重新分区,
 * 目的是为了让每一个学科都能单独进入到一个分区中,一个分区只有一个学科
 */
object FavoriteTeacherDemo4 {
  def main(args: Array[String]): Unit = {

    //求每一学科最受欢迎的老师TopN
    //先创建SparkContext和集群建立链接
    val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName)
    //添加一个判断,判断是否设置为集群或是,还是本地模式
    val flag: Boolean = args(0).toBoolean
    if (flag) {
      conf.setMaster("local[*]")
    }
    val sc: SparkContext = new SparkContext(conf)
    //创建RDD
    val rdd: RDD[String] = sc.textFile(args(1))
    //    http: / / bigdata.51doit.cn/laozhang
    //将读取到的数据进行切割和处理
    val subjectAndTeacherOne: RDD[((String, String), Int)] = rdd.map(e => {
      val sp: Array[String] = e.split("/")
      val url: Array[String] = sp(2).split("\\.")
      ((url(0), sp(3)), 1)
    })
    //将学科和老师为联合key进行聚合
    val reduced: RDD[((String, String), Int)] = subjectAndTeacherOne.reduceByKey(_ + _)
    //先触发一次action生产job然后对数据进行采样,计算出数据中的所有学科类别
    val subject: Array[String] = reduced.map(e => {
      e._1._1
    }).distinct().collect()
    //按照指定的规则重新分区
    val SubPartitioner: SubPartitioner = new SubPartitioner(subject)
    //这个新的rdd是按照自己的分区规则重新分好区的,这样下游rdd的分区数量就是我们自己定义的学科数量,分区规则就是我们自己定义的对应的分区编号
    val parRDD: RDD[((String, String), Int)] = reduced.partitionBy(SubPartitioner)
    val topN = args(2).toInt
   /* parRDD.foreachPartition(it=>{//
      //定义一个可排序的TreeSet集合,treeSet集合默认也有一个隐式参数,默认调用的也是ordering排序规则,为了实现需求,需要我们自定义排序规则
      implicit val ord: Ordering[((String, String), Int)] = Ordering[Int].on[((String, String), Int)](t => t._2).reverse
      val treeSet: mutable.TreeSet[((String, String), Int)] = new mutable.TreeSet[((String, String), Int)]()
      //遍历迭代器,将取出来的每条数据放入到treeSet集合中
      it.foreach(tp => {
        //将当前取出的每一条数据放入treeSet集合中
        treeSet.add(tp)
        //判断treeSet集合的长度,如果大于要求的topN,则移除最小的元素
        if (treeSet.size > topN) {
          val last: ((String, String), Int) = treeSet.last
          treeSet -= last
        }
      })

      println(treeSet.toList)
    })*/
    //对每个分区进行排序,按照有界优先队列的方式在Treeset集合中进行排序,这样就可以实现每个分区可以在executor端进行并行排序,而且不会造成内存溢出的情况
    val res: RDD[((String, String), Int)] = parRDD.mapPartitions(it => {
      //定义一个可排序的TreeSet集合,treeSet集合默认也有一个隐式参数,默认调用的也是ordering排序规则,为了实现需求,需要我们自定义排序规则
      implicit val ord: Ordering[((String, String), Int)] = Ordering[Int].on[((String, String), Int)](t => t._2).reverse
      val treeSet: mutable.TreeSet[((String, String), Int)] = new mutable.TreeSet[((String, String), Int)]()
      //遍历迭代器,将取出来的每条数据放入到treeSet集合中
      it.foreach(tp => {
        //将当前取出的每一条数据放入treeSet集合中
        treeSet.add(tp)
        //判断treeSet集合的长度,如果大于要求的topN,则移除最小的元素
        if (treeSet.size > topN) {
          val last: ((String, String), Int) = treeSet.last
          treeSet -= last
        }

      })
      treeSet.iterator
    })
    println(res.collect().toBuffer)
    //释放资源
    sc.stop()
  }
}


/**
 * 创建一个新的类用来继承 Partitioner这个类然后实现他的两个方法,这个类就是我们的定义分区规则的类
 * 这里需要传入一个参数,为了指定分区数,这里的分区数是按照学科的类别进行分区的,目的是为了将每一个学科都让进去到一个分区中,这个分区有且仅有一个学科
 */
class SubPartitioner(subject: Array[String]) extends Partitioner {
  //创建一个map集合
  private val map: mutable.HashMap[String, Int] = new mutable.HashMap[String, Int]()
  //初始化一个分区规则
  var index = 0
  for (elem <- subject) {
    map.put(elem, index)
    index += 1
  }

  //有多少学科就有多少分区
  override def numPartitions: Int = subject.length

  //getPartition方法是在Executor中的Task在ShuffleWrite之前执行的
  override def getPartition(key: Any): Int = {
    val subject: String = key.asInstanceOf[(String, String)]._1

    val maybeInt: Int = map.get(subject).get
    maybeInt
  }
}

3.5 第五种方法是,在调用reduceBykey的时候传入自定义分区器,自定义分区器,可以将每一个学科都能进入一个分区中,每一个分区有且仅有一个学科(reduceBykey默认使用的是HashPartitioner分区器)

package com.zxx.spark.day06

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

import scala.collection.mutable

/**
 * 第五种方法是,在调用reduceBykey的时候传入自定义分区器,
 * 自定义分区器,可以将每一个学科都能进入一个分区中,
 * 每一个分区有且仅有一个学科(reduceBykey默认使用的是HashPartitioner分区器)
 */
object FavoriteTeacherDemo5 {
  def main(args: Array[String]): Unit = {

    //求每一学科最受欢迎的老师TopN
    //先创建SparkContext和集群建立链接
    val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName)
    //添加一个判断,判断是否设置为集群或是,还是本地模式
    val flag: Boolean = args(0).toBoolean
    if (flag) {
      conf.setMaster("local[*]")
    }
    val sc: SparkContext = new SparkContext(conf)
    //创建RDD
    val rdd: RDD[String] = sc.textFile(args(1))
    //    http: / / bigdata.51doit.cn/laozhang
    //将读取到的数据进行切割和处理
    val subjectAndTeacherOne: RDD[((String, String), Int)] = rdd.map(e => {
      val sp: Array[String] = e.split("/")
      val url: Array[String] = sp(2).split("\\.")
      ((url(0), sp(3)), 1)
    })

    val subject: Array[String] = subjectAndTeacherOne.map(e => {
      e._1._1
    }).distinct().collect()
    //按照指定的规则重新分区
    val subPartitioner: SubPartitioner2 = new SubPartitioner2(subject)
    //这个新的rdd是按照自己的分区规则重新分好区的,这样下游rdd的分区数量就是我们自己定义的学科数量,分区规则就是我们自己定义的对应的分区编号
    //在调用reduceByKey的同时传入自定义分区器,reduceByKey底层调用的是combineByKeyWithClassTag,在底层是调用的shuffleRDD,默认使用的就是我们传入的分区器
    //我们传入的分区器,可以让每一个学科都在独立的分区中,然后在按照Key相同的一定会分在同一个分区的同一个组中,在局部聚合,然后全局聚合(因为每个分区只有独立的学科,所有在局部聚合完成之后就
    //没有全局聚合了,节省了shuffle的次数,节省资源
    val topN = args(2).toInt
    val reduced: RDD[((String, String), Int)] = subjectAndTeacherOne.reduceByKey(subPartitioner, _ + _)
    //在每个分区内new 一个TreeSet集合,这个集合可以自动排序,并且可以去重,可以实现有界优先队列的功能
    val res: RDD[((String, String), Int)] = reduced.mapPartitions(it => {
      //指定treeSet集合的排序规则
      implicit val ord: Ordering[((String, String), Int)] = Ordering[Int].on[((String, String), Int)](t => t._2).reverse
      //创建一个TreeSet集合
      val treeSet: mutable.TreeSet[((String, String), Int)] = new mutable.TreeSet[((String, String), Int)]()
      //遍历迭代器中的每一条数据,将数据放入集合中
      it.foreach(e => {
        treeSet.add(e)
        //判断集合的长度,当达到一定的长度的时候去除集合中最小的元素
        if (treeSet.size > topN) {
          //去除集合中最小的元素
          val last: ((String, String), Int) = treeSet.last
          treeSet.remove(last)
        }
      })
      treeSet.iterator
    })
    println(res.collect().toBuffer)
    //释放资源
    sc.stop()

  }
}

/**
 * 创建一个新的类用来继承 Partitioner这个类然后实现他的两个方法,这个类就是我们的定义分区规则的类
 * 这里需要传入一个参数,为了指定分区数,这里的分区数是按照学科的类别进行分区的,目的是为了将每一个学科都让进去到一个分区中,这个分区有且仅有一个学科
 */
class SubPartitioner2(subject: Array[String]) extends Partitioner {
  //创建一个map集合
  private val map: mutable.HashMap[String, Int] = new mutable.HashMap[String, Int]()
  //初始化一个分区规则
  var index = 0
  for (elem <- subject) {
    map.put(elem, index)
    index += 1
  }

  //有多少学科就有多少分区
  override def numPartitions: Int = subject.length

  //getPartition方法是在Executor中的Task在ShuffleWrite之前执行的
  override def getPartition(key: Any): Int = {
    val subject: String = key.asInstanceOf[(String, String)]._1

    val maybeInt: Int = map.get(subject).get
    maybeInt
  }
}

3.6 第六种方法是,调用底层的算子,new shuffleRDD,将自定义分区器,和自定义的排序规则传入shuffleRDD中

package com.zxx.spark.day06

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

import scala.collection.mutable

/**
 * 第六种方法是,调用底层的算子,new shuffleRDD,将自定义分区器,和自定义的排序规则传入shuffleRDD中
 *
 */
object FavoriteTeacherDemo6 {
  def main(args: Array[String]): Unit = {

    //求每一学科最受欢迎的老师TopN
    //先创建SparkContext和集群建立链接
    val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName)
    //添加一个判断,判断是否设置为集群或是,还是本地模式
    val flag: Boolean = args(0).toBoolean
    if (flag) {
      conf.setMaster("local[*]")
    }
    val sc: SparkContext = new SparkContext(conf)
    //创建RDD
    val rdd: RDD[String] = sc.textFile(args(1))
    //    http: / / bigdata.51doit.cn/laozhang
    //将读取到的数据进行切割和处理
    val subjectAndTeacherOne: RDD[((String, String), Int)] = rdd.map(e => {
      val sp: Array[String] = e.split("/")
      val url: Array[String] = sp(2).split("\\.")
      ((url(0), sp(3)), 1)
    })
    //先将学科和老师作为key进行聚合,这样可以求出每个学科中各个老师的总数量
    val reduced: RDD[((String, String), Int)] = subjectAndTeacherOne.reduceByKey(_ + _)
    //先调用一次action算子,对数据进行一次采样
    val subject: Array[String] = reduced.map(_._1._1).distinct().collect()
    //构建自定义的分区器
    val subPartitioner: SubPartitioner3 = new SubPartitioner3(subject)
    //调用底层的shuffleRDD,只进行排序,但是不调用聚合函数
    //将参与排序的字段放入key中
    val rddM: RDD[((Int, String, String), Null)] = reduced.map(e => {
      ((e._2, e._1._1, e._1._2), null)
    })
    //这个算子功能是重新分区并在每个分区内排序
    implicit val ord: Ordering[(Int, String, String)] = Ordering[Int].on[(Int, String, String)](t => t._1).reverse
    val res2: RDD[((Int, String, String), Null)] = rddM.repartitionAndSortWithinPartitions(subPartitioner)
    //    res2.saveAsTextFile("d://out2")
    //底层调用了shuffleRDD,传入自定义的分区规则,并且设置了在shuffle之前的排序规则
    val res: ShuffledRDD[(Int, String, String), Null, Null] = new ShuffledRDD[(Int, String, String), Null, Null](rddM, subPartitioner)
    res.setKeyOrdering(ord) //在shuffle前进行排序,每个分区排序,然后全局排序,在spark中参与排序的字段必须在key里面
    res.saveAsTextFile("d://out")
    //释放资源
    sc.stop()

  }
}

/**
 * 创建一个新的类用来继承 Partitioner这个类然后实现他的两个方法,这个类就是我们的定义分区规则的类
 * 这里需要传入一个参数,为了指定分区数,这里的分区数是按照学科的类别进行分区的,目的是为了将每一个学科都让进去到一个分区中,这个分区有且仅有一个学科
 */
class SubPartitioner3(subject: Array[String]) extends Partitioner {
  //创建一个map集合
  private val map: mutable.HashMap[String, Int] = new mutable.HashMap[String, Int]()
  //初始化一个分区规则
  var index = 0
  for (elem <- subject) {
    map.put(elem, index)
    index += 1
  }

  //有多少学科就有多少分区
  override def numPartitions: Int = subject.length

  //getPartition方法是在Executor中的Task在ShuffleWrite之前执行的
  override def getPartition(key: Any): Int = {
    val subject: String = key.asInstanceOf[(Int, String, String)]._2

    val maybeInt: Int = map.get(subject).get
    maybeInt
  }
}

 

你可能感兴趣的:(spark-core,spark)