聊一聊Spark实现TopN的几种方式

大家好,我是风云,欢迎大家关注我的个人原创公众号【笑看风云路】获取更多大数据技术干货,在未来的日子里我们一起来学习大数据相关的技术,一起努力奋斗,遇见更好的自己!
扫码进,更快捷:
笑看风云路

前言

在实际开发过程中,我们会经常碰到求TopN这样常见的需求,那在Spark中,是如何实现求TopN呢?带着这个问题,就来看一下TopN的实现方式都有哪些!

方式1:采用groupByKey

思路:

  1. 按照key对数据进行聚合(groupByKey)

  2. 对同组的key的所有value先转换为List,然后进行排序,最后取TopN

代码实现:

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

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

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

val result = rdd2.map(arr => (arr(0).trim, arr(1).trim.toInt))
    .groupByKey()
    .map {
        case (key, values) => {
            // 对values中的数据进行排序,然后获取最大的前三个数据
            val sortedValues = values.toList.sorted
            val top3Values = sortedValues.takeRight(3).reverse
            (key, top3Values)
        }
    }
// 打印输出
result.collect().foreach(println)
sc.stop()

方式2:采用两阶段聚合优化

思路:

  1. 第一阶段给每个key加上一个随机值前缀,然后进行局部的聚合操作
  2. 第二阶段去除每个key的前缀,然后进行全局的聚合操作

代码实现:

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

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

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

val result = rdd2.mapPartitions(iter => {
      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) => {
            val sortedValues = values.toList.sorted
            val top3Values = sortedValues.takeRight(3).reverse
            top3Values.map(count => (key,count))
          }
        }
        .groupByKey()
        .flatMap{
          case (key, values) => {
            val sortedValues = values.toList.sorted
            val top3Values = sortedValues.takeRight(3).reverse
            top3Values.map((key,_))
          }
        }
// 打印输出
result.collect().foreach(println)
sc.stop()

方式3:先获取每个分区的TopN,后获取全局TopN

思路:

  1. 对于每一个key获取每个分区中的TopN
  2. 做全局的数据聚合操作,获取TopN

代码实现:

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

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

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

val result3 = rdd2.map(arr => {
    val key = arr(0).trim
    val count = arr(1).trim.toInt
    (key, count)
})
.mapPartitions(iter => {
    import scala.collection.mutable

    val temp = iter.foldLeft(mutable.Map[String, ArrayBuffer[Int]]())((a, b) => {
        val key = b._1
        val count = b._2
        val buf = a.getOrElseUpdate(key, new mutable.ArrayBuffer[Int]())
        buf += count
        if (buf.size > 3) {
            val max3Vals = buf.sorted.takeRight(3)
            a(key) = max3Vals
        }
        a
    })
    val top3IterPrePartition = temp.toList.flatMap {
        case (key, countIters) => countIters.map(count => (key, count))
    }
    top3IterPrePartition.toIterator
})
.groupByKey()
.flatMap {
    case (key, values) => {
        val sorted = values.toList.sorted
        val top3 = sorted.takeRight(3).reverse
        top3.map((key, _))
    }
}
result3.foreachPartition(iter => iter.foreach(println))
sc.stop()

方式4:采用aggregateByKey

思路:

  1. 初始值为mutable.ArrayBufferInt
  2. 对每组key中的每个value和之前的聚合值进行聚合操作,就是在分区中,来一个value和上次取出的TopN进行一次排序,取出新的TopN
  3. 对每个分区操作后的局部聚合结果进行合并聚合操作,就是在分区间,来一个分区和上次取出的TopN进行一次合并排序,取出新的TopN

代码实现:

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

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

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

import scala.collection.mutable
val result4 = rdd2.map(arr => {
    val key = arr(0).trim
    val count = arr(1).trim.toInt
    (key, count)
}).aggregateByKey(mutable.ArrayBuffer[Int]())(
    (u, v) => {
        u += v
        u.sorted.takeRight(3).reverse
    },
    (u1, u2) => {

        u1 ++= u2
        u1.sorted.takeRight(3).reverse
    }
).flatMap {
    case (key, values) => {
        values.toList.map((key, _))
    }
}
result4.foreachPartition(iter => iter.foreach(println))
sc.stop()

优缺点

方式1的缺点:

  1. groupByKey会将相同key的所有value全部加载到内存进行处理,当value特别多的时候可能出现OOM异常
  2. groupByKey会将所有的value数据均发送给下一个RDD,性能比较低,因为在实际聚合操作中只需要部分数据

方式2的优缺点:

  1. 对于聚合类Shuffle操作(groupByKey,reduceByKey等)产生的问题能够很好的解决
  2. 对于非聚合类(join等)产生的问题很难使用该方法解决

方式3、方式4:

  1. 解决了方式1实现方式的两个缺点
  2. 都采用了先分区内预聚合,然后进行全局聚合的思想

结语

好了,今天就为大家分享到这里了。咱们下期见!
如果本文对你有帮助的话,欢迎点赞&收藏&分享,这对我继续分享&创作优质文章非常重要。感谢🙏🏻

–END–

非常欢迎大家加我个人微信,有关大数据的问题我们一起讨论。
在这里插入图片描述

扫码上方二维码,加我微信

  • 1
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笑看风云路

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值