持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情
上篇文章我们说了Transformation转换算子中的Value类型,本篇文章我们主要说一下Key-Value类型算子,因为key-Value算子数量较多,所以我们按其功能分为两部分去讲,这篇文章说一下聚合算子--reduceByKey、foldByKey、aggregateByKey、combineByKey。
1. reduceByKey(): 按照K聚合V(用的较多)
def reduceByKey(func: (V, V) => V): RDD[(K, V)]
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
功能:将RDD[K,V]中的元素按照相同的K 对V进行聚合。其存在多种重载形式,还可以设置新RDD的分区数。
注意:无法设置初始值,分区内和分区间的计算逻辑相同
从下图我们可以看出reduceByKey算子在进行Shuffle之前存在 预聚合 的过程,那这就会极大程度的避免了 数据倾斜 的问题。因为在 预聚合的过程是在分区内进行,也就是在内存中进行,那么这就会使得硬盘中读取数据量变小,减小了Shuffle的工作量,从而避免了数据倾斜的发生。
我们通过代码实际书写一下reduceByKey算子,这里我们统计单词出现的次数。
object KeyValue02_reduceByKey {
def main(args: Array[String]): Unit = {
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
//3具体业务逻辑
//3.1 创建第一个RDD
val rdd = sc.makeRDD(List(("a",1),("b",5),("a",5),("b",2)))
//3.2 计算相同key对应值的相加结果
val reduce: RDD[(String, Int)] = rdd.reduceByKey((v1,v2) => v1+v2)
//3.3 打印结果
reduce.collect().foreach(println)
//4.关闭连接
sc.stop()
}
}
2. foldByKey(): 分区内和分区间相同的aggregateByKey()
def foldByKey(zeroValue: v)(func: (V, V)=> V): RDD[(K, V)]
其中:
zerovalue: 是一个初始化值,它可以是任意类型参数
func: 是一个函数,两个输入参数相同
功能: aggregateByKey的简化操作,seqop和combop相同。即,分区内逻辑和分区间逻辑相同。
注意:有初始值,分区内和分区间的计算逻辑一致
实操demo:
object KeyValue05_foldByKey {
def main(args: Array[String]): Unit = {
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
//3具体业务逻辑
//3.1 创建第一个RDD
val list: List[(String, Int)] = List(("a",1),("a",3),("a",5),("b",7),("b",2),("b",4),("b",6),("a",7))
val rdd = sc.makeRDD(list,2)
//3.2 求相同key的value和
rdd.foldByKey(0)(_+_).collect().foreach(println)
//3.3 求相同key的最大值
// rdd.foldBykey(0)(math.max).collect()foreach(println)
//4.关闭连接
sc.stop()
}
}
3. aggregateByKey(): 按照K处理分区内和分区间逻辑
def aggregate ByKey[U: ClassTag](zeroValue: U)(seqOp:(U, V) => U, combop:(U,U) => U);:RDD[(K, U)]
源码中:
zeroValue(初始值): 给每一个分区中的每一种 key 一个初始值;
seqOp(分区内): 函数用于在每一个分区中用初始值逐步迭代value;
combOp(分区间): 函数用于合并每个分区中的结果。
注意:有初始值,分区内和分区间的计算逻辑可以不同,计算更灵活
代码实现如下:
object KeyValue04_aggregateByKey {
def main(args: Array[String]): Unit = {
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
//3具体业务逻辑
//3.1 创建第一个RDD
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a",1),("a",3),("a",5),("b",7),("b",2),("b",4),("b",6),("a",7)), 2)
//先求分区内对应key的最大值 之后分区间累加
// 1. 规约使用的初始值
// 2. 分区内累加的逻辑
// 3. 分区间累加的逻辑
// 填写的初始值只在分区内使用 分区间使用第一个元素合并
//3.2 取出每个分区相同key对应值的最大值,然后相加
rdd.aggregateByKey(0)(math.max(_, _) , _ + _).collect().foreach(println) // 省略后
// rdd.aggregateByKey(0)( (res,elem) => math.max(res,elem) , (res,elem) => res + elem).collect().foreach(println) // 无省略
//4.关闭连接
sc.stop()
}
}
4. combineByKey(): 转换结构后分区内和分区间操作
def combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C): RDD[(K, C)]
源码中的参数解析:
(1)createCombiner(转换数据的结构): combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初始值;
(2)mergeValue(分区内): 如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并;
(3)mergeCombiners(分区间): 由于每个分区都是独立处理的,因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器,就需要使用用户提供的mergeCombiners()方法将各个分区的结果进行合并。
功能: 针对相同的K,将 V 合并成一个集合。
注意:有初始值,且初始值还支持改变数据结构,计算最灵活
我们可以使用 combineByKey()算子记录下key的数量变换,如下图所示。
代码实现如下:
object KeyValue06_combineByKey {
def main(args: Array[String]): Unit = {
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
//3.1 创建第一个RDD
val list: List[(String, Int)] = List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))
val rdd: RDD[(String, Int)] = sc.makeRDD(list, 2)
//3.2 将相同key对应的值相加,同时记录该key出现的次数,放入一个二元组
val combineRdd: RDD[(String, (Int, Int))] = rdd.combineByKey(
(_, 1),
(acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1),
(acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
)
//3.3 打印合并后的结果
combineRdd.collect().foreach(println)
//3.4 计算平均值
combineRdd.map {
case (key, value) => {
(key, value._1 / value._2.toDouble)
}
}.collect().foreach(println)
//4.关闭连接
sc.stop()
}
}
总结
Transformation转换算子中的四个聚合算子相应的功能以及实操我都给大家说完了,但这里要注意的是,这四个聚合算子的功能和灵活性是逐级递增的,reduceByKey使用最简单,但相应的可灵活实现的操作较少,combineByKey可以改变初始值的数据结构,计算最为灵活。
如果大家想要了解的更多,可以看着图示实际应用一下。