大数据之spark_spark的Transformation算子解析

map

功能所做映射

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

flatMap

先map在压平,spark中没有flatten方法

val rdd2 = sc.parallelize(Array("a b c", "d e f", "h i j"))
rdd2.flatMap(_.split(' ')).collect
val rdd3 = sc.parallelize(List(List("a b c", "a b b"),List("e f g", "a f g")))
rdd3.flatMap(_.flatMap(_.split(" "))).collect

filter

功能为过滤数据

val rdd4 = sc.parallelize(List(5,6,4,7,3,8,2,9,1,10)).filter(_ % 2 == 0)

mapPartitions

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

mapPartitions算子和map算子的实际区别:

map算子:方法参数为RDD中的每一个元素,分析map算子源码,在执行map方法时,实际是将每个分区都转换成一个迭代器,然后由该迭代器将区内的数据一条条的取出进行map运算,每次处理一条数据,map处理的对象为每条数据

mapPartitions算子:方法参数为每一个分区的迭代器,然后我们可以对迭代器内的数据进行一系列其他操作,但最后也必须返回一个迭代器,进行操作时,调用的方法都是迭代器中的方法,mapPartitions算子每次可以处理一个区的数据,所以它的处理对象为每个区

所以mapPartitions要比map高效的多

val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5), 2)
//it表示分区的迭代器,返回的也必须是一个迭代器,所以调用的map为迭代器的map,而非RDD的map
var r1: RDD[Int] = rdd1.mapPartitions(it => it.map(x => x * 10))

mapPartitionsWithIndex

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

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

keys

可以取到对偶元组型数据的1号位的所有元素

val rdd2: RDD[(String, Int)] = sc.parallelize(List(("a", 1), ("b", 2), ("c", 3)))

val keys: RDD[String] = rdd2.keys

values

可以取到对偶元组型数据的2号位的所有元素

val rdd2: RDD[(String, Int)] = sc.parallelize(List(("a", 1), ("b", 2), ("c", 3)))
    
val values: RDD[Int] = rdd2.values

mapValues

可以取到对偶元组型数据的2号位的所有元素,并对其进行map操作,1号位元素key保持不变

val rdd2: RDD[(String, Int)] = sc.parallelize(List(("a", 1), ("b", 2), ("c", 3)))

val value: RDD[(String, Int)] = rdd2.mapValues(x => x * 10)

union

并集,要求两个RDD类型要一样,然后进行合并,不仅合并数据,而且合并分区

val rdd6 = sc.parallelize(List(5,6,4,7), 2)
val rdd7 = sc.parallelize(List(1,2,3,4), 3)
val rdd8 = rdd6.union(rdd7)  //新的rdd分区数为5

shuffle算子

什么是shuffle:将数据从上游按照一定的规律(由分区器决定),将数据给到下游的分区.严格的说,是下游task到上游拉取数据,shuffle不一定走网络,但走网络的也不一定就是shuffle,shuffle时上游一个分区的数据可能给到下游多个分区,严格的说是下游的task到上游的多个分区拉取数据,只有存在这种可能就是shuffle
在这里插入图片描述

shuffle算子在shuffle之前都必须获取上游的元数据信息和分区信息

shuffle算子shuffle后重新分区时,会调用HashPartition中getPartition方法对key进行hash运算:运算方法:key.hashCode%分区数(当key为null的时候,默认分到0号区,所以当出现大量的null的时候,可能会出现数据倾斜),然后就判断hash值是正数还是负数,如果是负数就再加上分区数,使负数变成正数,如果是正数就加上0,从而将各个key分到了新的区内.

groupByKey

将key相同的value进行分组聚合到同一区中,先在shuffle前进行局部聚合,然后再shuffle后全局聚合,方法的括号内可以指定下游的分区数,不指定就和上游保持一致,groupByKey对同一个key中的value拉取数据时,使用的是迭代器拉取.

val rdd2 = sc.parallelize(List(("jerry", 9), ("tom", 8), ("shuke", 7), ("tom", 2)))
val rdd2: RDD[(String, Iterable[Int])] = rdd1.groupByKey(5) //指定下游分区数为5
//聚合后("tom", (8,2)),("jerry", 9),("shuke", 7)

groupBy

使用该算子,可以指定分区字段,然后按我们自己指定的字段进行聚合,但聚合后会将之前所有的数据都放到新的value中,因为底层是将指定的字段作为key,之前所有的字段作为value生成新的元组,进行聚合的,所以groupBy相对于groupByKey更灵活,但不如groupByKey高效

val rdd1 = sc.parallelize(List(("hello", 9), ("tom", 8), ("kitty", 7), ("tom", 2)))
val rdd2 = rdd1.groupBy(_._2)

reduceByKey

使用该算子,是将key相同的数据在shuffle之前进行聚合并加总,然后到shuffle之后再全局聚合并加总

val rdd2 = sc.parallelize(List(("jerry", 9), ("tom", 8), ("shuke", 7), ("tom", 2)))
val rdd3 = rdd1.reduceByKey(_+_)

reduceByKey相对于groupByKey.mapValues(it => it.sum)更高效,因为reduceByKey会提前聚合加总一遍,减少了shuffle时需要传输的数据量

aggregateByKey

val pairRDD = sc.parallelize(List(("cat",2), ("cat", 5), ("mouse", 4),
  ("cat", 12), ("dog", 12), ("mouse", 2)), 2)
//用法1:
pairRDD.aggregateByKey(0)(_ + _, _ + _).collect  //等价于reduceByKey的功能
//参一为初始值,该初始值只在局部聚合时生效,局部聚合时,相当于每个shuffle之前的每个分区中相同的key的value都多了一个初始值,该初始值可以看作是多出的value
//参二为局部聚合函数,可以在shuffle之前对每个区内相同key的value进行一系列的操作
//参三为全局聚合函数,可以在shuffle之后全局聚合时,自定义聚合逻辑

//例如用法2:先给他一个初始值为100,然后局部求value中的最大值,然后每个分区都取到最大值后,再shuffle进行全局聚合
pairRDD.aggregateByKey(100)(math.max(_, _), _ + _).collect

foldByKey

该算子的作用于aggregateByKey算子的作用相似,也有一个初始值,但是只在全局聚合时可以自定义聚合逻辑,不如aggregateByKey算子灵活

pairRDD.foldByKey(100)(_ + _).collect

zip

该算子的作用是,将两个老算子进行映射生成对偶元组的新算子

val a = sc.parallelize(List("dog","cat","gnu","salmon",
  "rabbit","turkey","wolf","bear","bee"), 3)
val b = sc.parallelize(List(1,1,2,2,2,1,2,2,2), 3)
val c = b.zip(a) //("dog",1),("cat",1),("gnu",2)......

distinct

去重,去重时,是先进行局部去重,再全局去重,该算子底部调用的是reduceBykey方法进行去重的:

map(x => (x,null)).reduceByKey((a,_) => a ).map(_._1)
//处理相同的数据时,先将每个数据使用map方法都加上一个value为null的值,然后使用reduceByKey对null
//进行聚合,null聚合时一直都是null,所以聚合后,只取初始值a就可以了,聚合的另一个值用不着就可以用下划
//线表示,聚合后再取出元组中的第一个元素key,从而达到去重的效果
sc.parallelize(List(5,5,6,6,7,8,8,8)).distinct.collect

reduceByKey扩展

reduceByKey、foldByKey、aggregateByKey、combineByKey底层调用的都是combineByKeyWithClassTag,然后在combineByKeyWithClassTag中都有combineByKey和ShuffledRDD对数据进行处理,我们可以自己用combineByKey和ShuffledRDD实现reduceByKey的功能


import org.apache.spark.rdd.{PairRDDFunctions, RDD, ShuffledRDD}
import org.apache.spark.{Aggregator, HashPartitioner, SparkConf, SparkContext}

import scala.collection.mutable.ArrayBuffer

object ReduceByKeyDemo {

  def main(args: Array[String]): Unit = {

    val conf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    //创建SparkContext
    val sc: SparkContext = new SparkContext(conf)

    val lines = sc.textFile(args(0))

    val wordAndOne: RDD[(String, Int)] = lines.flatMap(_.split(" ")).map((_, 1))

    //分组聚合效率最高的reduceByKey
    //val redcued: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)

    //使用combineByKey实现与reduceByKey相同的功能
    val f1 = (v: Int) => v
    val f2 = (acc: Int, v2: Int) => acc + v2
    val f3 = (acc1: Int, acc2: Int) => acc1 + acc2;

    //val reduced: RDD[(String, Int)] = wordAndOne.combineByKey(f1, f2, f3, new HashPartitioner(wordAndOne.partitions.length), true)


    //自己new ShuffleRDD实现与reduceByKey相同的功能
    val shuffledRDD = new ShuffledRDD[String, Int, Int](wordAndOne, new HashPartitioner(wordAndOne.partitions.length))

    shuffledRDD.setAggregator(new Aggregator[String, Int, Int](f1, f2, f3))
    shuffledRDD.setMapSideCombine(true)
    //val res = reduced.collect()

    val res = shuffledRDD.collect()

    println(res.toBuffer)
  }

}

cogroup

协分组器,将多个RDD联合起来分组(group只对一个RDD分组),分组时,对两个RDD中key相同的进行分组,得到的value是两个RDD中key相同对应value的迭代器(因为一个key可能对应多个value),此分组器,没有局部聚合,如果是两个RDD进行cogroup会得到三个stage,两个shuffleWrite,一个sguffleReade,cogroup不仅可以对两个RDD进行分组,还可以对多个RDD进行分组,但最多四个,如果还想分更多,可以多次cogroup,在cogrop时,要求这几个RDD的key的类型必须相同,value可以不同.

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

    val rdd3: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd1.cogroup(rdd2)

    println(rdd3.collect().toBuffer)
    
得到如下数据
ArrayBuffer((tom,(CompactBuffer(1, 2),CompactBuffer(1))), 
(kitty,(CompactBuffer(2),CompactBuffer())), 
(jerry,(CompactBuffer(3),CompactBuffer(2))), 
(shuke,(CompactBuffer(),CompactBuffer(2))))

intersection

取两个RDD内元素完全相同时的交集(如果有重复的key,取交集的时候会去重)

    val rdd1 = sc.parallelize(List(("tom", 1), ("tom", 2), ("jerry", 3), ("kitty", 2)))
    val rdd2 = sc.parallelize(List(("jerry", 2), ("tom", 1), ("shuke", 2)))
    
    val rdd3: RDD[(String, Int)] = rdd1.intersection(rdd2)

    println(rdd3.collect().toBuffer)

得到如下数据
ArrayBuffer((tom,1))

该算子底层调用的是cogroup

  /**
   * Return the intersection of this RDD and another one. The output will not contain any duplicate
   * elements, even if the input RDDs did.
   *
   * @note This method performs a shuffle internally.
   */
  def intersection(other: RDD[T]): RDD[T] = withScope {
    this.map(v => (v, null)).cogroup(other.map(v => (v, null)))
        .filter { case (_, (leftGroup, rightGroup)) => leftGroup.nonEmpty && rightGroup.nonEmpty }
        .keys
  }

jion

相当于sql中的join关联,会将两个RDD中相同key的value关联起来,其他key不同的数据都筛除掉

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

    val rdd3: RDD[(String, (Int, Int))] = rdd1.join(rdd2)

    println(rdd3.collect().toBuffer)

得到如下数据
ArrayBuffer((tom,(1,1)), (tom,(2,1)), (jerry,(3,2)))

jion底层调用的也是cogroup

  /**
   * Return an RDD containing all pairs of elements with matching keys in `this` and `other`. Each
   * pair of elements will be returned as a (k, (v1, v2)) tuple, where (k, v1) is in `this` and
   * (k, v2) is in `other`. Uses the given Partitioner to partition the output RDD.
   */
  def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))] = self.withScope {
    this.cogroup(other, partitioner).flatMapValues( pair =>
      for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, w)
    )
  }

leftOuterJoin

左外连接,右边有相同的key就返回Some(value),没有就返回None,左边RDD的key全保留

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

    val rdd3: RDD[(String, (Int, Option[Int]))] = rdd1.leftOuterJoin(rdd2)

    println(rdd3.collect().toBuffer)

得到如下数据:
ArrayBuffer((tom,(1,Some(1))), (tom,(2,Some(1))), (kitty,(2,None)), (jerry,(3,Some(2))))

底层源码

  def leftOuterJoin[W](
      other: RDD[(K, W)],
      partitioner: Partitioner): RDD[(K, (V, Option[W]))] = self.withScope {
    this.cogroup(other, partitioner).flatMapValues { pair =>
      if (pair._2.isEmpty) {
        pair._1.iterator.map(v => (v, None))
      } else {
        for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, Some(w))
      }
    }
  }

rightOuterJoin

右外连接

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

    val rdd3: RDD[(String, (Option[Int],Int ))] = rdd1.rightOuterJoin(rdd2)

    println(rdd3.collect().toBuffer)
  
  得到如下数据
  ArrayBuffer((tom,(Some(1),1)), (tom,(Some(2),1)), (jerry,(Some(3),2)), (shuke,(None,2)))

底层源码

  def rightOuterJoin[W](other: RDD[(K, W)], partitioner: Partitioner)
      : RDD[(K, (Option[V], W))] = self.withScope {
    this.cogroup(other, partitioner).flatMapValues { pair =>
      if (pair._1.isEmpty) {
        pair._2.iterator.map(w => (None, w))
      } else {
        for (v <- pair._1.iterator; w <- pair._2.iterator) yield (Some(v), w)
      }
    }
  }

fullOuterJoin

全外连接


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

    val rdd3: RDD[(String, (Option[Int], Option[Int]))] = rdd1.fullOuterJoin(rdd2)

    println(rdd3.collect().toBuffer)

得到如下数据
ArrayBuffer((tom,(Some(1),Some(1))), (tom,(Some(2),Some(1))), (kitty,(Some(2),None)), (jerry,(Some(3),Some(2))), (shuke,(None,Some(2))))

底层源码

  def fullOuterJoin[W](other: RDD[(K, W)], partitioner: Partitioner)
      : RDD[(K, (Option[V], Option[W]))] = self.withScope {
    this.cogroup(other, partitioner).flatMapValues {
      case (vs, Seq()) => vs.iterator.map(v => (Some(v), None))
      case (Seq(), ws) => ws.iterator.map(w => (None, Some(w)))
      case (vs, ws) => for (v <- vs.iterator; w <- ws.iterator) yield (Some(v), Some(w))
    }
  }

subtract

求差集,判断两个RDD中的元素是否完全相等如果相等就移除掉,但差集之后只会保留左RDD中的数据

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

    val rdd3: RDD[(String, Int)] = rdd1.subtract(rdd2)

    println(rdd3.collect().toBuffer)

得到数据如下:
ArrayBuffer((jerry,3), (tom,2), (kitty,2))

subtractByKey

根据对偶元组中的key求差集,同样差集之后只会保留左RDD中的数据

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

    val rdd3: RDD[(String, Int)] = rdd1.subtractByKey(rdd2)

    println(rdd3.collect().toBuffer)
    
得到数据如下:
ArrayBuffer((kitty,2))

repartition

重新分区,但需要注意,同一个RDD调用多次repartition,并传入的分区数量都一样的话,那么就只会重新分区一次,后面调用的都不生效,数据不会发生变化,repartition方法在重新分区时一定会产生shuffle,在减少分区时不想产生shuffle的话可以调用coalesce算子

重新分区的作用:对原始分区不满意,想提高并行度加快运算速度时,可以重新分区,且重新分区将分区数变多时还可以减少数据倾斜的问题


    val rdd1 = sc.parallelize(List(("a", 1), ("b", 2), ("c", 3), ("d", 2),("e", 2),("f", 2),("g", 2),("h", 2)),4)

    val rdd2: RDD[(String, Int)] = rdd1.repartition(3)

    println(rdd2.partitions.length)  //   3

coalesce

当设定的分区数小于原始分区数时,可以不用产生shuffle,因为它减少分区时,是直接将之前的分区进行两两合并的操作形式,不产生shuffle的话就只会有一个stage,那么task的数量取决于当前stage的最后一个RDD的task数是多少,就生成多少个task
但分区数变多时,也必须产生shuffle,且调用该方法是可以传入第二个参数false或true,false就是不产生shuffle,只在减少分区时生效,true就是产生shuffle,减少或增加分区都生效
在这里插入图片描述
在这里插入图片描述

    val rdd1 = sc.parallelize(List(("a", 1), ("b", 2), ("c", 3), ("d", 2),("e", 2),("f", 2),("g", 2),("h", 2)),4)

    val rdd2: RDD[(String, Int)] = rdd1.coalesce(3)

    rdd2.collect()
    Thread.sleep(100000000)

partitionBy

可以自定义分区器,将相同的key分到同一个区内,但最后的结果不能保证数据均匀,会产生Shuffle

    val rdd1 = sc.parallelize(List(("a", 1), ("b", 2), ("c", 3), ("d", 4),("a", 5),("a", 6),("g", 7),("b", 8)),2)

    val rdd2: RDD[(String, Int)] = rdd1.partitionBy(new HashPartitioner(4))


    val rdd3: RDD[String] = rdd2.mapPartitionsWithIndex((inx, it) => it.map(x => inx + ":" + x))

    println(rdd3.collect().toBuffer)

    Thread.sleep(100000000)

得到如下数据:
ArrayBuffer(0:(d,4), 1:(a,1), 1:(a,5), 1:(a,6), 2:(b,2), 2:(b,8), 3:(c,3), 3:(g,7))

sortBy/sortByKey

按指定字段排序,和按key排序

注意:sortBy(x[Int] => x.toString) 最后得到的数据类型还是Int类型,因为sortBy括号内传入的排序规则,返回的数据类似始终是它本身,x.toString只是将他临时转成字符串,然后按字典顺序排序而已.

sortBy算子指定字段后,底层会将指定的字段变成key,然后调用sortByKey

sortByKey算子在底层排序时,会先在每个区内进行采样,然后根据采样数据运算出所有数据的一个大概的数据范围,然后按数据范围对下游进行分区,第一个区到最后一个区的每一个区都是有数据范围的,是按采样得到的总数据范围指定每一个区内数据范围,并按区的顺序排序好的,数据在进行局部排序之后,就会将每个区内的数据按这个范围shuffle到对应的区中,然后在下游的区中进行区内排序,排好之后,将数据按区的顺序读取出来,最终就可以得到完全排好的数据.

在进行随机采样时,会触发一次action,生成一个Job,因为底层调用了collect,该Job将采到数据收集起来,构建下游分区器RangePartition(范围分区器),划分数据储存范围,构建分区器也是为了shuffle做准备,且在shffle之前为了提高工作效率,会先在区内进行局部排序,排序后,将上游的数据分段,指定每段数据去到下游哪个分区,shuffle后归并全局数据,局部排序和全局排序都是使用的是内存加磁盘的形式进行操作,所以不用担心内存溢出数据丢失的问题.

sc.parallelize(List(5,11,22,13,2,1,10)).sortBy(x=>x,true)
sc.parallelize(List(5,11,22,13,2,1,10)).sortBy(x=>x+"",true)
sc.parallelize(List(5,11,22,13,2,1,10)).sortBy(x=>x.toString,true)
val rdd1 = sc.parallelize(List(("hello", 9), ("tom", 8), ("kitty", 7), ("tom", 2)))
val rdd2 = rdd1.sortByKey(false)

sortBy/sortByKey括号里面都可以传入一个布尔值,true升序,false降序,默认为true

排序扩展:

对于超出内存的大文件,我们想对其排序,并取topN时,我们还可以用以下方式实现:
1.将这个大文件分成多个小文件,然后取每个小文件中的topN,再将每个小文件的topN进行归并整合,取出最终的topN.
2.定义一个TreeSet,长度设定为topN的长度加1,然后自定义TreeSet的排序规则,用迭代器往TreeSet里面放数据,每放一条,再移除不满足条件的一条,到最后,TreeSet里面保留的就是我们想要的topN
3.使用有界优先队列,该队列会类似于TreeSet,迭代器往里面放数据时,会自动保留满足条件的几条数据,但不会在队列中排序,而且它也是在每个分区中取到满足条件的n条数据,然后调用reduce算子归并每个分区中的元素,取出最后我们想要的结果,取出数据后需要自己对他排一下序

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值