spark中的combineByKey函数的用法

2023-01-12  本文已影响0人  Yobhel

一、函数的源码

/**
   * Simplified version of combineByKeyWithClassTag that hash-partitions the resulting RDD using the
   * existing partitioner/parallelism level. This method is here for backward compatibility. It
   * does not provide combiner classtag information to the shuffle.
   *
   * @see `combineByKeyWithClassTag`
   */
  def combineByKey[C](
      createCombiner: V => C,
      mergeValue: (C, V) => C,
      mergeCombiners: (C, C) => C): RDD[(K, C)] = self.withScope {
    combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners)(null)
  }
 /**
   * Generic function to combine the elements for each key using a custom set of aggregation
   * functions. Turns a JavaPairRDD[(K, V)] into a result of type JavaPairRDD[(K, C)], for a
   * "combined type" C.
   *
   * Users provide three functions:
   *
   *  - `createCombiner`, which turns a V into a C (e.g., creates a one-element list)
   *  - `mergeValue`, to merge a V into a C (e.g., adds it to the end of a list)
   *  - `mergeCombiners`, to combine two C's into a single one.
   *
   * In addition, users can control the partitioning of the output RDD, the serializer that is use
   * for the shuffle, and whether to perform map-side aggregation (if a mapper can produce multiple
   * items with the same key).
   *
   * @note V and C can be different -- for example, one might group an RDD of type (Int, Int) into
   * an RDD of type (Int, List[Int]).
   */
  def combineByKey[C](createCombiner: JFunction[V, C],
      mergeValue: JFunction2[C, V, C],
      mergeCombiners: JFunction2[C, C, C],
      partitioner: Partitioner,
      mapSideCombine: Boolean,
      serializer: Serializer): JavaPairRDD[K, C] = {
      implicit val ctag: ClassTag[C] = fakeClassTag
    fromRDD(rdd.combineByKeyWithClassTag(
      createCombiner,
      mergeValue,
      mergeCombiners,
      partitioner,
      mapSideCombine,
      serializer
    ))
  }

由源码中看出,该函数中主要包含的参数:

createCombiner:V=>C
    mergeValue:(C,V)=>C
    mergeCombiners:(C,C)=>R
    partitioner:Partitioner  
    mapSideCombine:Boolean=true
    serializer:Serializer=null

这里的每一个参数都对分别对应这聚合操作的各个阶段

二、参数详解:

1、createCombiner:V=>C   分组内的创建组合的函数。通俗点将就是对读进来的数据进行初始化,其把当前的值作为参数,可以对该值做一些转换操作,转换为我们想要的数据格式

2、mergeValue:(C,V)=>C   该函数主要是分区内的合并函数,作用在每一个分区内部。其功能主要是将 V 合并到之前(createCombiner)的元素 C 上,注意,这里的 C 指的是上一函数转换之后的数据格式,而这里的 V 指的是原始数据格式(上一函数为转换之前的)

3、mergeCombiners:(C,C)=>R   该函数主要是进行多分取合并,此时是将两个 C 合并为一个 C,例如两个 C:(Int)进行相加之后得到一个 R:(Int)

4、partitioner:自定义分区数,默认是 hashPartitioner

5、mapSideCombine:Boolean=true   该参数是设置是否在 map 端进行 combine 操作

三、函数工作流程

首先明确该函数遍历的数据是(k,v)对的 rdd 数据

1、combinByKey 会遍历 rdd 中每一个(k,v)数据对,对该数据对中的 k 进行判断,判断该(k,v)对中的 k 是否在之前出现过,如果是第一次出现,则调用 createCombiner 函数,对该 k 对应的 v 进行初始化操作(可以做一些转换操作),也就是创建该 k 对应的累加其的初始值

2、如果这是一个在处理当前分区之前遇到的 k,会调用 mergeCombiners 函数,把该 k 对应的累加器的 value 与这个新的 value 进行合并操作

四、实例解释

实例 1:

import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
 
import scala.collection.mutable
 
object test {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("testCombineByKey").setMaster("local[2]")
    val ssc = new SparkSession
    .Builder()
      .appName("test")
      .master("local[2]")
      .config(conf)
      .getOrCreate()
    val sc = ssc.sparkContext
    sc.setLogLevel("error")
 
    val initialScores = Array((("1", "011"), 1), (("1", "012"), 1), (("2", "011"), 1), (("2", "013"), 1), (("2", "014"), 1))
    val d1 = sc.parallelize(initialScores)
  d1.map(x => (x._1._1, (x._1._2, 1)))
    .combineByKey(
      (v: (String, Int)) => (v: (String, Int)),
      (acc: (String, Int), v: (String, Int)) => (v._1+":"+acc._1,acc._2+v._2),
                 (p1:(String,Int),p2:(String,Int)) => (p1._1 + ":" + p2._1,p1._2 + p2._2)
    ).collect().foreach(println)
  }
}

从 map 函数开始说起:

1、map端将数据格式化为:(,(String,Int))->("1",("011",1))

2、接着combineByKye函数会逐个的读取map之后的每一个k,v数据对,当读取到第一个("1",("011",1)),此时回判断,“1”这个是否在之前的出现过,如果该k是第一次出现,则会调用createCombiner函数,经过转换,该实例中是对该value值么有做任何的改变原样返回,此时这个该value对应的key回被comgbineByKey函数创建一个累加其记录

3、当读取到第二个数据("1",("012",,1))的时候,回对“1”这个key进行一个判断,发现其在之前出现过,此时怎直接调用第二个函数,mergeValues函数,对应到该实例中,acc即为上一函数产生的结果,即("1",("011",1)),v即是新读进来的数据("1",("012",1))

4、此时执行该函数:(acc: (String, Int), v: (String, Int)) => (v._1+":"+acc._1,acc._2+v._2)

将新v中的第一个字符串与acc中的第一个字符串进行连接,v中的第二个值,与acc中的第二个值进行相加操作

5、当所有的分区内的数据计算完成之后,开始调用mergeCombiners函数,对每个分区的数据进行合并,该实例中p1和p2分别对应的是不同分区的计算结果,所以二者的数据格式是完全相同的,此时将第一个分区中的字符串相连接,第二个字符相加得到最终结果

6、最终输出结果为:
(2,(014:013:011,3))
(1,(012:011,2))

实例 2

import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
 
import scala.collection.mutable
 
object test {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("testCombineByKey").setMaster("local[2]")
    val ssc = new SparkSession
    .Builder()
      .appName("test")
      .master("local[2]")
      .config(conf)
      .getOrCreate()
    val sc = ssc.sparkContext
    sc.setLogLevel("error")
 
    val initialScores = Array((("1", "011"), 1), (("1", "012"), 1), (("2", "011"), 1), (("2", "013"), 1), (("2", "014"), 1))
    val d1 = sc.parallelize(initialScores)
    d1.map(x => (x._1._1, (x._1._2, 1)))     //("1",("011",1))
      .combineByKey(
      (v: (String, Int)) => (mutable.Set[String](v._1), (mutable.Set[Int](v._2))),
      (acc: (mutable.Set[String], mutable.Set[Int]), v: (String, Int)) => (acc._1 + v._1, acc._2 + v._2),
      (acc1: (mutable.Set[String], mutable.Set[Int]), acc2: (mutable.Set[String], mutable.Set[Int])) => (acc1._1 ++ acc2._1, acc1._2 ++ acc2._2)
    ).collect().foreach(println)
  }
}  

运行结果为:

(2,(Set(013, 014, 011),Set(1)))
(1,(Set(011, 012),Set(1)))

上一篇下一篇

猜你喜欢

热点阅读