Spark实现分组排序TopN

目录

1、第一种实现方式(采用groupByKey API)

2、第二种实现方式(采用两阶段聚合优化)

3、第三种实现方式(只获取每个分区的前N个数据)

4、第四种实现方式(采用aggregateByKey API)

5、第五种实现方式(采用二次排序实现)待更新


代码中使用的源数据groupsort.txt内容如下

aa 78
bb 98
aa 80
cc 98
aa 69
cc 87
bb 97
cc 86
aa 97
bb 78
bb 34
cc 85
bb 92
cc 72
bb 32
bb 23

1、第一种实现方式(采用groupByKey API)

(1)思路

        先将rdd转换为key/value键值对类型的rdd,然后按照key对数据进行聚合,对同组key的所有value数据进行排序,对排序之后的value集合获取出现次数最多的前3个数据。

(2)缺点

        ①  groupByKey这个API在现在这个版本的实现中,同组(相同key)的所有value全部加载到内存进行处理,当value特别多的时候就有可能出现OOM异常;

        ②  在对同组key数据进行聚合操作的业务场景中,groupByKey的性能有点低,groupByKey操作在会将所有的value数据均发送给下一个RDD,但是实际上我们在做聚合操作,只需要部分数据;

(3)代码实现

// 1. 构造上下文
    val conf = new SparkConf()
        .setMaster("local")
        .setAppName("group-sorted-topn")
    val sc = SparkContext.getOrCreate(conf)

    // 2. 创建rdd
    val path = "datas/groupsort.txt"
    val rdd = sc.textFile(path)

    // 3. rdd操作得出结果
    val rdd2 = rdd
      .map(_.split(" "))
      .filter(_.length == 2)

    // 缓存数据
    rdd2.cache()

    // 3.1 分组排序TopN的第一种实现方式
    // 思路:先将rdd转换为key/value键值对类型的rdd,然后按照key对数据进行聚合,对同组key的所有value数据进行排序,对排序之后的value集合获取出现次数最多的前3个数据
    // 缺点1:groupByKey这个API在现在这个版本的实现中,同组(相同key)的所有value全部加载到内存进行处理,
    // 当某一个key对应的value特别多的时候就有可能出现OOM异常
    // 缺点2:在对同组key数据进行聚合操作的业务场景中,
    // groupByKey的性能有点低,groupByKey操作在会将所有的value数据均发送给下一个RDD,但是实际上我们在做聚合操作,只需要部分数据
    val result1 = rdd2
      .map(arr => (arr(0).trim,arr(1).trim.toInt))
      .groupByKey()
      .map {
        case (key, values) => {
          // 对values中的数据进行排序,然后获取最大的前3个数据
          val sortedValues = values.toList.sorted
          val top3Values = sortedValues.takeRight(3).reverse
          (key, top3Values)
        }
      }

    result1.foreachPartition(iter=>{
      // 对数据进行迭代操作,功能类似mapPartitions,区别是foreachPartition只是迭代,没有返回值(返回值是Unit)
      iter.foreach(println)
    })

    // 考虑如何将输出数据的格式设置为Key/value键值对,key类型为string,value类型为int
    /**
     * eg:
     * aa 97
     * aa 80
     * aa 78
     * bb 98
     * bb 97
     * .......
     */
      result1
          .flatMap{
            case (key,vaules) => {
              vaules.map((key,_))
            }
          }
          .foreach(println)

2、第二种实现方式(采用两阶段聚合优化)

  (1)两阶段聚合

        ①  实现思路:第一阶段给每个key加一个随机数,然后进行局部的聚合操作;第二阶段去除每个key的前缀,然后进行全局的聚合操作;
        ②  实现原理:第一步将key添加随机前缀的方式可以让一个key变成多个key,可以让原本被一个task处理的数据分布到多个task上去进行局部的聚合,可以解决掉单个task处理数据量太大的问题;第二部去掉前缀,进行全局聚合操作;
        ③  优缺点:对于聚合类shuffle操作(groupByKey、reduceByKey等)产生的问题能够很好的解决;但是对于非聚合类shuffle操作(join等)产生的问题很难使用该方式解决;
        ④  适用场景:对RDD进行聚合类型操作的时候,某些Task处理数据过多或者产生OOM异常等情况可以采用该方式规避掉;

(2)代码实现

// 如果需要将random这个对象传入executor中执行(transformation类型API的代码具体执行时在executor中),
// random定义在driver中(main函数中,不是rdd的transformation api中)的时候,
// 要求random是可以进行序列化的,如果不可序列化,会报序列化异常
// val random = Random
    val result2 = rdd2
      .mapPartitions(iter => {
        // 对一个分区的数据进行一个操作, 这部分代码的执行时在executor上执行的
        val random = Random
        iter.map(arr => {
          val key = arr(0).trim
          val value = arr(1).trim.toInt
          ((random.nextInt(5), key), value)
        })
      })
      .groupByKey()
      .flatMap {
        case ((_, key), values) => {
          // 局部聚合后的数据处理,对values中的数据进行排序,然后获取最大的前3个数据
          // 操作完后,实际上是获取当前局部key中的出现次数最多的前3个数据
          // 输出的时候,将key的前缀去掉
          val sortedValues = values.toList.sorted
          val top3Values: List[Int] = sortedValues.takeRight(3).reverse
          // 因为在后续的全局聚合中,还需要进行数据的排序操作,
          // 排序的操作考虑的是单key-value的元素,所以使用flatMap函数进行扁平化操作
          top3Values.map(count => (key, count))
        }
      }
      .groupByKey()
      .flatMap {
        case (key, values) => {
          // 全局数据聚合
          // 对values中的数据进行排序,然后获取最大的前3个数据
          val sortedValues = values.toList.sorted
          val top3Values = sortedValues.takeRight(3).reverse
          top3Values.map((key,_))
        }
      }
    
    // 结果输出
    result2.foreachPartition(iter => {iter.foreach(println)})

3、第三种实现方式(只获取每个分区的前N个数据)

(1)思路

        第一步,对于每一个key获取每个分区中出现次数最多的前3个数据;第二步,做一个全局的数据聚合操作;

(2)代码实现

    // 分组排序TopN的第三种实现方式 ==> 解决groupByKey实现方式的两个缺点
    // 思路:第一步,获取每个分区中出现次数最多的前3个数据,对于每个key而言;第二步,做一个全局的数据聚合操作
    val result3 = rdd2
      .map(arr => {
        // 将数组转换为key/value键值对类型
        val key = arr(0).trim
        val count = arr(1).trim.toInt
        (key, count)
      })
      .mapPartitions(iter => {
        // 对每个分区中的数据做一个出现次数最多的前3个值的获取
        import scala.collection.mutable

        // 对iter进行迭代,然后将出现key对应的count值最大的前3个添加到tmpIter中
        val tmpIter = iter.foldLeft(mutable.Map[String, mutable.ArrayBuffer[Int]]())((a, b) => {
          // a表示mutable.Map对象,b是iter中的元素
          // 思路:判别b的key是否在a中存在,如果存在,就将b添加到a中,同时最多只保留值最大的前3个数据
          val key = b._1
          val count = b._2

          // 当前key对应的buf集合,如果当前key不存在,创建一个buf,然后添加到a中
          val buf = a.getOrElseUpdate(key, new mutable.ArrayBuffer[Int]())

          // 将count添加到buf中
          buf += count

          // 如果buf中数据超过3个,那么获取最大的三个值,进行保留
          if (buf.size > 3) {
            val max3Values = buf.sorted.takeRight(3)
            a(key) = max3Values
          }

          // 返回结果
          a
        })

        // 将数据类型转换为key/count的形式
        val top3KeyValuePairIterPrePartition = tmpIter
          .toList  // 转成list
          .flatMap {
          case (key, countIters) => countIters.map(count => (key, count))
        }

        // 结果返回
        top3KeyValuePairIterPrePartition.toIterator
      })
      .groupByKey()
      .flatMap{
        case (key, values) => {
          // 对values中的数据进行排序,然后获取最大的前3个数据
          val sortedValues = values.toList.sorted
          val top3Values = sortedValues.takeRight(3).reverse
          top3Values.map((key,_))
        }
      }

    result3.foreachPartition(iter => iter.foreach(println))

4、第四种实现方式(采用aggregateByKey API)

(1)aggregateByKey API说明

def aggregateByKey[U](zeroValue: U)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)]
    * zeroValue:对每组数据给定一个初始值,zeroValue就是每组数据的初始值,初始值即每个key的初始聚合值
    * seqOp: 对每组key对应value数据进行迭代,将value和之前的聚合值聚合操作,返回新的聚合值
    * ====> 对每个数据进行聚合的时候用到,一般在shuffle之前用到, 也就是在分区中被触发
    * combOp: 对两个聚合值进行数据聚合操作,返回新的聚合值
    * ====> 在shuffle操作后,对多个聚合值进行聚合操作, 也就是多个分区的数据合并

(2)代码实现

    import scala.collection.mutable
    val result4: RDD[(String, Int)] = rdd2
      .map(arr => {
        // 将数组转换为key/value键值对类型
        val key = arr(0).trim
        val count = arr(1).trim.toInt
        (key, count)
      })
      .aggregateByKey(mutable.ArrayBuffer[Int]())(
        (u, v) => {
          // 对每组key中的每个value进行value的数据和之前的(上一条数据的)聚合值聚合操作,聚合返回新的聚合值
          // u:之前的/上一条数据的聚合值,如果没有上一条数据,那么u是给定的初始值
          // v:每组key中的value值
          //  将v添加到u中
          u += v
          u.sorted.takeRight(3).reverse
        },
        (u1, u2) => {
          // 对每个分区操作后的局部聚合结果进行合并聚合操作
          // u1是上一个全局聚合操作后的临时聚合结果或者是第一个分区的局部聚合结果
          // u2是分区的聚合结果
          u1 ++= u2
          u1.sorted.takeRight(3).reverse
        }
      ).flatMap {
      case (key,values) =>{
        values.toList.map((key,_))
      }
    }

    result4.foreachPartition(iter => iter.foreach(println))

5、第五种实现方式(采用二次排序实现)待更新

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值