Transformation转换算子简单介绍--Key-Value类型(二)--聚合算子(上)

848 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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的工作量,从而避免了数据倾斜的发生。 image.png

我们通过代码实际书写一下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相同。即,分区内逻辑和分区间逻辑相同。

注意:有初始值,分区内和分区间的计算逻辑一致

image.png

实操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(分区间): 函数用于合并每个分区中的结果。

注意:有初始值,分区内和分区间的计算逻辑可以不同,计算更灵活

image.png

代码实现如下:

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的数量变换,如下图所示。 image.png

代码实现如下:

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可以改变初始值的数据结构,计算最为灵活。
如果大家想要了解的更多,可以看着图示实际应用一下。