spark碰到的一个核心高级函数,诸如groupByKey,reduceByKey等都是由它实现的,所以确实需要着重理解。
下面将一步一步尽可能用通俗语言解释combineByKey函数的生成过程,并举一个平均值的例子进行演示。如果步骤有误,欢迎指正
val test = sc.parallelize(List(('A', 1), ('B', 2), ('A', 2), ('A', 3)))
val cbk_test = test.combineByKey(
| count => (count, 1),
| (acc:(Int, Int), count) => (acc._1+count, acc._2+1),
| (acc1:(Int, Int), acc2:(Int, Int)) => (acc1._1+acc2._1, acc1._2+acc2._2))
cbk_test.map(x=>(x._1, x._2._1.toDouble/x._2._2)).collect
以上的代码返回的是每个键的平均值的Array
res0: Array[(string, Double)] = Array(('A', 1.5), ('B', 2.5))
下面我们来详细演示一下这个过程:
第一段代码创建了一个名为test的包含元组的RDD,每个元组由一个字符串和一个整数构成
第二段代码:对combineByKey方法调用,作用于test RDD
第一行:createCombiner方法(初始化方法),在新遇到的键时,赋予一个初始值,否则执行第二步mergeValue方法(合并方法)
第二行:mergeValue方法,对于已经出现过的键(已经初始化过的),调用该方法聚合,对该键的当前值与新值进行合并
第三行:mergeConbiner方法,因为每个元组都是独立处理的,所以同一个键可以计量多次,如果有两个或者更多的分区都对应同一个键,就需要该方法将各个分区的结果进行合并。
下面我们一步一步来演示
1.首先test第一个元素输入,将键值对中的值隐式传递到count中,通过函数将count变成(count, 1)的元组完成初始化。通俗来说,就是当('A', 1)输入时,检测到新键A,进行初始化。
此时(count, 1)变成(1,1),前面是该新键的值,后面是初始化的计数值
由于A是第一次出现,所以不调用mergeValue方法,又因为目前只有一个分区对应A键,所以不调用mergeConbiner方法。
接下来,将第二个元组输入,同样也是只执行初始化createCombiner方法
此时的结果为[("A", (1, 1)), ("B", (2, 1))]
2. 当第三个元组('A', 2)出现时,由于已经初始化过,所以执行mergeValue方法
(acc:(Int, Int), count) => (acc._1+count, acc._2+1),
三个参数:(Int, Int), count,此时的acc是指上一次的(count,1)的结果(1,1), acc._1=1,新输入进来的值被count接收,所以acc._1+count=1+2=3,acc._2=1+1=2,赋给A键下的acc
第四个元组('A', 3)出现时同理
此时的A键下的acc=(3,2),count接收新进入的值为3,执行acc._1+count=3+3=6, acc._2+1=2+1=3,此时A下的acc为(6,3),B下的acc为(2,1)
可以总结为acc._1表示总和,总和随着键值对的不断加入而累加同键下的值并对不同的键进行分组,acc._2表示计数,初始为1, 在相同键下,每执行一次mergeValue就增加1
3.mergeConbiner(),
(acc1:(Int, Int), acc2:(Int, Int)) => (acc1._1+acc2._1, acc1._2+acc2._2))
最后所有分区的结果合并,之前演示的是在同一个分区中的计算情况,然而真实的情况有可能是:
分区1: A:acc=(1,1) B:acc=(2,1)
分区2: A:acc=(2+3,1+1)=(5,2)
所以最后要将所有不同分区计算的结果进行合并:
A:acc=(6,3), B:acc=(2,1) 也就是acc1._1+acc2._1, acc1._2+acc2._2的由来
需要注意的是,如果存在多个分区,那么合并分区会一直执行,所以即使有更多分区代码也不需要更改,只定义acc1和acc2即可对所有分区进行合并叠加
4.cbk_test.map(x=>(x._1, x._2._1.toDouble/x._2._2)).collect

最后,映射cbk_test的字符,元组中的累加值/计数值
得到平均值
1192





