rdd算子之byKey系列

spark中有一些xxxByKey的算子。我们来看看。

groupByKey

解释

假设我们要对一些字符串列表进行分组:

object GroupByKeyOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")

    val context = new SparkContext(conf)

    val rdd: RDD[String] = context.makeRDD(List(
      "spark", "scala", "hive", "flink",
      "kafka", "kafka", "hbase", "flume",
      "sqoop", "hadoop", "kafka", "spark",
      "flink", "kafka", "kafka", "hbase"
    ),4)

    val mapRDD: RDD[(String, Int)] = rdd.map((_,1))

    val groupByRDD: RDD[(String, Iterable[Int])] = mapRDD.groupByKey()

    groupByRDD.saveAsTextFile("groupby_out")

    TimeUnit.MINUTES.sleep(50)

    context.stop()
  }
}

那会出现什么情况呢?为了查看DAG图,我让程序睡眠了。

在这里插入图片描述

groupByKey存在了shuffle操作。后续我们会详细解释shuffle。

当下,我们只需知道在stage1出现了一个ShuffledRDD
在这里插入图片描述

进入groupByKey的源码:

  def groupByKey(): RDD[(K, Iterable[V])] = self.withScope {
    groupByKey(defaultPartitioner(self))
  }

首先是要拿到默认的分区器。
在这里插入图片描述

假设我们没有自己的分区器,所以就会使用HashPartitioner

再假设也没有设定

spark.default.parallelism

的值,

那么就会取几个rdd中分区数最大的那个值,然后根据这个值来构造HashPartitioner。为什么会有几个rdd呢?rdd是可以join的。

在这里插入图片描述

在这里插入图片描述

所谓的分区,就是key的hashcode值模上分区数,再保证非负即可。

拿到了分区器,我们进入groupByKey

在这里插入图片描述

他这里有3个函数:

val createCombiner = (v: V) => CompactBuffer(v)

首先是把组内的第一个数放进CompactBuffer

val mergeValue = (buf: CompactBuffer[V], v: V) => buf += v

然后将组内其他的数放入CompactBuffer

val mergeCombiners = (c1: CompactBuffer[V], c2: CompactBuffer[V]) => c1 ++= c2

最后是多个CompactBuffer的大合并。

进入combineByKeyWithClassTag,我们看到:

在这里插入图片描述

他把3个函数丢给了Aggregator,然后把Aggregator丢给了ShuffledRDD

不妨看一下程序运行的结果:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

为什么是4个分区,因为我们只有一个rdd,这个rdd就是4个分区的(指定的)。

为什么hiveflink都在0号分区,这是HashPartitionergetPartition计算的结果。

为什么所有的1被包裹在CompactBuffer中,因为groupByKey就是用CompactBuffer来保存分组结果的。

至于这个CompactBuffer

在这里插入图片描述

由于包访问权限,我们也用不了。但他说了,这个东西和ArrayBuffer很像。所以我们用ArrayBuffer来替代CompactBuffer

实现

既然我们已经看到了所有事实,我们就来模仿一个groupByKey吧。

因为最后需要的是ShuffledRDD,它要什么我们给他什么。

在这里插入图片描述

主构造函数要一个之前的rdd和一个分区器,这个好说。

那么那3个泛型是啥?

在这里插入图片描述

看看人家是怎么写的。

那三个泛型和Aggregator的三个泛型一样。

所以先new一个Aggregator。所以要先准备三个函数。

那三个函数甚至可以直接从groupByKey偷来:

在这里插入图片描述

在这里插入图片描述

根据三个函数的类型,我们得知V在我们实现的代码里就是Int,C就是ArrayBuffer(用ArrayBuffer替代CompactBuffer)。

所以K是什么?

在这里插入图片描述

ShuffledRDD里面其实说清楚了,K是key class,我们这里就是String咯。

所以最终的代码是:


object GroupByKeyOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")

    val context = new SparkContext(conf)

    val rdd: RDD[String] = context.makeRDD(List(
      "spark", "scala", "hive", "flink",
      "kafka", "kafka", "hbase", "flume",
      "sqoop", "hadoop", "kafka", "spark",
      "flink", "kafka", "kafka", "hbase"
    ),4)

    val mapRDD: RDD[(String, Int)] = rdd.map((_,1))

    val createCombiner = (v:Int) => ArrayBuffer[Int](v)
    val mergeValue = (buf: ArrayBuffer[Int], v: Int) => buf += v
    val mergeCombiners = (c1: ArrayBuffer[Int], c2: ArrayBuffer[Int]) => c1 ++= c2

    val aggregator: Aggregator[String, Int, ArrayBuffer[Int]] = new Aggregator[String, Int, ArrayBuffer[Int]](createCombiner, mergeValue, mergeCombiners)

    val shuffledRDD: ShuffledRDD[String, Int, ArrayBuffer[Int]] = new ShuffledRDD[String, Int, ArrayBuffer[Int]](mapRDD, new HashPartitioner(rdd.partitions.length))

    shuffledRDD.setMapSideCombine(false)
    shuffledRDD.setAggregator(aggregator)

    shuffledRDD.saveAsTextFile("shuffledRDD_out")

    context.stop()
  }
}

至于为什么要

shuffledRDD.setMapSideCombine(false)

在这里插入图片描述

人家有说,就是map端不要combine,你是分组,你就算用了combine也不会减少数据量。然后会让map端的所有数据插进大大的hash table中,使老年代压力很大。

关于groupByKey最后要注意的是这其实是个隐式转换。

在这里插入图片描述

groupByKey是由RDD调用的,但是RDD里面没有这个方法,这个方法在PairRDDFunctions中。

所以,RDD一定是被PairRDDFunctions包裹了:

在这里插入图片描述

groupBy

说到groupbyKey,那就要顺便说一下groupBy

看一个简单的例子:

object GroupByOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")

    val context = new SparkContext(conf)

    val rdd = context.makeRDD(List("Hello", "Spark", "Hi", "Scala"))

    val f = (str: String) => str.charAt(0)

    val groubyRdd = rdd.groupBy(f)
    
    groubyRdd.collect().foreach(println)
    context.stop()
  }
}

结果:

(S,CompactBuffer(Spark, Scala))
(H,CompactBuffer(Hello, Hi))

也就是说,groupBy要By什么更加灵活,而groupByKey只能是按对偶元组的第一个元素分组。

点进源码看看:

在这里插入图片描述

他其实还是变成了一个对偶元组然后调用groupByKey,所以我们的测试代码本质上是:

object GroupByOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")

    val context = new SparkContext(conf)

    val rdd = context.makeRDD(List("Hello", "Spark", "Hi", "Scala"))

    val f = (str: String) => str.charAt(0)

    //    val groubyRdd = rdd.groupBy(f)

    val groupbyRdd: RDD[(Char, Iterable[String])] = rdd.map(t => (f(t), t)).groupByKey()

    groupbyRdd.collect().foreach(println)
    context.stop()
  }
}

reduceByKey

我们刚刚看了PairRDDFunctions这个类了:

在这里插入图片描述

他这里有大量的xxByKey操作。我们再看看reduceByKey

来个例子:

object ReduceByKeyOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")

    val context = new SparkContext(conf)
    val dataRDD = context.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("a", 4), ("c", 5)))

    val func = (v: Int, w: Int) => v + w

    val reduceRDD: RDD[(String, Int)] = dataRDD.reduceByKey(func)

    reduceRDD.collect().foreach(println)

    context.stop()
  }
}

结果:

(a,5)
(b,2)
(c,8)

所以reduceByKey只是比groupByKey多了聚合的功能。

在这里插入图片描述

底层调用的还是combineByKeyWithClassTag

参数中的第一个函数就是将一个值取出来占个位(什么也不做)。

我们写的func在局部聚合和全局聚合都使用了。

最底层当然还是一个ShuffledRDD

distinct

distinct就是去重。

比如:

object DistinctOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")

    val context = new SparkContext(conf)
    val rdd = context.makeRDD(List(1, 2, 3, 4, 1, 2, 3, 4, 5))

    rdd.distinct().collect().foreach(println)
    context.stop()
  }
}

为什么要在reduceByKey下面讲distinct呢?

我们点进distinct的源码:

在这里插入图片描述

如果修改我们的程序,大概是这样的:

object DistinctOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")

    val context = new SparkContext(conf)
    val rdd = context.makeRDD(List(1, 2, 3, 4, 1, 2, 3, 4, 5))

    //    rdd.distinct().collect().foreach(println)

    rdd.map(x => (x, null)).reduceByKey((x, _) => x).map(_._1).foreach(println)
    context.stop()
  }
}

他这里去重用的就是reduceByKey

aggregateByKey

aggregateByKey的使用更加地有弹性。

在这里插入图片描述

首先他有初始值,这和foldByKey一样,然后还有区内函数和分区间的函数。

我们需要知道,初始值被用在了什么地方:

在这里插入图片描述

可以看到初始值只被作用在区内函数中。

举个例子:

object AggregateByKeyOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
    val context = new SparkContext(conf)
    val rdd: RDD[(String, Int)] = context.makeRDD(List(("a", 1), ("a", 2), ("c", 3), ("b", 4), ("c", 5), ("c", 6)),2)

    val seqOp = (x: Int, y: Int) => math.max(x, y)

    val combOp = (x: Int, y: Int) => x + y

    val aggRDD: RDD[(String, Int)] = rdd.aggregateByKey(10)(seqOp, combOp)


    aggRDD.saveAsTextFile("agg-out")

  }
}

先看看运行的结果吧:

在这里插入图片描述

在这里插入图片描述

aggregate是如何做的呢?

我们造的rdd有两个分区,所以第一个分区为:

("a", 1), ("a", 2), ("c", 3)

第二个分区为:

("b", 4), ("c", 5), ("c", 6)

分区内是比较相同hashcode值的key的value的最大值,并且初始值也会参与分区内的运算,所以第一个分区中

("a", 1), ("a", 2), ("a", 10)

比较的结果就是:

("a", 10)
("c", 3), ("c", 10)

比较的结果就是

("c", 10)

至于第二个分区,最后会剩下:

("b", 10), ("c", 10)

全局聚合要求相同的key的value值相加。

所以有

("a", 10), ("c", 10+10), ("b", 10)

combineByKey

在这里插入图片描述

combineByKey有点不同的就是传入的第一个函数可以变换数据结构。

我们来个需求:

求相同的key的值的平均值。

object CombineByKeyOperator {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")

    val context = new SparkContext(conf)
    val rdd = context.makeRDD(List(
      ("a", 1), ("a", 2), ("b", 3),
      ("b", 4), ("b", 5), ("a", 6)
    ), 2)

    val createCombiner = (x: Int) => (x, 1)

    val mergeValue = (t: (Int, Int), v: Int) => (t._1 + v, t._2 + 1)

    val mergeCombiners = (t1: (Int, Int), t2: (Int, Int)) => (t1._1 + t2._1, t1._2 + t2._2)

    //获取相同key的平均值
    val aggRdd: RDD[(String, (Int, Int))] = rdd.combineByKey(createCombiner, mergeValue, mergeCombiners)


    val result: RDD[(String, Int)] = aggRdd.mapValues {
      case (sum, count) => {
        sum / count
      }
    }
    result.collect().foreach(println)

    context.stop()
  }
}

我需要陈述传入的三个函数是如何工作的。

因为我们造的数据分了两个分区,所以

("a", 1), ("a", 2), ("b", 3)

为一个分区。

 ("b", 4), ("b", 5), ("a", 6)

为另一个分区。

首先看

("a", 1)

a这个key出现过吗?显然没有。所以应用createCombiner

于是("a", 1)变成了a==>(1,1)。这个表示的意思就是a这个key所对应的值是(1,1)

然后看第二个数("a", 2)

还是问a出现过吗?啊,a出现过。所以应用第二个函数mergeValue。那么如果做呢?

((1,1),2) => (1+2, 1+1)

得到的(3,2)中3是数值的相加,2是a的个数的相加。

再看第二个分区中key为a的情况,因为只有一个a,所以最后的结果就是:

a==>(6,1)

区内的计算结束了。

分区间的聚合需要mergeCombiners函数。也就是(3,2)要和(6,1)应用该函数(我们一直在谈论key为a的情况)。

所以最后就是:

((3,2),(6,1))=>(3+6,2+1)

这样我们就把key为a的所有元组的值的和与出现的次数都统计出来了,最后相除即为平均数。

如此一来combineByKey的工作原理就清晰了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值