[Spark算子] 对比 groupByKey / reduceByKey / foldByKey / aggregateByKey 算子区别

本文探讨Spark中四个重要的聚合算子:groupByKey、reduceByKey、foldByKey和aggregateByKey。通过一个小案例解释了reduceByKey在处理键值对数据时如何计算每个键的平均值,强调了分区内和分区间计算的过程。reduceByKey通过将相同key的value相加,最后得到每个key仅保留一个记录的结果。文章还提及aggregateByKey,暗示了它与前三个算子的区别。

在这里插入图片描述

reduceByKey 和 groupByKey 的区别?

在Spark中,shuffle操作必须要进行落盘处理,
这个意思是说,进行shuffle数据移动过程中,我们不可能知道每个Key的value何时统计完,
如果不进行落盘处理,那么数据就会一直等待所有分区的数据移动到相同的分区中,内存肯定是
扛不住的,所以要进行落盘到磁盘,然后等到所有分区数据移动完成后,进行磁盘IO读取,
这样解决了数据等待问题,但是也暴露出shuffle操作的性能低
----------------------------------------------------------------------
reduceByKey与groupbyByKey的区别就是groupbyByKey没有在map端做join,
而reduceByKey是先在map端做局部聚合,然后再合并数据
----------------------------------------------------------------------
从shuffle角度: 
reduceByKey和groupByKey存在shuffle的操作,但是reduceByKey可以在shuffle前对分区内
相同key的数据进行预聚合(combine)功能,这样就会减少落盘的数据量,而groupByKey只是进行
分组,不存在数据量减少的问题,reduceByKey性能比较高

从功能的角度:
reduceByKey其实包含分组和聚合的功能.GroupByKey只能分组,不能聚合,所以在分组聚合的场合下,
推荐使用reduceByKey,如果仅仅是分组而不需要聚合.那么还是只是要groupByKey

小案例:给定一组数据:
(“spark”, 12), (“hadoop”, 26), (“hadoop”, 23), (“spark”,15), (“scala”, 26),
(“spark”, 25), (“spark”, 23), (“hadoop”, 16), (“scala”, 24), (“spark”,16),
键值对的key表示图书名称,value表示某天图书销量。计算每个键对应的平均值,也就是计算每种图书的每天平均销量

val rdd = sc.makeRDD(Array(("spark", 12), ("hadoop", 26),
("hadoop", 23), ("spark", 15), ("scala", 26), ("spark", 25),
("spark", 23), ("hadoop", 16), ("scala", 24), ("spark", 16)))

// groupByKey : 下面三种方式结果都一样
rdd.groupByKey().map(x => (x._1, x._2.sum.toDouble / x._2.size)).foreach(println) //(scala,25.0) (spark,18.2) (hadoop,21.666666666666668)
rdd.groupByKey().map{case (k,v) => (k, v.sum.toDouble / v.size)}.foreach(println)
rdd.groupByKey().mapValues(v => v.sum.toDouble / v.size).foreach(println)
-------------------------------------------------------------------------------------
//reduceByKey
//第一步: 通过mapValues算子,把 (spark,12) => (spark,(12,1))
rdd.mapValues((_,1)).foreach(println) //(spark,(12,1)) (hadoop,(26,1))

//第二步: 我们通过把value的第一个元素相加得到数量, 第二个元素相加得到这个key出现的个数
//reduceByKey 会寻找相同key的数据,当找到两条记录时,参数(x,y)就代表两条记录的value,这里是(x1,x2)(y1,y2) 
//做(x,y) => (x1+y1,x2+y2) 处理,反复执行这个操作,直到每个key只剩下一条记录
rdd.mapValues((_,1))
    .reduceByKey((x,y) => (x._1 + y._1, x._2 + y._2))
    .foreach(println) //(scala,(50,2)) (spark,(91,5)) (hadoop,(65,3))
    
//第三步: 就最后相除一下得到结果
rdd.mapValues((_,1))
    .reduceByKey((x,y) => (x._1 + y._1, x._2 + y._2))
    .mapValues( x => x._1.toDouble / x._2)
    .foreach(println) //(scala,25.0) (spark,18.2) (hadoop,21.666666666666668)
------------------------------------------------------------------------------------
// foldByKey  : 这种方式与上面类似,只不过多了一个初值,这里(0,0)代表,每个分区的元素都要计算这个初值  
rdd.mapValues((_ , 1)).foldByKey((0,0))((x,y) => (x._1+y._1, x._2+y._2)).foreach(println) //(scala,(50,2)) (spark,(91,5)) (hadoop,(65,3))
rdd.mapValues((_ , 1))
    .foldByKey((0,0))((x,y) => (x._1+y._1, x._2+y._2))
    .mapValues(x => x._1.toDouble / x._2)
    .foreach(println)  //(scala,25.0) (spark,18.2) (hadoop,21.666666666666668)
------------------------------------------------------------------------------------
// aggregateByKey
// aggregateByKey => 定义初值 + 分区内的聚合函数(x,y) + 分区间的聚合函数(a,b)
rdd.mapValues((_, 1))
  .aggregateByKey((0,0))(
    (x,y) => {println(s"x=$x, y=$y"); (x._1 + y._1, x._2 + y._2)},
    (a,b) => {println(s"a=$a, b=$b"); (a._1 + b._1, a._2 + b._2)}
  ).mapValues(x => x._1.toDouble / x._2).foreach(println)  

//以下是打印结果
//首先,rdd通过mapValues算子, 把value的值变成了一个元组类型,并把第二个值变成1,例如("spark",12)=> (12,1) 
// 所以此时, x代表初值(0,0), y代表传入的value值(12,1)
// (x._1 + y._1, x._2 + y._2) => (0+12,0+1) 作为下一步的x值, 再与下一个key相同的value值(也就是y值)进行计算
x=(0,0), y=(12,1) ------ ("spark", 12) 
x=(0,0), y=(26,1) ------ ("hadoop", 26)
x=(26,1), y=(23,1)------ ("hadoop", 23)
x=(12,1), y=(15,1)------ ("spark", 15)
x=(0,0), y=(26,1) ------ ("scala", 26)
x=(27,2), y=(25,1)------ ("spark", 25)
x=(52,3), y=(23,1)------ ("spark", 23)
x=(49,2), y=(16,1)------ ("hadoop", 16)
x=(26,1), y=(24,1)------ ("scala", 24)
x=(75,4), y=(16,1)------ ("spark", 16)

----------------------------------------------------------------------------------------
// 换种方式,我们根本不需要先通过mapValues进行转换,直接使用aggregateByKey算子
// 这里x,就是初值(0,0), y代表 (spark,1)中的1,是我们传入的Int值
// 所以这里 x._1 + y
rdd.aggregateByKey((0,0))(
  (x,y) => {println(s"x=$x, y=$y"); (x._1 + y, x._2 + 1)},
  (a,b) => {println(s"a=$a, b=$b"); (a._1 + b._1, a._2 + b._2)}
).mapValues(x => x._1.toDouble / x._2).collect 
//打印出来可以看到过程,首先这是一个分区,所以很清晰的按照rdd内元素的顺序进行计算
//其中分区内的计算规则(x._1 + y, x._2 + 1), 分区间的规则:(a._1 + b._1, a._2 + b._2)
//x=(0,0), y=12  ------ ("spark", 12) 
//x=(0,0), y=26  ------ ("hadoop", 26)
//x=(26,1), y=23 ------ ("hadoop", 23)
//x=(12,1), y=15 ------ ("spark", 15)
//x=(0,0), y=26  ------ ("scala", 26)
//x=(27,2), y=25 ------ ("spark", 25) 
//x=(52,3), y=23 ------ ("spark", 23)
//x=(49,2), y=16 ------ ("hadoop", 16)
//x=(26,1), y=24 ------ ("scala", 24)
//x=(75,4), y=16 ------ ("spark", 16)

梳理一下流程:
第一个元素(spark,12),我们说了x代表初值一个元组(0,0),y代表value值,也就是12,所以按照后面的计算
(x._1 + y, x._2 +1) => (spark,(0+12,0+1))
接着计算第二个元素(hadoop,26), x还是初值, 我们推测,那个初值的作用就是给每个key的第一个value赋初值(0,0)
(x._1 + y, x._2 +1) => (hadoop,(0+26,0+1))
接着计算第三个元素(hadoop,23), 此时key为hadoop的元素,value就不是初值了, 是(26,1),那么就从这个值开始
(x._1 + y, x._2 +1) => (hadoop,(26+23,1+1)) => (hadoop,(49,2)) => 直到遇到下一个key为hadoop的元素,从(49,2)开始累加
…后面以此类推
这是计算完分区内的元素,还要计算分区间的元素
由于分区内最后计算出来得到的值就是一个元组,比如上面的结果最后一条
x=(75,4),y=16,计算完代表这个分区内,key为spark的元素,已经计算完了,
值为(75+16,4+1) => (91,5) ,这个值作为分区内的最后一个元素,也是下一步分区间计算的初值
假设还有一个分区,里面也有key为spark的值,最后得到一个元组比如(q,w)
那么分区间的计算两个数据类型都是元组,所以可以直接相加,
那么分区间的计算规则(a,b) => (a._1 + b._1, a._2 + b._2)
a代表第一个分区间的结果值,b代表第二个分区间的结果值
最后得到(91+q, 5+w)
这里最疑惑的点在于x和y究竟代表什么,一句可能不太恰当的理解
x就是传入的初值,这个值是按照ByKey的方式给每个唯一Key赋的值,作为第一步,
然后y值就是传入参数的value值 .

理解:
reduceByKey会寻找相同key的数据,当找到这样的两条记录时会对其value(分别记为x,y)做 (x,y)=> XXX 的处理,
即只保留求和之后的数据作为value。反复执行这个操作直至每个key只留下一条记录。

对于aggregateByKey的源码进行解析理解一下
在这里插入图片描述

我们对比上面的案例
rdd.aggregateByKey((0,0))(
  (x,y) => {println(s"x=$x, y=$y"); (x._1 + y, x._2 + 1)},
  (a,b) => {println(s"a=$a, b=$b"); (a._1 + b._1, a._2 + b._2)}
).mapValues(x => x._1.toDouble / x._2).collect 

其中初值zeroValue的初值是U,
分区内的函数 (U,V) => U 
其中(U,V) 就代表(x,y) ,然后x = U,最后得到一个U类型的数据
分区间的函数 (U,U) 代表(a,b) 最终得到一个 RDD[(K,U)]

简单描述:reduceByKey、foldByKey、aggregateByKey、combineByKey 的区别?

reduceByKey:   相同 key 的第一个数据不进行任何计算,`分区内`和`分区间`计算规则相同
FoldByKey:     相同 key 的第一个数据和初始值进行分区内计算,`分区内`和`分区间`计算规则相同
AggregateByKey:相同 key 的第一个数据和初始值进行分区内计算,`分区内`和`分区间`计算规则可以不相同
CombineByKey:  当计算时,发现数据结构不满足要求时,可以让第一个数据转换结构。分区内和分区间计算规则不相同
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值