Spark Transformation转换算子
- 一.Value类型
- 二.双Value类型
- 三.Key-Value类型
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)