Key-Value 类型
大多数的 Spark 操作可以用在任意类型的 RDD 上, 但是有一些比较特殊的操作只能用在key-value类型的 RDD 上.
这些特殊操作大多都涉及到 shuffle 操作, 比如: 按照 key 分组(group), 聚集(aggregate)等.
在 Spark 中, 这些操作在包含对偶类型(Tuple2)的 RDD 上自动可用(通过隐式转换).
object RDD { implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)]) (implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null): PairRDDFunctions[K, V] = { new PairRDDFunctions(rdd) }
键值对的操作是定义在PairRDDFunctions类上, 这个类是对RDD[(K, V)]的装饰.
partitionBy
作用: 对 pairRDD 进行分区操作,如果原有的 partionRDD 的分区器和传入的分区器相同, 则返回原 pairRDD,否则会生成 ShuffleRDD,即会产生 shuffle 过程。
partitionBy 算子源码
def partitionBy(partitioner: Partitioner): RDD[(K, V)] = self.withScope { if (self.partitioner == Some(partitioner)) { self } else { new ShuffledRDD[K, V, V](self, partitioner) }}
scala> val rdd1 = sc.parallelize(Array((1, "a"), (2, "b"), (3, "c"), (4, "d")))rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[0] at parallelize at :24scala> rdd1.partitions.lengthres1: Int = 2scala> rdd1.partitionBy(new org.apache.spark.HashPartitioner(3)).partitions.lengthres3: Int = 3
reduceByKey(func, [numTasks])
作用: 在一个(K,V)的 RDD 上调用,返回一个(K,V)的 RDD,使用指定的reduce函数,将相同key的value聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置。
scala> val rdd1 = sc.parallelize(List(("female",1),("male",5),("female",5),("male",2)))rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at :24scala> rdd1.reduceByKey(_ + _)res4: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[1] at reduceByKey at :27scala> res1.collectres5: Array[(String, Int)] = Array((female,6), (male,7))
groupByKey()
作用: 按照key进行分组.
scala> val rdd1 = sc.parallelize(Array("hello", "world", "dsj", "hello", "are", "go"))rdd1: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[9] at parallelize at :24scala> val rdd2 = rdd1.map((_, 1))rdd2: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[10] at map at :26scala> rdd2.groupByKey()res9: org.apache.spark.rdd.RDD[(String, Iterable[Int])] = ShuffledRDD[11] at groupByKey at :29scala> res9.collectres10: Array[(String, Iterable[Int])] = Array((are,CompactBuffer(1)), (go,CompactBuffer(1)), (hello,CompactBuffer(1, 1)), (world,CompactBuffer(1)), (dsj,CompactBuffer(1)))scala> res10.map(t => (t._1, t._2.sum))res11: Array[(String, Int)] = Array((are,1), (go,1), (hello,2), (world,1), (dsj,1)) scala> res10.collectres7: Array[(String, Int)] = Array((are,1), (hello,2), (go,1), (atguigu,1), (world,1))
注意:
0. 基于当前的实现, groupByKey必须在内存中持有所有的键值对. 如果一个key有太多的value, 则会导致内存溢出(OutOfMemoryError)
0. 所以这操作非常耗资源, 如果分组的目的是为了在每个key上执行聚合操作(比如: sum 和 average), 则应该使用PairRDDFunctions.aggregateByKey 或者PairRDDFunctions.reduceByKey, 因为他们有更好的性能(会先在分区进行预聚合)
reduceByKey和groupByKey的区别
1. reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。
1. groupByKey:按照key进行分组,直接进行shuffle。
1. 开发指导:reduceByKey比groupByKey性能更好,建议使用。但是需要注意是否会影响业务逻辑。
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])
函数声明:
def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)] = self.withScope { aggregateByKey(zeroValue, defaultPartitioner(self))(seqOp, combOp)}
使用给定的 combine 函数和一个初始化的zero value, 对每个key的value进行聚合.
这个函数返回的类型U不同于源 RDD 中的V类型. U的类型是由初始化的zero value来定的. 所以, 我们需要两个操作: - 一个操作(seqOp)去把 1 个v变成 1 个U - 另外一个操作(combOp)来合并 2 个U
第一个操作用于在一个分区进行合并, 第二个操作用在两个分区间进行合并.
为了避免内存分配, 这两个操作函数都允许返回第一个参数, 而不用创建一个新的U
参数描述:
1. zeroValue:给每一个分区中的每一个key一个初始值;
1. seqOp:函数用于在每一个分区中用初始值逐步迭代value;
1. combOp:函数用于合并每个分区中的结果。
案例:
需求: 创建一个 pairRDD,取出每个分区相同key对应值的最大值,然后相加
scala> val rdd = sc.parallelize(List(("a",3),("a",2),("c",4),("b",3),("c",6),("c",8)),2)rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at :24scala> rdd.aggregateByKey(Int.MinValue)(math.max(_, _), _ +_)res14: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[1] at aggregateByKey at :27scala> res14.collectres1: Array[(String, Int)] = Array((b,3), (a,3), (c,12))
练习: 计算每个 key 的平均值
foldByKey
参数: (zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
作用:aggregateByKey的简化操作,seqop和combop相同
scala> val rdd = sc.parallelize(Array(("a",3), ("a",2), ("c",4), ("b",3), ("c",6), ("c",8)))rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[2] at parallelize at :24scala> rdd.foldByKey(0)(_ + _).collectres16: Array[(String, Int)] = Array((b,3), (a,5), (c,18))
思考: workcount 可以使用那些算子?
思考: reduceByKey, aggregateByKey, foldByKey 的区别和联系?
combineByKey[C]
函数声明:
def combineByKey[C]( createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C): RDD[(K, C)] = self.withScope { combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners, partitioner, mapSideCombine, serializer)(null)}
1. 作用: 针对每个K, 将V进行合并成C, 得到RDD[(K,C)]
1. 参数描述:
2 createCombiner: combineByKey会遍历分区中的每个key-value对. 如果第一次碰到这个key, 则调用createCombiner函数,传入value, 得到一个C类型的值.(如果不是第一次碰到这个 key, 则不会调用这个方法)
3 mergeValue: 如果不是第一个遇到这个key, 则调用这个函数进行合并操作. 分区内合并
4 mergeCombiners 跨分区合并相同的key的值(C). 跨分区合并
1. workcount
案例
需求1: 创建一个 pairRDD,根据 key 计算每种 key 的value的平均值。(先计算每个key出现的次数以及可以对应值的总和,再相除得到结果)
scala> val input = sc.parallelize(Array(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)),2)input: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[16] at parallelize at :24scala> input.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))res17: org.apache.spark.rdd.RDD[(String, (Int, Int))] = ShuffledRDD[17] at combineByKey at :27scala> res17.collectres18: Array[(String, (Int, Int))] = Array((b,(286,3)), (a,(274,3)))scala> res17.map(t => (t._1, t._2._1.toDouble / t._2._2)).collectres19: Array[(String, Double)] = Array((b,95.33333333333333), (a,91.33333333333333))
对比几个按照 key 聚集的函数的区别和联系
sortByKey
作用: 在一个(K,V)的 RDD 上调用, K必须实现 Ordered[K] 接口(或者有一个隐式值: Ordering[K]), 返回一个按照key进行排序的(K,V)的 RDD
scala> val rdd = sc.parallelize(Array((1, "a"), (10, "b"), (11, "c"), (4, "d"), (20, "d"), (10, "e")))rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[20] at parallelize at :24scala> rdd.sortByKey()res20: org.apache.spark.rdd.RDD[(Int, String)] = ShuffledRDD[23] at sortByKey at :27scala> res20.collectres21: Array[(Int, String)] = Array((1,a), (4,d), (10,b), (10,e), (11,c), (20,d))scala> rdd.sortByKey(true).collectres22: Array[(Int, String)] = Array((1,a), (4,d), (10,b), (10,e), (11,c), (20,d))scala> rdd.sortByKey(false).collectres23: Array[(Int, String)] = Array((20,d), (11,c), (10,b), (10,e), (4,d), (1,a))
mapValues
作用: 针对(K,V)形式的类型只对V进行操作
scala> val rdd = sc.parallelize(Array((1, "a"), (10, "b"), (11, "c"), (4, "d"), (20, "d"), (10, "e")))rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[30] at parallelize at :24scala> rdd.mapValues("").collectres24: Array[(Int, String)] = Array((1,), (10,), (11,), (4,), (20,), (10,))
join(otherDataset, [numTasks])
内连接:
在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的(K,(V,W))的RDD
scala> var rdd1 = sc.parallelize(Array((1, "a"), (1, "b"), (2, "c")))rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[32] at parallelize at :24scala> var rdd2 = sc.parallelize(Array((1, "aa"), (3, "bb"), (2, "cc")))rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[33] at parallelize at :24scala> rdd1.join(rdd2).collectres25: Array[(Int, (String, String))] = Array((1,(a,aa)), (1,(b,aa)), (2,(c,cc)))
注意:
1 如果某一个 RDD 有重复的 Key, 则会分别与另外一个 RDD 的相同的 Key进行组合.
2 也支持外连接: leftOuterJoin, rightOuterJoin, and fullOuterJoin.
cogroup(otherDataset, [numTasks])
作用:在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
scala> val rdd1 = sc.parallelize(Array((1, 10),(2, 20),(1, 100),(3, 30)),1)rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[37] at parallelize at :24scala> val rdd2 = sc.parallelize(Array((1, "a"),(2, "b"),(1, "aa"),(3, "c")),1)rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[38] at parallelize at :24scala> rdd1.cogroup(rdd2).collectres26: Array[(Int, (Iterable[Int], Iterable[String]))] = Array((1,(CompactBuffer(10, 100),CompactBuffer(a, aa))), (3,(CompactBuffer(30),CompactBuffer(c))), (2,(CompactBuffer(20),CompactBuffer(b))))