Spark 6

6. RDD的使用

6.1 什么是RDD

RDD的全称为Resilient Distributed Dataset,是一个弹性、可复原的分布式数据集,是Spark中最基本的抽象,是一个不可变的、有多个分区的、可以并行计算的集合。RDD中并不装真正要计算的数据,而装的是描述信息,描述以后从哪里读取数据,调用了用什么方法,传入了什么函数,以及依赖关系等。

6.2 RDD的特点

  • 有一些列连续的分区:分区编号从0开始,分区的数量决定了对应阶段Task的并行度
  • 有一个函数作用在每个输入切片上或对应的分区上: 每一个分区都会生成一个Task,对该分区的数据进行计算,这个函数就是具体的计算逻辑
  • RDD和RDD之间存在一系列依赖关系:RDD调用Transformation后会生成一个新的RDD,子RDD会记录父RDD的依赖关系,包括宽依赖(有shuffle)和窄依赖(没有shuffle)
  • (可选的)K-V的RDD在Shuffle会有分区器,默认使用HashPartitioner
  • (可选的)如果从HDFS中读取数据,会有一个最优位置:spark在调度任务之前会读取NameNode的元数据信息,获取数据的位置,移动计算而不是移动数据,这样可以提高计算效率。

6.3 RDD的算子(方法)分类

  • Transformation:即转换算子,调用转换算子会生成一个新的RDD,Transformation是Lazy的,不会触发job执行。

  • Action:行动算子,调用行动算子会触发job执行,本质上是调用了sc.runJob方法,该方法从最后一个RDD,根据其依赖关系,从后往前,划分Stage,生成TaskSet。

6.4 创建RDD的方法

  • 从HDFS指定的目录据创建RDD

Scala
val lines: RDD[String] = sc.textFile("hdfs://node-1.51doit.cn:9000/log")

  • 通过并行化方式,将Driver端的集合转成RDD

Scala
 val rdd1: RDD[Int] = sc.parallelize(Array(1,2,3,4,5,6,7,8,9))

6.5 查看RDD的分区数量

Scala
val rdd1: RDD[Int] = sc.parallelize(Array(1,2,3,4,5,6,7,8,9))
rdd1.partitions.length

6.6 RDD的Transformation算子

6.6.1 map

map算子的功能为做映射,即将原来的RDD中对应的每一个元素,应用外部传入的函数进行运算,返回一个新的RDD

Scala
 val rdd1: RDD[Int] = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 2)
 val rdd2: RDD[Int] = rdd1.map(_ * 2)

6.6.2 flatMap

flatMap算子的功能为扁平化映射,即将原来RDD中对应的每一个元素应用外部的运算逻辑进行运算,然后再将返回的数据进行压平,类似先map,然后再flatten的操作,最后返回一个新的RDD

Scala
val arr = Array(
  "spark hive flink",
  "hive hive flink",
  "hive spark flink",
  "hive spark flink"
)
val rdd1: RDD[String] = sc.makeRDD(arr, 2)
val rdd2: RDD[String] = rdd1.flatMap(_.split(" "))

6.6.3 filter

filter的功能为过滤,即将原来RDD中对应的每一个元素,应用外部传入的过滤逻辑,然后返回一个新的的RDD

Scala
 val rdd1: RDD[Int] = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 2)
 val rdd2: RDD[Int] = rdd1.filter(_ % 2 == 0)

6.6.4 mapPartitions

将数据以分区为的形式返回进行map操作,一个分区对应一个迭代器,该方法和map方法类似,只不过该方法的参数由RDD中的每一个元素变成了RDD中每一个分区的迭代器,如果在映射的过程中需要频繁创建额外的对象,使用mapPartitions要比map高效的过。

Scala
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5), 2)
var r1: RDD[Int] = rdd1.mapPartitions(it => it.map(x => x * 10))

map和mapPartitions的区别,mapPartitions一定会比map效率更高吗?

不一定:如果对RDD中的数据进行简单的映射操作,例如变大写,对数据进行简单的运算,map和mapPartitions的效果是一样的,但是如果是使用到了外部共享的对象或数据库连接,mapPartitions效率会更高一些。

原因:map出入的函数是一条一条的进行处理,如果使用数据库连接,会每来一条数据创建一个连接,导致性能过低,而mapPartitions传入的函数参数是迭代器,是以分区为单位进行操作,可以事先创建好一个连接,反复使用,操作一个分区中的多条数据。

特别提醒:如果使用mapPartitions方法不当,即将迭代器中的数据toList,就是将数据都放到内存中,可能会出现内存溢出的情况。

6.6.5 mapPartitionsWithIndex

类似于mapPartitions, 不过函数要输入两个参数,第一个参数为分区的索引,第二个是对应分区的迭代器。函数的返回的是一个经过该函数转换的迭代器。

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9), 2)
val rdd2 = rdd1.mapPartitionsWithIndex((index, it) => {
  it.map(e => s"partition: $index, val: $e")
})

6.6.6 keys

RDD中的数据为对偶元组类型,调用keys方法后返回一个新的的RDD,该RDD的对应的数据为原来对偶元组的全部key,该方法有隐式转换

Scala
val lst = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
val keyRDD: RDD[String] = wordAndOne.keys

6.6.7 values

RDD中的数据为对偶元组类型,调用values方法后返回一个新的的RDD,该RDD的对应的数据为原来对偶元组的全部values

Scala
val lst = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
val valueRDD: RDD[Int] = wordAndOne.values
//

6.6.8 mapValues

RDD中的数据为对偶元组类型,将value应用传入的函数进行运算后再与key组合成元组返回一个新的RDD

Scala
val lst = List(("spark", 5), ("hive", 3), ("hbase", 4), ("flink", 8))
val rdd1: RDD[(String, Int)] = sc.parallelize(lst, 2)
//将每一个元素的次数乘以10再可跟key组合在一起
//val rdd2 = rdd1.map(t => (t._1, t._2 * 10))
val rdd2 = rdd1.mapValues(_ * 10)

6.6.9 flatMapValues

RDD中的数据为对偶元组类型,将value应用传入的函数进行flatMap打平后再与key组合成元组返回一个新的RDD

Scala
val lst = List(("spark", "1,2,3"), ("hive", "4,5"), ("hbase", "6"), ("flink", "7,8"))
val rdd1: RDD[(String, String)] = sc.parallelize(lst, 2)
//将value打平,再将打平后的每一个元素与key组合("spark", "1,2,3") =>("spark",1),("spark",2),("spark",3)
val rdd2: RDD[(String, Int)] = rdd1.flatMapValues(_.split(",").map(_.toInt))
//    val rdd2 = rdd1.flatMap(t => {
//      t._2.split(",").map(e => (t._1, e.toInt))
//    })

6.6.10 uion

将两个类型一样的RDD合并到一起,返回一个新的RDD,新的RDD的分区数量是原来两个RDD的分区数量之和

Scala
//两个RDD进行union,对应的数据类型必须一样
//Union不会去重
val rdd1 = sc.parallelize(List(1,2,3,4), 2)
val rdd2 = sc.parallelize(List(5, 6, 7, 8, 9,10), 3)
val rdd3 = rdd1.union(rdd2)
println(rdd3.partitions.length)

6.6.11 reduceByKey

将数据按照相同的key进行聚合,特点是先在每个分区中进行局部分组聚合,然后将每个分区聚合的结果从上游拉取到下游再进行全局分组聚合

Scala
val lst = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
val reduced: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)

6.6.12 combineByKey

Scala
val lst = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
//调用combineByKey传入三个函数
//val reduced = wordAndOne.combineByKey(x => x, (a: Int, b: Int) => a + b, (m: Int, n: Int) => m + n)
val f1 = (x: Int) => {
  val stage = TaskContext.get().stageId()
  val partition = TaskContext.getPartitionId()
  println(s"f1 function invoked in state: $stage, partition: $partition")
  x
}
//在每个分区内,将key相同的value进行局部聚合操作
val f2 = (a: Int, b: Int) => {
  val stage = TaskContext.get().stageId()
  val partition = TaskContext.getPartitionId()
  println(s"f2 function invoked in state: $stage, partition: $partition")
  a + b
}
//第三个函数是在下游完成的
val f3 = (m: Int, n: Int) => {
  val stage = TaskContext.get().stageId()
  val partition = TaskContext.getPartitionId()
  println(s"f3 function invoked in state: $stage, partition: $partition")
  m + n
}
val reduced = wordAndOne.combineByKey(f1, f2, f3)

combineByKey要传入三个函数:

第一个函数:在上游执行,该key在当前分区第一次出现时,对value处理的运算逻辑

第二个函数:在上游执行,当该key在当前分区再次出现时,将以前相同key的value进行运算的逻辑

第三个函数:在下游执行,将来自不同分区,相同key的数据通过网络拉取过来,然后进行全局聚合的逻辑

6.6.13 groupByKey

按照key进行分组,底层使用的是ShuffledRDD,mapSideCombine = false,传入的三个函数只有前两个被调用了,并且是在下游执行的

Scala
 val lst = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
//按照key进行分组
val grouped: RDD[(String, Iterable[Int])] = wordAndOne.groupByKey()

6.6.14 foldByKey

与reduceByKey类似,只不过是可以指定初始值,每个分区应用一次初始值,先在每个进行局部聚合,然后再全局聚合,局部聚合的逻辑与全局聚合的逻辑相同。

Scala
 val lst: Seq[(String, Int)] = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)

//与reduceByKey类似,只不过是可以指定初始值,每个分区应用一次初始值
val reduced: RDD[(String, Int)] = wordAndOne.foldByKey(0)(_ + _)

6.6.15 aggregateByKey

与reduceByKey类似,并且可以指定初始值,每个分区应用一次初始值,传入两个函数,分别是局部聚合的计算逻辑、全局聚合的逻辑。

Scala

val lst: Seq[(String, Int)] = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
//在第一个括号中传入初始化,第二个括号中传入两个函数,分别是局部聚合的逻辑和全局聚合的逻辑
val reduced: RDD[(String, Int)] = wordAndOne.aggregateByKey(0)(_ + _, _ + _)

6.6.16 ShuffledRDD

reduceByKey、combineByKey、aggregateByKey、foldByKey底层都是使用的ShuffledRDD,并且mapSideCombine = true

Scala
val f1 = (x: Int) => {
  val stage = TaskContext.get().stageId()
  val partition = TaskContext.getPartitionId()
  println(s"f1 function invoked in state: $stage, partition: $partition")
  x
}
//在每个分区内,将key相同的value进行局部聚合操作
val f2 = (a: Int, b: Int) => {
  val stage = TaskContext.get().stageId()
  val partition = TaskContext.getPartitionId()
  println(s"f2 function invoked in state: $stage, partition: $partition")
  a + b
}
//第三个函数是在下游完成的
val f3 = (m: Int, n: Int) => {
  val stage = TaskContext.get().stageId()
  val partition = TaskContext.getPartitionId()
  println(s"f3 function invoked in state: $stage, partition: $partition")
  m + n
}
//指定分区器为HashPartitioner
val partitioner = new HashPartitioner(wordAndOne.partitions.length)
val shuffledRDD = new ShuffledRDD[String, Int, Int](wordAndOne, partitioner)
//设置聚合亲器并关联三个函数
val aggregator = new Aggregator[String, Int, Int](f1, f2, f3)
shuffledRDD.setAggregator(aggregator) //设置聚合器
shuffledRDD.setMapSideCombine(true) //设置map端聚合

如果设置了setMapSideCombine(true),那么聚合器中的三个函数都会执行,前两个在上游执行,第三个在下游执行

如果设置了setMapSideCombine(false),那么聚合器中的三个函数只会执行前两个,并且这两个函数都是在下游执行

6.6.17 distinct

distinct是对RDD中的元素进行取重,底层使用的是reduceByKey实现的,先局部去重,然后再全局去重

Scala
val arr = Array(
  "spark", "hive", "spark", "flink",
  "spark", "hive", "hive", "flink",
  "flink", "flink", "flink", "spark"
)
val rdd1: RDD[String] = sc.parallelize(arr, 3)
//去重
val rdd2: RDD[String] = rdd1.distinct()

distinct的底层实现如下:

Scala
val rdd11: RDD[(String, Null)] = rdd1.map((_, null))
val rdd12: RDD[String] = rdd11.reduceByKey((a, _) => a).keys

6.6.18 partitionBy

按照指的的分区器进行分区,底层使用的是ShuffledRDD

Scala
val lst: Seq[(String, Int)] = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
val partitioner = new HashPartitioner(wordAndOne.partitions.length)
//按照指定的分区进行分区
val partitioned: RDD[(String, Int)] = wordAndOne.partitionBy(partitioner)

6.6.19 repartitionAndSortWithinPartitions

按照值的分区器进行分区,并且将数据按照指的的排序规则在分区内排序,底层使用的是ShuffledRDD,设置了指定的分区器和排序规则

Scala
val lst: Seq[(String, Int)] = List(
  ("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
  ("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
  ("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
  ("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
val partitioner = new HashPartitioner(wordAndOne.partitions.length)
//按照指定的分区进行分区,并且将数据按照指定的排序规则在分区内排序
val partitioned = wordAndOne.repartitionAndSortWithinPartitions(partitioner)

repartitionAndSortWithinPartitions的底层实现:

Scala
new ShuffledRDD[K, V, V](self, partitioner).setKeyOrdering(ordering)

6.6.20 sortBy

Scala
val lines: RDD[String] = sc.textFile("hdfs://node-1.51doit.cn:9000/words")
//切分压平
val words: RDD[String] = lines.flatMap(_.split(" "))
//将单词和1组合
val wordAndOne: RDD[(String, Int)] = words.map((_, 1))
//分组聚合
val reduced: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)
//按照单词出现的次数,从高到低进行排序
val sorted: RDD[(String, Int)] = reduced.sortBy(_._2, false)

6.6.21 sortByKey

按照指的的key排序规则进行全局排序

Scala
val lines: RDD[String] = sc.textFile("hdfs://node-1.51doit.cn:9000/words")
//切分压平
val words: RDD[String] = lines.flatMap(_.split(" "))
//将单词和1组合
val wordAndOne: RDD[(String, Int)] = words.map((_, 1))
//分组聚合
val reduced: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)
//按照单词出现的次数,从高到低进行排序
//val sorted: RDD[(String, Int)] = reduced.sortBy(_._2, false)
//val keyed: RDD[(Int, (String, Int))] = reduced.keyBy(_._2).sortByKey()
val sorted = reduced.map(t => (t._2, t)).sortByKey(false)

sortBy、sortByKey是Transformation,但是为什么会生成job?

因为sortBy、sortByKey需要实现全局排序,使用的是RangePartitioner,在构建RangePartitioner时,会对数据进行采样,所有会触发Action,根据采样的结果来构建RangePartitioner。

RangePartitioner可以保证数据按照一定的范围全局有序,同时在shuffle的同时,有设置了setKeyOrdering,这样就又可以保证数据在每个分区内有序了!

6.6.22 reparation

reparation的功能是重新分区,一定会shuffle,即将数据打散。reparation的功能是改变分区数量(可以增大、减少、不变)可以将数据相对均匀的重新分区,可以改善数据倾斜的问题

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 3)
//repartition方法一定shuffle
//不论将分区数量变多、变少、或不变,都shuffle
val rdd2 = rdd1.repartition(3)

reparation的底层调用的是coalesce,shuffle = true

Scala
coalesce(numPartitions, shuffle = true)

6.6.23 coalesce

coalesce可以shuffle,也可以不shuffle,如果将分区数量减少,并且shuffle = false,就是将分区进行合并

  • shuffle = true

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 3)
//shuffle = true
val rdd2 = rdd1.coalesce(3, true)
//与repartition(3)功能一样

  • shuffle = false

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 4)
//shuffle = false
val rdd2 = rdd1.coalesce(2, false)

6.6.24  cogroup

协同分组,即将多个RDD中对应的数据,使用相同的分区器(HashPartitioner),将来自多个RDD中的key相同的数据通过网络传入到同一台机器的同一个分区中(与groupByKey、groupBy区别是,groupByKey、groupBy只能对一个RDD进行分组)

注意:调用cogroup方法,两个RDD中对应的数据都必须是对偶元组类型,并且key类型一定相同

Scala
//通过并行化的方式创建一个RDD
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2), ("jerry", 4)), 3)
//通过并行化的方式再创建一个RDD
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2), ("jerry", 4)), 2)
//将两个RDD都进行分组
val grouped: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd1.cogroup(rdd2)

6.6.25 join

两个RDD进行join,相当于SQL中的内关联join

两个RDD为什么要进行jion?想要的数据来自于两个数据集,并且两个数据集的数据存在相同的条件,必须关联起来才能得到想要的全部数据

Scala
//通过并行化的方式创建一个RDD
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)), 2)
//通过并行化的方式再创建一个RDD
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2), ("jerry", 4)), 2)
val rdd3: RDD[(String, (Int, Double))] = rdd1.join(rdd2)

6.6.26 leftOuterJoin

左外连接,相当于SQL中的左外关联

Scala
 //通过并行化的方式创建一个RDD
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)), 2)
//通过并行化的方式再创建一个RDD
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2), ("jerry", 4)), 2)
val rdd3: RDD[(String, (Int, Option[Int]))] = rdd1.leftOuterJoin(rdd2)

6.6.27 rightOuterJoin

右外连接,相当于SQL中的右外关联

Scala
//通过并行化的方式创建一个RDD
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)), 2)
//通过并行化的方式再创建一个RDD
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2), ("jerry", 4)), 2)
val rdd3: RDD[(String, (Option[Int], Int))] = rdd1.rightOuterJoin(rdd2)

6.6.28 fullOuterJoin

全连接,相当于SQL中的全关联

Scala
 //通过并行化的方式创建一个RDD
val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)), 2)
//通过并行化的方式再创建一个RDD
val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2), ("jerry", 4)), 2)
val rdd3: RDD[(String, (Option[Int], Option[Int]))] = rdd1.fullOuterJoin(rdd2)

6.6.29 intersection

求交集,底层使用的是cogroup实现的

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,4,6), 2)
val rdd2 = sc.parallelize(List(3,4,5,6,7,8), 2)
//求交集
val rdd3: RDD[Int] = rdd1.intersection(rdd2)

//使用cogroup实现intersection的功能
val rdd11 = rdd1.map((_, null))
val rdd22 = rdd2.map((_, null))
val rdd33: RDD[(Int, (Iterable[Null], Iterable[Null]))] = rdd11.cogroup(rdd22)
val rdd44: RDD[Int] = rdd33.filter { case (_, (it1, it2)) => it1.nonEmpty && it2.nonEmpty }.keys

6.6.30 subtract

求两个RDD的差集,将第一个RDD中的数据,如果在第二个RDD中出现了,就从第一个RDD中移除

Scala
val rdd1 = sc.parallelize(List("A", "B", "C", "D", "E"))
val rdd2 = sc.parallelize(List("A", "B"))

val rdd3: RDD[String] = rdd1.subtract(rdd2)
//返回 C D E

6.6.31 cartesian

笛卡尔积

Scala
val rdd1 = sc.parallelize(List("tom", "jerry"), 2)
val rdd2 = sc.parallelize(List("tom", "kitty", "shuke"), 3)
val rdd3 = rdd1.cartesian(rdd2)

6.7 RDD的Action算子

Action算子会触发Job的生成,底层调用的是sparkContext.runJob方法,根据最后一个RDD,从后往前,切分Stage,生成Task

6.7.1 saveAsTextFile

将数据以文本的形式保存到文件系统中,一个分区对应一个结果文件,可以指定hdfs文件系统,也可以指定本地文件系统(本地文件系统要写file://协议),数据的写入是下Executor中Task写入的,是多个Task并行写入的。

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,5), 2)
rdd1.saveAsTextFile("hdfs://node-1.51doit.cn:9000/out2")

6.7.2 collect

每个分区对应的Task,将数据在Executor中,将数据以集合的形式保存到内存中,然后将每个分区对应的数据以数组形式通过网络收集回Driver端,数据按照分区编号有序返回

Scala

val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 4)
val rdd2 = rdd1.map(_ * 10)
//调用collect方法,是一个Action
val res: Array[Int] = rdd2.collect()
println(res.toBuffer)

collect底层实现:

Scala
def collect(): Array[T] = withScope {
  //this代表最后一个RDD,即触发Action的RDD
  //(iter: Iterator[T]) => iter.toArray 函数代表对最后一个进行的处理逻辑,即将每个分区对应的迭代器中的数据迭代处出来,放到内存中
  //最后将没法分区对应的数组通过网络传输到Driver端
  val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
  //在Driver端,将多个数组合并成一个数组
  Array.concat(results: _*)
}

使用collect方法的注意事项:

如果Driver的内存相对较小,并且每个分区对应的数据比较大,通过网络传输的数据,返回到Driver,当返回到Driver端的数据达到了一定大小,就不收集了,即将一部分无法收集的数据丢弃

如果需要将大量的数据收集到Driver端,那么可以在提交任务的时候指定Driver的内存大小 (--driver-memory 2g)

6.7.3 aggregate

aggregate方式是Action,可以将多个分区的数据进行聚合运算,例如进行相加,比较大小等

aggregate方法可以指定一个初始值,初始值在每个分区进行聚合时会应用一次,全局聚合时会在使用一次

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 4)

//f1是在Executor端执行的
val f1 = (a: Int, b: Int) => {
  println("f1 function invoked ~~~~")
  a + b
}

//f2实在Driver端执行的
val f2 = (m: Int, n: Int) => {
  println("f2 function invoked !!!!")
  m + n
}

//返回的结果为55
val r1: Int = rdd1.aggregate(0)(f1, f2)

//返回的结果为50055
val r2: Int = rdd1.aggregate(10000)(f1, f2)

Scala
val rdd1 = sc.parallelize(List("a", "b", "c", "d"), 2)
val r: String = rdd1.aggregate("&")(_ + _, _ + _)

//返回的回的有两种:应为task的分布式并行运行的,先返回的结果在前面
// &&cd&ab 或 &&ab&cd

6.7.4 reduce

将数据先在每个分区内进行局部聚合,然后将每个分区返回的结果在Driver端进行全局聚合

Scala
 
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 4)
val f1 = (a: Int, b: Int) => {
  println("f1 function invoked ~~~~")
  a + b
}
//f1这个函数即在Executor中执行,又在Driver端执行
//reduce方法局部聚合的逻辑和全局聚合的逻辑是一样的
//局部聚合是在每个分区内完成(Executor)
//全局聚合实在Driver完成的
val r = rdd1.reduce(f1)

6.7.5 sum

 sum方法是Action,实现的逻辑只能是相加

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 4)
//sum底层调用的是fold,该方法是一个柯里化方法,第一个括号传入的初始值是0.0
//第二个括号传入的函数(_ + _) ,局部聚合和全局聚合都是相加
val r = rdd1.sum()

6.7.6 fold

fold跟reduce类似,只不过fold是一个柯里化方法,第一个参数可以指定一个初始值

Scala
val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 4)
//fold与reduce方法类似,该方法是一个柯里化方法,第一个括号传入的初始值是0.0
//第二个括号传入的函数(_ + _) ,局部聚合和全局聚合都是相加
val r = rdd1.fold(0)(_ + _)

6.7.7 min、max

将整个RDD中全部对应的数据求最大值或最小值,底层的实现是:现在每个分区内求最大值或最小值,然后将每个分区返回的数据在Driver端再进行比较(min、max没有shuffle)

Scala
val rdd1 = sc.parallelize(List(5,7 ,9,6,1 ,8,2, 4,3,10), 4)
//没有shuffle
val r: Int = rdd1.max()

6.7.8 count

返回rdd元素的数量,先在每个分区内求数据的条数,然后再将每个分区返回的条数在Driver进行求和

Scala
val rdd1 = sc.parallelize(List(5,7 ,9,6,1 ,8,2, 4,3,10), 4)
//在每个分区内先计算每个分区对应的数据条数(使用的是边遍历,边计数)
//然后再将每个分区返回的条数,在Driver进行求和
val r: Long = rdd1.count()

6.7.9 take

返回一个由数据集的前n个元素组成的数组,即从RDD的0号分区开始取数据,take可能触发一到多次Action(可能生成多个Job)因为首先从0号分区取数据,如果取够了,就直接返回,没有取够,再触发Action,从后面的分区继续取数据,直到取够指定的条数为止

Scala
val rdd1 = sc.parallelize(List(5,7 ,9,6,1 ,8,2, 4,3,10), 4)
//可能会触发一到多次Action
val res: Array[Int] = rdd1.take(2)

6.7.10 first

返回RDD中的第一个元素,类似于take(1),first返回的不是数组

Scala
val rdd1 = sc.parallelize(List(5,7 ,9,6,1 ,8,2, 4,3,10), 4)
//返回RDD中对应的第一条数据
val r: Int = rdd1.first()

6.7.11 top

将RDD中数据按照降序或者指定的排序规则,返回前n个元素

Scala
val rdd1 = sc.parallelize(List(
  5, 7, 6, 4,
  9, 6, 1, 7,
  8, 2, 8, 5,
  4, 3, 10, 9
), 4)

val res1: Array[Int] = rdd1.top(2)
//指定排序规则,如果没有指定,使用默认的排序规则
implicit val ord = Ordering[Int].reverse
val res2: Array[Int] = rdd1.top(2)
val res3: Array[Int] = rdd1.top(2)(Ordering[Int].reverse)

top底层调用的使用takeOrdered

Scala
def top(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope {
  takeOrdered(num)(ord.reverse)
}

6.7.12 takeOrdered

top底层丢的是takeOrdered,takeOrdered更灵活,可以传指定排序规则。底层是先在每个分区内求topN,然后将每个分区返回的结果再在Diver端求topN

在每个分区内进行排序,使用的是有界优先队列,特点是数据添加到其中,就会按照指定的排序规则排序,并且允许数据重复,最多只存放最大或最小的N个元素

Scala
def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope {
  if (num == 0) {
    Array.empty
  } else {
    val mapRDDs = mapPartitions { items =>
      // Priority keeps the largest elements, so let's reverse the ordering.
      //使用有界优先队列
      val queue = new BoundedPriorityQueue[T](num)(ord.reverse)
      queue ++= collectionUtils.takeOrdered(items, num)(ord)
      Iterator.single(queue)
    }
    if (mapRDDs.partitions.length == 0) {
      Array.empty
    } else {
      mapRDDs.reduce { (queue1, queue2) =>
        queue1 ++= queue2 //将多个有界优先队列进行++= ,返回两个有界优先队列最大的N个
        queue1
      }.toArray.sorted(ord)
    }
  }
}

  

6.7.13 foreach

将数据一条一条的取出来进行处理,函数没有返回

Scala
val sc = SparkUtil.getContext("FlowCount", true)

val rdd1 = sc.parallelize(List(
  5, 7, 6, 4,
  9, 6, 1, 7,
  8, 2, 8, 5,
  4, 3, 10, 9
), 4)

rdd1.foreach(e => {
  println(e * 10) //函数是在Executor中执行
})

使用foreach将数据写入到MySQL中,不好 ,效率低

Scala
rdd1.foreach(e => {
  //但是不好,为什么?
  //每写一条数据用一个连接对象,效率太低了
  val connection = DriverManager.getConnection("jdbc:mysql://node-1.51doit.cn:3306/doit35?characterEncoding=utf-8", "root", "123456")
  val preparedStatement = connection.prepareStatement("Insert into tb_res values (?)")
  preparedStatement.setInt(1, e)
  preparedStatement.executeUpdate()
})

6.7.14 foreachPartition

和foreach类似,只不过是以分区位单位,一个分区对应一个迭代器,应用外部传的函数,函数没有返回值,通常使用该方法将数据写入到外部存储系统中,一个分区获取一个连接,效率更高

Scala
rdd1.foreachPartition(it => {
  //先创建好一个连接对象
  val connection = DriverManager.getConnection("jdbc:mysql://node-1.51doit.cn:3306/doit35?characterEncoding=utf-8", "root", "123456")
  val preparedStatement = connection.prepareStatement("Insert into tb_res values (?)")
  //一个分区中的多条数据用一个连接进行处理
  it.foreach(e => {
    preparedStatement.setInt(1, e)
    preparedStatement.executeUpdate()
  })
  //用完后关闭连接
  preparedStatement.close()
  connection.close()
})

6.8 RDD特殊的算子

6.8.1 cache、persist

将数据缓存到内存,第一次触发Action,才会将数据放入内存,以后在触发Action,可以复用前面内存中缓存的数据,可以提升技术效率

cache和persist的使用场景:一个application多次触发Action,为了复用前面RDD的数据,避免反复读取HDFS(数据源)中的数据和重复计算,可以将数据缓存到内存或磁盘【executor所在的磁盘】,第一次触发action才放入到内存或磁盘,以后会缓存的RDD进行操作可以复用缓存的数据。

一个RDD多次触发Action缓存才有意义,如果将数据缓存到内存,内存不够,以分区位单位,只缓存部分分区的数据,cache底层调用persist,可以指定更加丰富的存储基本,支持多种StageLevel,可以将数据序列化,默认放入内存使用的是java对象存储,但是占用空间大,优点速度快,也可以使用其他的序列化方式

cache和persist方法,严格来说,不是Transformation,应为没有生成新的RDD,只是标记当前rdd要cache或persist

6.8.2 checkpoint

checkpoint使用场景:适合复杂的计算【机器学习、迭代计算】,为了避免中间结果数据丢失重复计算,可以将宝贵的中间结果保存到hdfs中,保证中间结果安全。

在调用rdd的checkpint方法之前,一定要指定checkpoint的目录sc.setCheckPointDir,指的HDFS存储目录,为保证中间结果安全,将数据保存到HDFS中

第一次触发Action,才做checkpoint,会额外触发一个job,这个job的目的就是将结果保存到HDFS中

如果RDD做了checkpoint,这个RDD以前的依赖关系就不在使用了,触发多次Action,checkpoint才有意义,多用于迭代计算

checkpoint严格的说,不是Transformation,只是标记当前RDD要做checkpoint

你可能感兴趣的:(hadoop,hdfs,大数据)