RDD 的操作分为转化(Transformation)操作和行动(Action)操作。转化操作就是从一个 RDD 产生一个新的 RDD,而行动操作就是进行实际的计算。转换操作和行动操作一般也被叫做转换算子和行动算子。
RDD 的操作是惰性的,当 RDD 执行转化操作的时候,实际计算并没有被执行,只有当 RDD 执行行动操作时才会促发计算任务提交,从而执行相应的计算操作。
RDD转换算子
首先,我们需要理解一件事。这种算子所输入的往往是一个或多个函数,这个函数用来对转换算子所指定的数据做具体处理。例如:
//这里使用map使得rdd的每条数据都*2,所以输入和输出数据都是单条数据。
def mapFunction(num:Int): Int = {
num * 2
}
val mapRDD: RDD[Int] = rdd.map(mapFunction)
输入的具体函数根据我们的业务决定。很多情况下,这种函数可以简化成匿名函数。
val mapRDD: RDD[Int] = rdd.map(_*2)
具体的简化流程可以看我的另外一篇文章:Scala的匿名函数在map()上的简化。
那么接下来,我们开始讲解一些经典的算子。
RDD 根据数据处理方式的不同将算子整体上分为 Value 类型、双 Value 类型和 Key-Value类型。
Value类型
1. map
对RDD中的数据进行逐条转换,有点像MR的map操作。
例如,我们对List中所有的数都*2。
val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4))
val mapRDD: RDD[Int] = dataRDD.map(_*2)
/*
结果:
2
4
6
8
*/
2. mapPartitions
和map()不同,mapPartitions()将数据以分区为单位进行处理。匿名函数的输入是一个分区内数据的迭代器,输出也要求是一个迭代器。
这里将每个分区的数据用迭代器的方式输出到控制台。
val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4), 2)
val mapRDD: RDD[Int] = dataRDD.mapPartitions(
iters => {
while(iters.hasNext){
println(iters.next())
}
iters
}
)
/*
结果:
3
1
2
4
这里可以看到,由于分成了两个区,变成了(1,2)(3,4)两个区
1和3是不同分区,所以不一定有固定顺序。但是3一定在4前面,1一定在2前面
*/
这里像map()一样,我们对List中所有的数都*2。
//第二个参数2表示分区数量为2
val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4), 2)
val mapRDD: RDD[Int] = dataRDD.mapPartitions(
iters => {
iters.map(_*2)
}
)
/*
结果:
2
4
6
8
*/
map和mapPartitions的区别
- map对分区内的数据一条一条类似串行处理,而mapPartitions直接输入一个迭代器,可以作类似批处理操作。
- map是对每条数据进行处理,处理前后的数据量不会变化;而mapPartitions返回的是一个迭代器,数量不要求和处理前数量一致,因此可以进行filter等操作。
- map因为进行逐条操作,因此性能较低,相对地,类似批处理的mapPartitions性能较高;但是 mapPartitions 算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用。使用 map 操作。
3. mapPartitionsWithIndex
mapPartitionsWithIndex和mapPartitions的不同是,mapPartitionsWithIndex除了获得分区对应的迭代器,还可以得到该分区的编号。
val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4), 2)
val mapRDD: RDD[String] = dataRDD.mapPartitionsWithIndex(
(index, iters) => {
iters.map("分区"+index+": "+_)
}
)
/*
结果:
分区0: 1
分区0: 2
分区1: 3
分区1: 4
*/
4. flatMap
flatMap将RDD的数据作扁平化操作后再做处理。
val dataRDD = sparkContext.makeRDD(List("1 2", "3 4"), 2)
val mapRDD = dataRDD.flatMap(
string => {
string.split(" ")
}
)
/*
结果:
1
2
3
4
*/
5. glom
glom将同一分区的数据合成一个相同类型的数组做处理,分区不变。
val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1, 2, 3, 4), 2)
val mapRDD: RDD[Array[Int]] = dataRDD.glom()
/*
结果:
[I@64aad6db
[I@ae7950d
这里输出的是创建的数组的地址
*/
6. groupBy
groupBy根据指定的规则进行分组,数据会根据规则找到自己的分组。分区数默认不变,但是数据会被打乱重新组合,可能会使得数据从原来的分区传输到分组所在的分区。这个操作称作shuffle。
val dataRDD = sparkContext.makeRDD(List(1, 2, 3, 4), 2)
val mapRDD = dataRDD.groupBy(
_%3
)
/*
结果:
分区0:
(0,CompactBuffer(3))
(2,CompactBuffer(2))
分区1:
(1,CompactBuffer(1, 4))
可以看到,这里有2个分区,3个分组。
原本在分区1的数字3到了分区0,而原本在分区0的数字1到了分区1。
这就能看出在shuffle之后,数据换到了它分组所在的分区。shuffle可能会使得所在分区改变。
*/
7. filter
filter根据指定的规则作过滤,保留符合规则的数据。数据过滤后分区不变,但过滤后的分区数据可能会导致不均衡,出现数据倾斜。
val dataRDD = sparkContext.makeRDD(List(1, 2, 3, 4), 2)
val mapRDD = dataRDD.filter(_%2 == 0)
/*
结果:
2
4
*/
8. coalesce
根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率。在参数shuffle为默认值false时,被缩小的两个分区会直接合并成一个分区,不会做shuffle操作。因此性能比较高。
//四个分区缩减成一个分区
val dataRDD = sparkContext.makeRDD(List(1, 2, 3, 4), 4)
val mapRDD = dataRDD.coalesce(1)
9. repartition
repartition意思是重新分区,我们可以通过源码理解其原理。
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
coalesce(numPartitions, shuffle = true)
}
通过源码可以看到,repartition是是用了shuffle为true的coalesce,通过shuffle来改变分区数量,它既可以使RDD的分区数变大也可以变小。
val dataRDD = sparkContext.makeRDD(List(1, 2, 3, 4), 2)
val mapRDD = dataRDD.repartition(4)
一般而言,我们缩小分区直接用coalesce,因为可以不需要shuffle;而repartition是直接使用shuffle改变数据分区,因此一般用于扩大分区。
双Value类型
1. intersection
对源 RDD 和参数 RDD 求交集后返回一个新的 RDD,并且去重。若两个RDD类型不一致会报错。intersection可以指定返回的分区数。
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD = dataRDD1.intersection(dataRDD2, 2)
/*
结果:
4
3
*/
2. union
对源 RDD 和参数 RDD 进行合并返回一个新的 RDD,若两个RDD类型不一致会报错。union操作后的分区数为两个父RDD的分区之和,所以是窄依赖。
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD = dataRDD1.union(dataRDD2)
/*
结果:
1
2
3
4
3
4
5
6
*/
3. subtract
对源 RDD 和参数 RDD 求差集后返回一个新的 RDD,若两个RDD类型不一致会报错。默认情况下,分区数为源RDD的分区数,subtract可以指定返回的分区数。
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD = dataRDD1.subtract(dataRDD2)
/*
结果:
1
2
*/
4. zip
将两个 RDD 中的元素,以键值对的形式进行合并。其中,键值对中的 Key 为第 1 个 RDD中的元素,Value 为第 2 个 RDD 中的相同位置的元素。
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD = dataRDD1.zip(dataRDD2)
/*
结果:
(1,3)
(2,4)
(3,5)
(4,6)
*/
如果两个RDD的分区数不一样,或者两个RDD内的数据无法像key-value的格式对应,程序会报错。而两个RDD的数据类型不一样并不会影响结果。
Key-Value类型
1. partitionBy
根据指定的Partitioner重新分区,Spark默认的分区器是HashPartitioner。
val rdd: RDD[(Int, String)] = sparkContext.makeRDD(Array((1,"aaa"),(2,"bbb"),(3,"ccc")),3)
val dataRDD = rdd.partitionBy(new HashPartitioner(2))
2. reduceByKey
根据相同的Key对其中的Value进行聚合。
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("a",3)))
//聚合时两两累加得到结果
val dataRDD2 = dataRDD1.reduceByKey(_+_)
/*
结果:
(a,4)
(b,2)
*/
3. groupByKey
将数据源的数据根据 key 对 value 进行分组。
val dataRDD1 =
sparkContext.makeRDD(List(("a",1),("b",2),("a",3)))
val dataRDD2 = dataRDD1.groupByKey()
/*
结果:
(a,CompactBuffer(1, 3))
(b,CompactBuffer(2))
*/
val dataRDD3 = dataRDD1.groupByKey(2)
/*
结果:
分区0:
(b,CompactBuffer(2))
分区1:
(a,CompactBuffer(1, 3))
*/
reduceByKey和groupByKey的区别
- reduceByKey的操作是分组+聚合;groupByKey仅仅是分组。所以在分组聚合的场合下,推荐使用 reduceByKey。如果仅仅是分组而不需要聚合,那么还是只能使用 groupByKey
- reduceByKey 和 groupByKey 都存在 shuffle 的操作,但是 reduceByKey
可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而 groupByKey 只是进行分组,不存在数据量减少的问题,reduceByKey 性能比较高。
4. aggregateByKey
aggregateByKey算子是函数柯里化。
- 第一个参数列表表示初始值。这里的初始值是为了让初始值和第一个值在分区内的规则做计算,否则分区内计算无法从头开始。
- 第二个参数列表内含有两个参数,第一个参数表示分区内的计算规则,第二个参数表示分区间的计算规则。
// TODO : 取出每个分区内相同 key 的最大值然后分区间相加
val rdd =
sc.makeRDD(List(
("a",1),("a",2),("c",3),
("b",4),("c",5),("c",6)
),2)
// 0:("a",1),("a",2),("c",3) => (a,2)(c,3)
// => (a,10)(b,10)(c,20)
// 1:("b",4),("c",5),("c",6) => (b,4)(c,6)
//这里的第一个参数0表示初始值(a, 0)或(b, 0)或(c, 0),再与RDD内的数值作比较
val resultRDD =
rdd.aggregateByKey(0)(
(x, y) => math.max(x,y),
(x, y) => x + y
)
/*
结果:
(b,4)
(a,2)
(c,9)
*/
RDD行动算子
RDD是懒执行的,只有运行到RDD行动算子才会实际上创建job,进行DAG创建与调度,根据宽依赖进行Stage划分,从而每个Stage生成TaskSet,并将Task发给对应的Executor。
1. reduce
聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据。
val rdd: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4))
// 聚合数据
val reduceResult: Int = rdd.reduce(_+_)
2. collect
在驱动程序中,以数组 Array 的形式返回数据集的所有元素。
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 收集数据到 Driver
rdd.collect().foreach(println)
3. count
返回 RDD 中元素的个数。
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val countResult: Long = rdd.count()
4. first
返回 RDD 中的第一个元素。
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val firstResult: Int = rdd.first()
println(firstResult)
5. take
返回一个由 RDD 的前 n 个元素组成的数组。
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val takeResult: Array[Int] = rdd.take(2)
println(takeResult.mkString(","))
6. takeOrdered
返回该 RDD 排序后的前 n 个元素组成的数组。
val rdd: RDD[Int] = sc.makeRDD(List(1,3,2,4))
// 返回 RDD 中元素的个数
val result: Array[Int] = rdd.takeOrdered(2)
7. aggregate
分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合。
这里需要注意的是,行动算子的aggregate和转换算子的aggregateByKey不同,行动算子的aggregate在分区内和分区外的规则都与初始值进行了数据聚合;而转换算子的aggregateByKey只与分区内的规则进行了聚合。
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
// 将该 RDD 所有元素相加得到结果
val result: Int = rdd.aggregate(0)(_ + _, _ + _)
/*
结果:
10
*/