spark常用算子解析

概述
spark 算子分为两类:transform与action两类,其中transform类算子只是定义一系列处理逻辑,它并不会触发计算而action 算子会触发整个计算逻辑。

Transform类算子:

  1. map 与 mapPartitions
  /** **
     * map算子
     * 对RDD中的每个元素都执行传入的函数
     * eg:对每个元素都做+1 操作
     */
    val sparkSession = SparkSession.builder().
      master("local[*]").
      appName("tranform-operator").
      getOrCreate()
    val sc = sparkSession.sparkContext
    val sourceRdd = sc.parallelize(0.to(10))
    sourceRdd.map(_ + 1).foreach(println(_))
    sparkSession.stop()
/****
*mapPartitions算子是针对每个分区的元素做转换
* 
****/ 
  val sparkSession = SparkSession.builder().
      master("local[*]").
      appName("tranform-operator").
      getOrCreate()
    val sc = sparkSession.sparkContext
    val sourceRdd = sc.parallelize(0.to(10))
    sourceRdd.mapPartitions(partition => {
      println("-----------------"+partition)
      partition.map(x => x + 1)
    })
    sparkSession.stop()

小结:
这里可以直接参考:https://www.cnblogs.com/mazhujun/p/9627346.html
map算子是每次传入一个元素到我们定义的函数中而mapPartitions算子是将整个分区传入我们定义的函数中。
map算子定义:

  def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    //这里即将每个元素作用于我们自己定义的函数
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
  }

mapPartitions算子定义:

 def mapPartitions[U: ClassTag](
      f: Iterator[T] => Iterator[U],
      preservesPartitioning: Boolean = false): RDD[U] = withScope {
    val cleanedF = sc.clean(f)
    //这里即将整个分区传入到我们定义的函数中
    new MapPartitionsRDD(
      this,
      (context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(iter),
      preservesPartitioning)
  }

由上面的一些定义我们可以想到相关的一些场景, 例如数据库等需要进行连接的创建这时如果使用map算子那么会没个元素都去建立一次连接 这样的做法一方面是性能方面有很大的隐患另外是数据库连接也是存在上限的,对数据库也是会造成影响的。另外这里在使用mapPartitions时还需要注意应当避免在mapPartitons内定义缓冲的buffer因为这样会将数据放在内存中有可能会引发oom问题,正确的做法是定义一个iterator类避免数据存放在内存。

错误用法:
这里会将数据堆积在内存中

sourceRdd.repartition(1).mapPartitions(
       partition => {
         var list=List[Int]()
         while (partition.hasNext){
           val i=partition.next()
           println("------"+i)
           list=list.::(i)
         }
         list.iterator
       }).foreach(println(_))

正确方式:
这种方式避免了创建过多的数据库连接 同时又避免了将数据直接堆积在内存。

 sourceRdd.repartition(1).mapPartitions(
      partition => {
        // val connection=DriverManager.getConnection().....
        val res = partition.map(x => {
          //connection.insert()....
          println("----------" + x)
          x
        })
        //connection.close()
        res
      }).foreach(println(_))
    sparkSession.stop()
  }

2)flatMap
flatMap 算子即对传入的每个集合中的每个元素做统一的一个转换操作然后在将所有集合的结果放入统一的一个集合中,

  val sourceRdd = sc.parallelize(Array(Array(0, 1), Array(2, 3)))
    sourceRdd.foreach(x => {
      println(x.isInstanceOf[Array[Int]])
      println(x)
      x.foreach(println(_))
    })
    println("-----------------------------------------------")
    sourceRdd.flatMap(x=>{x.map(_+1)}).foreach(println(_))
    sparkSession.stop()

这里的打印结果:可以看到在经过flatMap的作用后已经传入到foreach中的已经是一个iterator了不在是单一的一个元素集合了
true
true
[I@3f6b24aa
[I@dc8f1c9
0
2
1
3

-------------------------------------
1
2
3
4

3)filter
filter 是过滤操作,将rdd中不满足条件的数据过滤掉并返回新的Rdd

  val sourceRdd = sc.parallelize(Array(1,2,3,4,5))
  sourceRdd.filter(x=>x>4).foreach(println(_))
  结果:5

4)distinct算子
对Rdd中的数据进行去重操作

 val sourceRdd = sc.parallelize(Array(1,1,2,3,4,5))
 sourceRdd.distinct().foreach(println(_))

5)reduceByKey
键值对算子。按照key 进行分组然后对数据进行聚合操作。

 val sourceRdd = sc.parallelize(Array((1,2),(1,3),(2,1),(2,2)))
sourceRdd.reduceByKey((x,y)=>x+y).foreach(println(_))
sparkSession.stop()
结果:
(2,3)
(1,5)

6)groupByKey
对相同key 的数据进行分组:

    val sourceRdd = sc.parallelize(Array((1,2),(1,3),(2,1),(2,2)))
    sourceRdd.groupByKey().foreach(println(_))
    sparkSession.stop()
    结果:   
    (2,CompactBuffer(1, 2))
    (1,CompactBuffer(2, 3))

小结:这里需要说明下 如果实现相同效果的功能尽可能使用reduceByKey操作,reduceByKey 会在map端进行数据的聚合操作,在reduce端数据量会减少很多,而groupByKey不会进行Map聚合 所有数据都需要做shuffer处理,reduceByke与groupByKey都是使用combineByKeyWithClassTag实现的 combineByKeyWithClassTag算子提供的mapSideCombine(是否map端聚合)groupByKey 提供的参数是false reduce 直接使用默认的true。
7)aggregateByKey
算子定义:
参数解析:
第一部分参数: zeroValue 初始值(这个值在分区不同的情况下会影响最终的结果,这个值是每个分区中的每一个分组的初始值)
numPartitions 转换后的分区数

第二部分参数:
seqOp:此函数针对map端数据做初步聚合
combOp:对Map端聚合的数据进行二次聚合

 def aggregateByKey[U: ClassTag](zeroValue: U, numPartitions: Int)(seqOp: (U, V) => U,
      combOp: (U, U) => U): RDD[(K, U)] = self.withScope {
    aggregateByKey(zeroValue, new HashPartitioner(numPartitions))(seqOp, combOp)
  }

eg:

 val sourceRdd = sc.parallelize(Array((1, 2), (1, 3))).repartition(2)
    sourceRdd.aggregateByKey(1, 2)((x, y) => {
      println("==========" + x + " " + y)
      x+y
    }, (c, d) => c + d).foreach(println(_))
    sparkSession.stop()
说明: 上面说到初始值在分区不同情况下结果会不同
这里分别将分区设置为1 2结果分别为:(16)  (17)
而打印的x y 两次也分别为:
分区为11,2  第一次迭代
3.3  第二次迭代
结果为第二次迭代的和 6
分区为21,3  第一个分区
1,2  第二个分区
结果为每个分区迭代结果之和在求和 7
以上是通过实验得出的结论 这部分的详细代码可以参考  ExternalAppendOnlyMap 的insert方
法,这里会判断是否已经存在key值在map中如果不存在则用我们的那个初始值做一些处理,如果存在
则直接执行我们定义的map端函数,这里收分区数影响应该是每个分区都是使用的不同的map对象。

8)combineByKey
combineByKey函数与aggregateByKey函数类似:
函数共接受三个参数:
1)初始化函数(这个函数的作用逻辑与上面的aggregateByKey 一致)
2)map聚合函数
3) reduce端聚合函数

  val sourceRdd = sc.parallelize(Array((1, 2), (1, 3))).repartition(1)
  sourceRdd.combineByKey(
  v=>v+1,
  (x:Int,y:Int)=>x+y,
  (c:Int,d:Int)=>c+d
  ).foreach(println(_))
  sparkSession.stop()

9)sortBy
就是个排序的算子

  val sourceRdd = sc.parallelize(Array((3, 1), (1, 2), (4, 3), (2, 1))).repartition(2)
//val sourceRdd = sc.parallelize(Array(2,4,1,5,7,2,5)).repartition(1)
sourceRdd.mapPartitionsWithIndex((index, iter) => {
  iter.map(x => println(index + " --- " + x)
  )
}).count()
 sourceRdd.sortBy(x => x._1).collect().foreach(println(_))
sourceRdd.sortByKey().collect().foreach(println(_))
sparkSession.stop()

Action类算子:
1)reduce
对数据进行两两规约操作

 val sourceRdd = sc.parallelize(Array(1, 1, 2, 3, 4, 5))
 println(sourceRdd.reduce(_ + _))

2)aggregate
3)collect
4)first
5)take
6)foreach
7) foreachPartitions
8) saveAsTextFile
9) countByKey
10) collectAsMap
11) lookup
12) top

你可能感兴趣的:(spark)