Spark Transformation转换算子

本文详细介绍了Spark中的Transformation算子,包括Value类型、双Value类型和Key-Value类型的操作,如map、flatMap、groupByKey、reduceByKey等,还讨论了它们的区别和应用场景,帮助读者深入理解Spark数据处理。

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

RDD转换算子整体上分为:Value类型、双Value类型和Key-Value类型


一.Value类型

顾名思义是对单个value值进行运算的算子类型。下面主要从函数签名、功能、案例+图解三个方法介绍这几类算子。

1.map():映射

1)函数签名:

def map[U: ClassTag](f: T => U): RDD[U]

2)功能说明

参数f是一个函数,它可以接收一个参数。当某个RDD执行map方法时,会遍历该RDD中的每一个数据项,并依次应用f函数,从而产生一个新的RDD。即,这个新RDD中的每一个元素都是原来RDD中每一个元素依次应用f函数而得到的。

map方法的部分源代码:

def map[U: ClassTag](f:T=> U): RDD[U]= withScope {
	val clea nF= sc. clea n(f)
	new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.m ap(cleanF))
}
private[spark] class MapPartitionsRDD[U: ClassTag T: ClassTag](
	...
	override def getPartitions: Array[Partition]= firstParent[T].partit ions,
	....
}
protected[spark] def firstParent[U; ClassTag] RDD[U]={
	dependencies. head.rdd. asInst anceOf[RDD[U]]
}

3)案例:创建一个1-4数组的RDD,两个分区,将所有元素*2形成新的RDD

在这里插入图片描述

object TestMap {
  def main(args: Array[String]): Unit = {
    //1.创建SparkConf并设置App名称
    val conf = new SparkConf().setAppName("CT").setMaster("local[*]")
    //2.创建SparkContext,该对象是提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)

    //3具体业务逻辑
    // 3.1 创建一个RDD
    val rdd: RDD[Int] = sc.makeRDD(1 to 4,2)
    // 3.2 调用map方法,每个元素乘以2
    val mapRdd: RDD[Int] = rdd.map(_ * 2)

    // 3.3 打印修改后的RDD中数据
    mapRdd.collect().foreach(println)
    
    //4.关闭连接
    sc.stop()
  }
}

说明:由于具体的业务逻辑代码只有第3部分几行,剩下的关于SparkContext对象的创建及资源关闭,后面的案例将直接省略这部分代码。

2.mapPartitions():以分区为单位执行map

mapPartitions:以分区为单位,对RDD中的元素进行映射。一般适用于批处理的操作,比如:将RDD中的元素插入到数据库中,需要数据库连接,如果每一个元素都创建一个连接,效率很低;可以对每个分区的元素,创建一个连接。

1)函数签名:

def mapPartitions[U: ClassTag](
f: Iterator[T] => Iterator[U],   
preservesPartitioning: Boolean = false): RDD[U])    

参数说明:

  • f函数:把每一个分区的数据,分别放入到迭代器中,进行批处理。传入的是一个迭代器,也就是该分区的整体数据。
  • preservePartitioning: 是否保留上游RDD的分区信息,默认flase

2)功能说明: Map是一次处理一个元素,而mapPartitions一次处理一个分区数据

3)案例:创建–个RDD,4个元素,2个分区,使每个元素*2组成新的RDD

在这里插入图片描述

//3具体业务逻辑
// 3.1 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(1 to 4, 2)

// 3.2 调用mapPartitions方法,每个元素乘以2
val rdd1 = rdd.mapPartitions(x=>x.map(_*2))
//注意与map处理数据的区别:
//val mapRdd: RDD[Int] = rdd.map(_ * 2)

// 3.3 打印修改后的RDD中数据
rdd1.collect().foreach(println)
*map()和mapPartitions()区别

在这里插入图片描述

mapPartitions一般适用于批处理的操作,比如:将RDD中的元素插入到数据库中,需要数据库选接,如果每一个元素都创建一个连接, 效率很低——可以对每个分区的元素,创建一个连接

3.mapPartitionsWithIndex():带分区号的2

mapPartitionsWithIndex:以分区为单位,对RDD中的元素进行映射,并且带分区编号

1)函数签名:

def mapPartitionsWithIndex[U: ClassTag](
	f: (Int, Iterator[T]) => Iterator[U],   
	preservesPartitioning: Boolean = false): RDD[U]
)

Int即表示分区编号.

2)功能说明:类似于mapPartitions,比mapPartitions多一个整数参数表示分区号

3)案例:创建一个RDD,使每个元素跟所在分区号形成一个元组,组成一个新的RDD

在这里插入图片描述

//3具体业务逻辑
// 3.1 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(1 to 4,2)
// 3.2 调用map方法,每个元素乘以2
val mapRdd = rdd.mapPartitionsWithIndex(
  (index,items) => {
    items.map(a => (index,a))
  }
)

// 3.3 打印修改后的RDD中数据
mapRdd.collect().foreach(println)

4.flatMap():扁平化

faltMap:对集合中的元素进行扁平化处理

1)函数签名:

def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U])

2)功能说明

  • 与map操作类似,将RDD中的每一个元素通过应用f函数依次转换为新的元素,并封装到RDD中

区别:

  • 在flatMap操作中,f函数的返回值是一个集合并且会将每一个该集合中的元素拆分出来放到新的RDD中

3)案例:创建一个集合,集合里面存储的还是子集合,把所有子集合中数据取出放入到一个大的集合中

在这里插入图片描述

//3具体业务逻辑
// 3.1 创建一个RDD
val listRDD=sc.makeRDD(List(List(1,2),List(3,4),List(5,6),List(7)), 2)

// 3.2 把所有子集合中数据取出放入到一个大的集合中
listRDD.flatMap(list=>list).collect.foreach(println)

注意:如果匿名函数输入和输出相同,那么不能简化

  • listRDD.flatMap(list=>list)

5.glom():分区转数组

glom:将RDD一个分区中的元素,组合成一个新的数组

1)函数签名:

def glom(): RDD[Array[T]]

2)功能说明

  • 该操作将RDD中每一个分区变成一个数组,并放置在新的RDD中,数组中元素的类型与原分区中元素类型一致

3)案例:创建一个2个分区的RDD,并将每个分区的数据放到一个数组,求出每个分区的最大值

在这里插入图片描述

// 3.1 创建一个RDD
val rdd = sc.makeRDD(1 to 4, 2)

// 3.2 求出每个分区的最大值  0->1,2   1->3,4
//glom求出的是Array,map遍历的就是Array
val maxRdd: RDD[Int] = rdd.glom().map(_.max)

// 3.3 求出所有分区的最大值的和 2 + 4
println(maxRdd.collect().sum)

6.groupBy():分组

groupBy:按照指定的规则,对RDD中的元素进行分组

1)函数签名:

def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]

2)功能说明

  • 分组,按照传入函数的返回值进行分组;
  • 将相同的key对应的值放入一个迭代器:如 1,2,3,4 按照奇偶数分组
    • (0,CompactBuffer(2, 4))
    • (1,CompactBuffer(1, 3))

3)案例:创建一个RDD,按照元素模以2的值进行分组。

在这里插入图片描述

// 3.1 创建一个RDD
val rdd = sc.makeRDD(1 to 4, 2)

// 3.2 将每个分区的数据放到一个数组并收集到Driver端打印
rdd.groupBy(_ % 2).collect().foreach(println)
//(0,CompactBuffer(2, 4))
//(1,CompactBuffer(1, 3))

// 3.3 创建一个RDD
val rdd1: RDD[String] = sc.makeRDD(List("hello","hive","hadoop","spark","scala"))

// 3.4 按照首字母第一个单词相同分组
rdd1.groupBy(str=>str.substring(0,1)).collect().foreach(println)
WordCount案例

在这里插入图片描述

val rdd: RDD[String] = sc.makeRDD(List("Hello Scala", "Hello Spark", "Hello World"))

//算子中使用case模式匹配元组时,需要用大括号,否则会报错
rdd.flatMap(_.split(" ")).map((_,1)).groupBy(_._1).map{
  case (str, tuples) => {
    (str,str.size)
  }
}.collect().foreach(println)

复杂版:

//复杂版思路:("Hello Scala", 2)==>(hello,2),(Scala,2)     ("Hello Spark", 3)==>(hello,3),(Spark,3)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("Hello Scala", 2), ("Hello Spark", 3), ("Hello World", 2)))

//1)对RDD中的元素进行扁平映射
val flatMapRDD: RDD[(String, Int)] = rdd.flatMap {
  case (words, count) => {
    words.split(" ").map(word => (word, count))
  }
}

//2)按照单词对RDD中的元素进行分组     (Hello,CompactBuffer((Hello,2), (Hello,3), (Hello,2)))
val groupByRDD: RDD[(String, Iterable[(String, Int)])] = flatMapRDD.groupBy(_._1)

//3)对RDD的元素重新进行映射
val resRDD: RDD[(String, Int)] = groupByRDD.map {
  case (word, datas) => {
    (word, datas.map(_._2).sum)
  }
}

resRDD.collect().foreach(println)

注意:算子中使用case模式匹配元组时,需要用大括号,否则会报错

7.filter():过滤

filter:按照指定的过滤规则,对RDD中的元素进行过滤

1)函数签名:

def filter: T => Boolean): RDD[T]

2)功能说明.

  • 接收一个返回值为布尔类型的函数作为参数。
  • 当某个RDD调用flter方法时,会对该RDD中每一个元素应用f函数,如果返回值类型为true,则该元素会被添加到新的RDD中

3)案例:创建一个RDD ( 由整数组成),过滤出其中奇偶数

在这里插入图片描述

val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4),2)

//3.1 过滤出符合条件的数据
val filterRdd: RDD[Int] = rdd.filter(_ % 2 == 0)

//3.2 收集并打印数据
filterRdd.collect().foreach(println)

8.sample():随机抽样

1)函数签名:

def sample(
withReplacement: Boolean, 
fraction: Double,
seed: Long = Utils.random.nextLong): RDD[T] 

参数说明:

  • withReplacement表示:抽出的数据是否放回,
    • true为有放回的抽样,
    • false为无放回的抽样;
  • fraction:
    • 当withReplacement=true时:选择每个元素的期望次数,取值必须大于等于0
    • 当withReplacement=false时:选择每个元素的概率,取值一定是[0,1];
  • seed表示:指定随机数生成器种子,一般不需要指定

2)功能说明:从大量的数据中采样

3)案例:创建一个RDD (1-10),从中选择放回和不放回抽

在这里插入图片描述

// 3.1 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(1 to 10)

// 3.2 打印放回抽样结果
rdd.sample(true, 0.4, 2).collect().foreach(println)

// 3.3 打印不放回抽样结果
rdd.sample(false, 0.2, 3).collect().foreach(println)

9.distinct():去重

1)函数签名:

第一种方式:def distinct(): RDD[T]

  • 默认情况下,distinct会生成与原RDD分区个数一致的分区数
  • 源码如下:
    def distinct(): RDD[T]= withScope {
    	distinct(partitions.length)
    }
    

第二种方式:

  • 可以去重后,修改分区个数
  • 源码如下:
    def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
      map(x => (x, null)).reduceByKey((x, y) => x, numPartitions).map(_._1)
    }
    

例如:数据为 1,2,3,4,1,2;第二种方式:map(x => (x, null)).reduceByKey((x, y) => x, numPartitions).map(_._1)各部分结果为

map(x => (x, null))reduceByKey((x, y) => x, numPartitions)map(_._1)
结果(1,null)
(2,null)
(3,null)
(4,null)
(1,null)
(2,null)
(1,(null,null)–>(1,null)
(2,(null,null)–>(2,null)
(3,null)–>(3,null)
(4,null)–>(4,null)
1
2
3
4

2)功能说明:对内部的元素去重,并将去重后的元素放到新的RDD中。

用分布式的方式去重相比于HashSet集合方式,不容易产生OOM

3)案例:

//创建RDD
val numRDD: RDD[Int] = sc.makeRDD(List(1,1,1,2),3)

numRDD.mapPartitionsWithIndex{
  (index,datas)=>{
    println(index + "--->" + datas.mkString(","))
    datas
  }
}.collect()

println("---------------")

//对RDD中的数据进行去重
val newRDD: RDD[Int] = numRDD.distinct(2)

newRDD.mapPartitionsWithIndex{
  (index,datas)=>{
    println(index + "--->" + datas.mkString(","))
    datas
  }
}.collect()

输出结果:

0--->1
2--->1,2
1--->1
---------------
0--->2
1--->1

如果:
//对RDD中的数据进行去重
val newRDD: RDD[Int] = numRDD.distinct(2)
结果将会有一个分区为空:
1--->1
2--->2
0--->

10.coalesce():重新分区

coalesce算子包括:配置执行Shuffle和配置不执行Shuffle两种方式。

  • 默认是不执行shuffle,一般用于缩减分区

repartition:底层调用的就是coalesce,只不过默认是执行shuffle,一般用于扩大分区

1)不执行Shuffle方式[默认]

1)函数签名:

def coalesce(numPartitions: Int, shuffle: Boolean = false,
               partitionCoalescer:Option[PartitionCoalescer] = Option.empty)
              (implicit ord: Ordering[T] = null): RDD[T] = withScope

2)功能说明:缩减分区数,用于大数据集过滤后,提高小数据集的执行效率,但容易产生数据倾斜。

3)案例:

在这里插入图片描述
如图需求2,数据容易产生倾斜。

//需求一:4个分区合并为2个分区
val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4), 4)

val coalesceRdd: RDD[Int] = rdd.coalesce(2)

//打印查看对应分区数据
val indexRdd: RDD[Int] = coalesceRdd.mapPartitionsWithIndex(
  (index, items) => {
    // 遍历每个分区数据并打印,并带分区号
    items.foreach(item => {
      println(index + "=>" + item)
    })
    // 返回分区的数据
    items
  }
)
indexRdd.collect()

输出结果:
1=>3
1=>4
0=>1
0=>2

注意:默认情况下,如果使用coalesce扩大分区是不起作用的 。因为底层没有执行shuffle

  • 如果扩大分区可以使用repartition
2)执行Shuffle方式
//3个分区变2个分区:执行Shuffle方式
val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5, 6), 3)
val coalesceRdd: RDD[Int] = rdd.coalesce(2, true)

//打印查看对应分区数据
val indexRdd: RDD[Int] = coalesceRdd.mapPartitionsWithIndex(
  (index, items) => {
    // 遍历每个分区数据并打印,并带分区号
    items.foreach(item => {
      println(index + "=>" + item)
    })
    // 返回分区的数据
    items
  }
)
indexRdd.collect()

#输出结果:
0=>1
1=>2
0=>3
1=>4
0=>5
1=>6

可以看出执行Shuffle的方式,分区数据不会像默认形式那样倾斜。原因是,Shuffle会将数据分区打乱重组。

3)Shuffle原理

在这里插入图片描述
问题:假如求shuffle后,分区0中所有数据的和,需要等待读取所有分区的数据后,才能求和。如果数据一直放在内存中,容易导致OOM

解决办法:Shufle,必须要落盘等待数据的到来???

11.repartition():重新分区

repartition:底层调用的就是coalesce,只不过默认是执行shuffle,一般用于扩大分区

1)函数签名: def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

2)功能说明:

  • 该操作内部其实执行的是coalesce操作,参数shuffle的默认值为true。
  • 无论是将分区数多的RDD转换为分区数少的RDD,还是将分区数少的RDD转换为分区数多的RDD,repartition操作都可以完成,因为无论如何都会经shuffle过程。

3)案例:创建一个4个分区的RDD,对其重新分区。

在这里插入图片描述

//创建RDD
val numRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4,5,6),3)
//如果扩大分区    使用repartition
val newRDD: RDD[Int] = numRDD.repartition(4)

newRDD.mapPartitionsWithIndex{
  (index,datas)=>{
    println(index + "--->" + datas.mkString(","))
    datas
  }
}.collect()
*coalesce和repartition区别

1)coalesce重新分区,可以选择是否进行shuffle过程。由参数shuffle: Boolean = false/true决定。

2)repartition实际上是调用的coalesce,进行shuffle。源码如下:

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
}

3)coalesce一般为缩减分区,如果扩大分区,不使用shuffle是没有意义的,repartition扩大分区执行shuffle。

12.sortBy():排序

sortBy:对RDD中的元素进行排序(默认升序)

1)函数签名

def sortBy[K](
      f: (T) => K,
      ascending: Boolean = true,
      numPartitions: Int = this.partitions.length)
      (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T] 

2)功能说明:

  • 该操作用于排序数据。
  • 在排序之前,可以将数据通过f函数进行处理,之后按照f函数处理的结果进行排序;
  • 默认为正序排列
  • 排序后新产生的RDD的分区数与原RDD的分区数一致。

3)案例:创建一个RDD,按照不同的规则进行排序

//1)默认是升序排
val rdd: RDD[Int] = sc.makeRDD(List(2, 1, 3, 4, 6, 5))
val sortRdd: RDD[Int] = rdd.sortBy(num => num)
sortRdd.collect().foreach(println)

//2)配置为倒序排
val sortRdd2: RDD[Int] = rdd.sortBy(num => num, false)
sortRdd2.collect().foreach(println)

//3)按照字符的int值排序
val strRdd: RDD[String] = sc.makeRDD(List("1", "22", "12", "2", "3"))
strRdd.sortBy(num => num.toInt).collect().foreach(println)

//4)tuple先按照tuple的第一个值排序,相等再按照第2个值排
val rdd3: RDD[(Int, Int)] = sc.makeRDD(List((2, 1), (1, 2), (1, 1), (2, 2)))
rdd3.sortBy(t=>t).collect().foreach(println)

13.pipe():调用脚本

1)函数签名: def pipe(command: String): RDD[String]

2)功能说明

  • pipe:管道,针对每个分区,都调用一次shell脚本,返回输出的RDD。
  • 注意:在Worker节点可以访问到的位置放脚本

在这里插入图片描述

# 1)编写一个脚本,并增加执行权限
[zxy@hadoop102 spark]$ vim pipe.sh
#!/bin/sh
echo "Start"
while read LINE; do
   echo ">>>"${LINE}
done
[zxy@hadoop102 spark]$ chmod 777 pipe.sh

[zxy@hadoop102 spark]$  bin/spark-shell 
# 2)创建一个只有一个分区的RDD
scala> val rdd = sc.makeRDD (List("hi","Hello","how","are","you"),1)
# 3)将脚本作用该RDD并打印
scala> rdd.pipe("/opt/module/spark/pipe.sh").collect()
res18: Array[String] = Array(Start, >>>hi, >>>Hello, >>>how, >>>are, >>>you)
# 4)创建一个有两个分区的RDD
scala> val rdd = sc.makeRDD(List("hi","Hello","how","are","you"),2)
# 5)将脚本作用该RDD并打印
scala> rdd.pipe("/opt/module/spark/pipe.sh").collect()
res19: Array[String] = Array(Start, >>>hi, >>>Hello, Start, >>>how, >>>are, >>>you)

二.双Value类型

双Value类型的转换算子,使用场景是两个RDD之间的操作,类似于数学集合间的操作。

1.union()并集

1)函数签名:

def union(other: RDD[T]): RDD[T]

2)功能说明:

  • 对源RDD和参数RDD求并集后返回一个新的RDD

在这里插入图片描述

3)案例:创建两个RDD求并集

在这里插入图片描述

2.subtract ()差集

1)函数签名:

def subtract(other: RDD[T]): RDD[T]

2)功能说明

  • 计算差的一种函数,去除两个RDD中相同元素,不同的RDD将保留下来

在这里插入图片描述

3.intersection()交集

1)函数签名:

def intersection(other: RDD[T]): RDD[T]

2)功能说明

  • 对源RDD和参数RDD求交集后返回一个新的RDD

在这里插入图片描述

4.zip()拉链

def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)] 

功能说明:

  • 该操作可以将两个RDD中的元素,以键值对的形式进行合并。
  • 其中,键值对中的Key为第1个RDD中的元素,Value为第2个RDD中的元素。
  • 将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常

在这里插入图片描述
总案例:

val rdd1: RDD[Int] = sc.makeRDD(1 to 4)

val rdd2: RDD[Int] = sc.makeRDD(3 to 6)

//计算两个RDD的并集
rdd1.union(rdd2).collect().foreach(println)
//计算第一个RDD与第二个RDD的差集并打印
rdd1.subtract(rdd2).collect().foreach(println)
//计算第一个RDD与第二个RDD的交集并打印
rdd1.intersection(rdd2).collect().foreach(println)
//计算第一个RDD与第二个RDD的拉链并打印:元素、分区数需要相同
rdd1.zip(rdd2).collect().foreach(println)

三.Key-Value类型

Key-Value类型的转换算子,只适用于RDD中是键值对数据类型的情况。

1.partitionBy():按照key重新分区

partitionBy:按照指定的分区器,通过key对RDD中的元素进行分区

1)函数签名:

def partitionBy(partitioner: Partitioner): RDD[(K, V)]

2)功能说明

  • 将RDD[K,V]中的K按照指定Partitioner重新进行分区;
  • 如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区,否则会产生shuffle过程
  • 默认分区器 HashPartitioner
# HashPartitioner源码及key的分区计算规则
class HashPartitioner(partitions: Int) extends Partitioner{
    require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")
    
    def numPartitions: Int = partitions
    //分区规则
    def getPartition(key: Any): Int = key match {
        case null => 0
        case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
    }
    
    override def equals(other: Any): Boolean = other match {
        case h: HashPartitioner =>
            h.numPartitions == numPartitions
        case _ =>
            false
    }
    
    override def hashCode: Int = numPartitions
}

  def nonNegativeMod(x: Int, mod: Int): Int = {
    val rawMod = x % mod
    rawMod + (if (rawMod < 0) mod else 0)
  }

3)案例:创建一个3个分区的RDD,对其重新分区

在这里插入图片描述

//1 创建第一个RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "aaa"), (2, "bbb"), (3, "ccc")), 3)
//2 使用默认分区
val rdd2: RDD[(Int, String)] = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))

//查看新RDD的分区数
println(rdd2.partitions.size)

//3.自定义分区
val rdd3: RDD[(Int, String)] = rdd.partitionBy(new MyPartitioner(2))

//4.打印查看对应分区数据
val indexRdd: RDD[(Int, String)] = rdd3.mapPartitionsWithIndex(
    (index, datas) => {
        // 打印每个分区数据,并带分区号
        datas.foreach(data => {
            println(index + "=>" + data)
        })
        // 返回分区的数据
        datas
    }
)

indexRdd.collect()

自定义分区:

// 自定义分区
class MyPartitioner(num: Int) extends Partitioner {
    // 设置的分区数
    override def numPartitions: Int = num

    // 具体分区逻辑
    override def getPartition(key: Any): Int = {

        if (key.isInstanceOf[Int]) {

            val keyInt: Int = key.asInstanceOf[Int]
            if (keyInt % 2 == 0)
                0
            else
                1
        }else{
            0
        }
    }
}

2.reduceByKey():按照K聚合V

reduceByKey():将相同的key放在一起,对Value进行聚合操作。其存在多种重载形式,还可以设置新RDD的分区数。

1)函数签名:

def reduceByKey(func: (V, V) => V): RDD[(K, V)]

2)案例:统计单词出现次数

在这里插入图片描述

val rdd = sc.makeRDD(List(("a",1),("b",5),("a",5),("b",2)))
//计算相同key对应值的相加结果
val reduce: RDD[(String, Int)] = rdd.reduceByKey((x,y) => x+y)
//打印结果
reduce.collect().foreach(println)

3.groupByKey():按照K重新分组

groupByKey
按照key对RDD中的元素进行分组

功能说明:

  • groupByKey对每个key进行操作,但只生成一个seq,并不进行聚合
  • 该操作可以指定分区器或者分区数(默认使用HashPartitioner)

在这里插入图片描述

案例:创建一个pairRDD,将相同key对应值聚合到一个seq中,并计算相同key对应值的相加结果。

val rdd = sc.makeRDD(List(("a",1),("b",5),("a",5),("b",2)))

//将相同key对应值聚合到一个Seq中
val group: RDD[(String, Iterable[Int])] = rdd.groupByKey()

//打印结果
group.collect().foreach(println)
//(a,CompactBuffer(1, 5))
//(b,CompactBuffer(3, 2))

//计算相同key对应值的相加结果
group.map(t=>(t._1,t._2.sum)).collect().foreach(println)
*reduceByKey和groupByKey区别

1)reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]

2)groupByKey:按照key进行分组,直接进行shuffle。其结果是value的集合。

3)指导原则:在不影响业务逻辑的前提下,优先选用reduceByKey。

  • 求和操作不影响业务逻辑,求平均值影响业务逻辑

4.aggregateByKey():按照K处理分区内和分区间逻辑

1)函数签名:aggregateByKey(zeroValue)(分区内计算规则,分区间计算规则)

def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,
      combOp: (U, U) => U): RDD[(K, U)]

2)参数说明:

  • 1)zeroValue(初始值):给每一个分区中的每一种key一个初始值;
  • 2)seqOp(分区内):函数用于在每一个分区中用初始值逐步迭代value;
  • 3)combOp (分区间) :函数用于合并每个分区中的结果。

3)案例:取出每个分区相同key对应值的最大值,然后相加

在这里插入图片描述

具体流程:

在这里插入图片描述

val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 3), ("a", 2), ("c", 4), ("b", 3), ("c", 6), ("c", 8)), 2)

//取出每个分区相同key对应值的最大值,然后相加
rdd.aggregateByKey(0)(math.max(_, _), _ + _).collect().foreach(println)

5.foldByKey():分区内和分区间相同的aggregateByKey()

1)函数签名:foldByKey(zereValue)(分区内/间计算规则)

def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]

2)案例:求wordcount
在这里插入图片描述

val list: List[(String, Int)] = List(("a",1),("a",1),("a",1),("b",1),("b",1),("b",1),("b",1),("a",1))
val rdd = sc.makeRDD(list,2)

//求wordcount:两者作用相同
//rdd.aggregateByKey(0)(_+_,_+_).collect().foreach(println)
rdd.foldByKey(0)(_+_).collect().foreach(println)

6.combineByKey():转换结构后分区内和分区间操作

1)函数签名:combineByKey(对当前key的value进行转换,分区内计算规则,分区间计算规则)

def combineByKey[C](
      createCombiner: V => C,
      mergeValue: (C, V) => C,
      mergeCombiners: (C, C) => C): RDD[(K, C)]

2)参数说明:

createCombiner:V=>C

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

mergeValue:(C,V)=>C

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

mergeCombiners:(C,C)=>R

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

3)具体案例说明:求每个学生的平均成绩(学生科目数不一致)

简单点说,combineByKey对应的三个参数及功能:

  • createCombiner: V => C, 对RDD中当前key取出第一个value做一个初始化
  • mergeValue: (C, V) => C, 分区内计算规则,主要在分区内进行,将当前分区的value值,合并到初始化得到的c上面
  • mergeCombiners: (C, C) => C, 分区间计算规则

在这里插入图片描述

val list: List[(String, Int)] = List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))
val input: RDD[(String, Int)] = sc.makeRDD(list, 2)

//1.将相同key对应的值相加,同时记录该key出现的次数,放入一个二元组
val combineRdd: RDD[(String, (Int, Int))] = 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)
)

//2.打印合并后的结果
combineRdd.collect().foreach(println)

//3.计算平均值
combineRdd.map {
    case (key, value) => {
        (key, value._1 / value._2.toDouble)
    }
}.collect().foreach(println)

7.几种聚合算子的对比

reduceByKey、aggregateByKey、foldByKey、combineByKey四个聚合算子其底层都是调用PairRDDFunctions类的combineByKeyWithClassTag方法,只是各自传递的参数不同。

1)combineByKeyWithClassTag的函数签名:

def combineByKeyWithClassTag[C](
    createCombiner: V => C,
    mergeValue: (C, V) => C,
    mergeCombiners: (C, C) => C,
    partitioner: Partitioner,
    mapSideCombine: Boolean = true,
    serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] 

2)各聚合函数的调用参数:
在这里插入图片描述

8.sortByKey():按照K进行排序

1)函数签名:

def sortByKey(
	ascending: Boolean = true,   
	numPartitions: Int = self.partitions.length): RDD[(K, V)]

默认升序:ascending: Boolean = true

2)功能说明

  • 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
  • 如果key为自定义类型,要求必须混入Ordered特质,并重写其compare方法
    在这里插入图片描述
object TestSortByKey {
  def main(args: Array[String]): Unit = {
    //1.创建SparkConf并设置App名称
    val conf = new SparkConf().setAppName("CT").setMaster("local[*]")
    //2.创建SparkContext,该对象是提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)

    //如果key为自定义类型,要求必须混入Ordered特质
    val stdList: List[(Student, Int)] = List(
      (new Student("zhangsan", 18), 1),
      (new Student("lisi", 18), 1),
      (new Student("zhangsan", 19), 1),
      (new Student("wangwu", 18), 1),
      (new Student("zhangsan", 20), 1)
    )
    val stdRDD: RDD[(Student, Int)] = sc.makeRDD(stdList)

    val resRDD: RDD[(Student, Int)] = stdRDD.sortByKey()
    
    resRDD.collect().foreach(println)

    //4.关闭连接
    sc.stop()
  }
}

//需要混入Ordered特质,并且还有Serializable特质(闭包检查)
class Student(var name:String,var age:Int) extends Ordered[Student] with Serializable {
  //指定比较规则
  override def compare(that: Student): Int = {
    //先按照名称排序升序,如果名称相同的话,再按照年龄降序排序
    var res: Int = this.name.compareTo(that.name)
    if(res == 0){
      res = that.age - this.age
    }
    res
  }

  override def toString = s"Student($name, $age)"
}
#输出结果:
(Student(lisi, 18),1)
(Student(wangwu, 18),1)
(Student(zhangsan, 20),1)
(Student(zhangsan, 19),1)
(Student(zhangsan, 18),1)

9.mapValues():只对V进行操作

1)函数签名:

def mapValues[U](f: V => U): RDD[(K, U)]

针对于(K,V)形式的类型,该算子只对V进行操作。如创建一个pairRDD,并将value添加字符串“|||”:

val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (1, "d"), (2, "b"), (3, "c")))

// 对value添加字符串"|||"
rdd.mapValues(_ + "|||").collect().foreach(println)

#输出结果:
(1,|||a)
(1,|||d)
(2,|||b)
(3,|||c)

10.join():连接,将相同key对应的多个value关联在一起

1)函数签名:

def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
def join[W](other: RDD[(K, W)], numPartitions: Int): RDD[(K, (V, W))] 

2)功能说明

  • 在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD

join算子:相当于内连接,将两个RDD中的key相同的数据匹配,如果key匹配不上,那么数据不关联。

在这里插入图片描述

//创建第一个RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))

//创建第二个pairRDD
val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (4, 6)))

//join操作并打印结果
rdd.join(rdd1).collect().foreach(println)

同样也还有左连接、右连接、全连接:

def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
def rightOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (Option[V], W))]
def fullOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (Option[V], Option[W]))] 

11.cogroup():类似全连接,但是在同一个RDD中对key聚合

1)函数签名

def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]

2)功能说明

  • 在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(lterable,Iterable))类型的RDD
  • 操作两个RDD中的KV元素,每个RDD中相同key中的元素分别聚合成一个集合

在这里插入图片描述

val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c")))

val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1,4),(2,5),(3,6)))

//cogroup两个RDD并打印结果
rdd.cogroup(rdd1).collect().foreach(println)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值