RDD算子

本文详细介绍了Spark中的RDD算子,包括转化算子如map、mapPartitions、filter、groupByKey和行动算子如reduce、collect、count。转化算子产生新的RDD而不立即计算,而行动算子触发实际计算。RDD操作是惰性的,map和mapPartitions的区别在于处理方式,reduceByKey和groupByKey的主要差异在于是否包含预聚合。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值