Spark的算子的分类

从大方向来说,Spark 算子大致可以分为以下两类:

1)Transformation 变换/转换算子:这种变换并不触发提交作业,完成作业中间过程处理。

Transformation 操作是延迟计算的,也就是说从一个RDD 转换生成另一个 RDD 的转换操作不是马上执行,需要等到有 Action 操作的时候才会真正触发运算。

2)Action 行动算子:这类算子会触发 SparkContext 提交 Job 作业。

Action 算子会触发 Spark 提交作业(Job),并将数据输出 Spark系统。

从小方向来说,Spark 算子大致可以分为以下三类:

1)Value数据类型的Transformation算子,这种变换并不触发提交作业,针对处理的数据项是Value型的数据。
  2)Key-Value数据类型的Transfromation算子,这种变换并不触发提交作业,针对处理的数据项是Key-Value型的数据对。

3)Action算子,这类算子会触发SparkContext提交Job作业。

spark作业按照算子表示的执行过程图:

spark行动算子和转换算子 spark中action算子_spark行动算子和转换算子


常见的transformation算子:

1.map 原rdd的形态,转化为另外一种形态

/**
      * 1、map:将集合中每个元素乘以7
      *   map是最常用的转换算子之一,将原rdd的形态,转化为另外一种形态,
      *   需要注意的是这种转换是one-to-one
      * @param sc
      */
    def transformationMap_01(list:List[Int], sc:SparkContext): Unit = {
        val bs = 7
        val listRDD = sc.parallelize(list)
        val retRDD = listRDD.map(num => num * bs)
        retRDD.foreach(println)
    }

2.flatMap:将行拆分为单词

/**
  * 3、flatMap:将行拆分为单词
  *     和map算子类似,只不过呢,rdd形态转化对应为one-to-many
  * @param sc
  */
def transformationFlatMap_02(sc:SparkContext): Unit = {
    val list = List(
        "lu jia hui",
        "chen zhi xiang"
    )
    val listRDD = sc.parallelize(list)
    listRDD.flatMap(line => line.split("\\s+")).foreach(println)
}

3.filter 条件过滤算子

/**
      * filter:过滤出集合中的奇数(even)
      */
    def transformationFilter_03(list:List[Int], sc:SparkContext): Unit = {
        val listRDD = sc.parallelize(list    )
        val filterRDD = listRDD.filter(num => {
//            1 / 0
            num % 2 == 0
        })
        filterRDD.foreach(println)
    }

4.sample:根据给定的随机种子seed,随机抽样出数量为frac的数据

/**
      * sample:根据给定的随机种子seed,随机抽样出数量为frac的数据
      * 参数:
      *     withReplacement:true或者false
      *         true:代表有放回的抽样
      *         false:代表无放回的抽样
      *     fraction:抽取样本空间占总体的比例(分数的形式传入)
      *         without replacement: 0 <= fraction <= 1
      *         with replacement: fraction >= 0
      *     seed:随机数生成器
      *     new Random().nextInt(10)
      *     注意:我们使用sample算子不能保证提供集合大小就恰巧是rdd.size * fraction,
      * 结果大小会在前面数字上下浮动
      *   sample算子,在我们后面学习spark调优(dataskew)的时候,可以用的到
      */
    def transformationSample_04(sc:SparkContext): Unit = {
        val list = 0 to 99999
        val listRDD = sc.parallelize(list)
        val sampleRDD = listRDD.sample(false, 0.05)
        println(s"sampleRDD的集合大小:${sampleRDD.count()}")
    }

5.union:返回一个新的数据集,由原数据集和参数联合而成

/**
      * union:返回一个新的数据集,由原数据集和参数联合而成
      *   该union操作和sql中的union all操作一模一样
      */
    def  transformationUnion_05(sc:SparkContext): Unit = {
        val list1 = List(1, 2, 3, 4, 5)
        val list2 = List(6, 5, 4, 7, 9)
        val lRDD1 = sc.parallelize(list1)
        val lRDD2 = sc.parallelize(list2)

        lRDD1.union(lRDD2).foreach(t => println(t + " "))
    }

6.groupByKey:对数组进行 group by key操作 慎用

/**
      * groupByKey:对数组进行 group by key操作 慎用
      * 一张表,student表
      *     stu_id  stu_name   class_id
      * 将学生按照班级进行分组,把每一个班级的学生整合到一起
      * 建议groupBykey在实践开发过程中,能不用就不用,主要是因为groupByKey的效率低,
      * 因为有大量的数据在网络中传输,而且还没有进行本地的预处理
      * 我可以使用reduceByKey或者aggregateByKey或者combineByKey去代替这个groupByKey
      */
    def  transformationGBK_06(sc:SparkContext): Unit = {
        val list = List(
            "1  郑祥楷 1807bd-bj",
            "2  王佳豪 1807bd-bj",
            "3  刘鹰 1807bd-sz",
            "4  宋志华 1807bd-wh",
            "5  刘帆 1807bd-xa",
            "6  何昱 1807bd-xa"
        )
        val listRDD = sc.parallelize(list)
        println("分区个数:" + listRDD.getNumPartitions)
        val cid2StuInfo = listRDD.map { case (line) => {
            val fields = line.split("\\s+")

            (fields(2), line)
        }}
        val gbkRDD = cid2StuInfo.groupByKey()
        gbkRDD.foreach{case (cid, stus) => {
            println(s"$cid ---> ${stus} ----> ${stus.size}")
        }}
    }

7.reduceByKey 聚合算子会进行本地预聚合

/**
          *
          * 一张表,student表
          *     stu_id  stu_name   class_id
          *  统计每个班级的人数
          *  相同的统计下,reduceByKey要比groupByKey效率高,因为在map操作完毕之后发到reducer之前
          *  需要先进行一次本地的预聚合,每一个mapper(对应的partition)执行一次预聚合
          * @param sc
          */
        def  transformationRBK_07(sc:SparkContext): Unit = {
            val list = List(
                "1  郑祥楷 1807bd-bj",
                "2  王佳豪 1807bd-bj",
                "3  刘鹰 1807bd-sz",
                "4  宋志华 1807bd-wh",
                "5  刘帆 1807bd-xa",
                "6  何昱 1807bd-xa"
            )
            val listRDD = sc.parallelize(list)
            println("分区个数:" + listRDD.getNumPartitions)
            val cid2Count = listRDD.map { case (line) => {
                val fields = line.split("\\s+")
                (fields(2), 1)
            }}
            cid2Count.reduceByKey(_+_).foreach(println)
        }
8.join: 打印关联的组合信息
  /**
      * join: 打印关联的组合信息
      *    关联操作中rdd的类型必须(K, V)
      *    inner join:等值连接  返回左右两张表中能对应上的数据
      *    left/right outer join:左(右)外连接--->
      *         返回左(右)表所有数据,右(左)表能对应上的话显示,对应不上的话显示为null
      *    left/right full outer join
      *         返回左右两张表中都有的数据:左外连接+右外连接
      * 一张学生信息表stu,一张班级信息表class
      *     stu--->  stu_id  stu_name    cid
      *     class--> cid    cname
      * 现在要求查询以下信息:
      *     stu_id stu_name cname
      *  用SQL:
      *     select
      *         s.stu_id,
      *         s.stu_name,
      *         c.cname
      *     from stu s
      *     left join class c on s.cid = c.cid
      * @param sc
      */
    def  transformationJOIN_08(sc:SparkContext): Unit = {
        val stu = List(
            "1  郑祥楷 1",
            "2  王佳豪 1",
            "3  刘鹰 2",
            "4  宋志华 3",
            "5  刘帆 4",
            "6  OLDLi 5"
        )
        val cls = List(
            "1 1807bd-bj",
            "2 1807bd-sz",
            "3 1807bd-wh",
            "4 1807bd-xa",
            "7 1805bd-bj"
        )
        val stuRDD = sc.parallelize(stu)
        val clsRDD = sc.parallelize(cls)
        val cid2STURDD:RDD[(String, String)] = stuRDD.map{case line => {
            val fields = line.split("\\s+")
            (fields(2), line)
        }}
        val cid2ClassRDD:RDD[(String, String)] = clsRDD.map{case line => {
            val fields = line.split("\\s+")
            (fields(0), fields(1))
        }}
        //两张表关联--join
        println("---------inner join-------------")
        val cid2InfoRDD:RDD[(String, (String, String))] = cid2STURDD.join(cid2ClassRDD)
        cid2InfoRDD.foreach(println)
        println("---------left join-------------")
        val cid2LefJoinRDD:RDD[(String, (String, Option[String]))] = cid2STURDD.leftOuterJoin(cid2ClassRDD)
        cid2LefJoinRDD.foreach(println)
        println("---------full join-------------")
        val cid2FullJoinRDD:RDD[(String, (Option[String], Option[String]))] = cid2STURDD.fullOuterJoin(cid2ClassRDD)
        cid2FullJoinRDD.foreach(println)
    }

9.sortByKey:将学生成绩进行排序

/**
      * sortByKey:将学生成绩进行排序
      *     分区内部有序
      * 学生表stu:id   name    chinese-score
      *
      */
    def transformationsbk_09(sc:SparkContext): Unit = {
        val stuList = List(
            "1  王浩玉 93.5",
            "2  贾小红 56.5",
            "3  薛亚曦 60.5",
            "4  宋其 125"
        )
        val stuRDD = sc.parallelize(stuList)
        println("------------------sortByKey----------------------")
        val score2InfoRDD:RDD[(Double, String)] = stuRDD.map { case (line) => {
            val fields = line.split("\\s+")
            (fields(2).toDouble, line)
        }}
        score2InfoRDD.sortByKey(false, numPartitions = 1).foreach(println)
        println("------------------sortBy----------------------")
        val sbkRDD2:RDD[String] = stuRDD.sortBy(line => line, false, 1)(
            new Ordering[String](){//按照成绩排序
                override def compare(x: String, y: String) = {
                    val xScore = x.split("\\s+")(2).toDouble
                    val yScore = y.split("\\s+")(2).toDouble
                    yScore.compareTo(xScore)
                }
            },
            ClassTag.Object.asInstanceOf[ClassTag[String]]
        )
        sbkRDD2.foreach(println)
    }

10.combineByKey

/**
      * 使用combineByKey和aggregateByKey来模拟groupByKey和reduceByKey
      * 不管combineByKey还是aggregateByKey底层都是使用combineByKeyWithClassTag来实现的
      *
      * 这两个有啥区别?
      *
      *
      *
      *
      */
    object _02SparkTransformationOps {
        def main(args: Array[String]): Unit = {
            Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
            Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
            Logger.getLogger("org.project-spark").setLevel(Level.WARN)
            val conf = new SparkConf()
                .setMaster("local[2]")
                .setAppName(s"${_02SparkTransformationOps.getClass.getSimpleName}")
            val sc = new SparkContext(conf)
    
            val list = List(
                "5  刘帆 1807bd-xa",
                "2  王佳豪 1807bd-bj",
                "8  邢宏 1807bd-xa",
                "3  刘鹰 1807bd-sz",
                "4  宋志华 1807bd-wh",
                "1  郑祥楷 1807bd-bj",
                "7  张雨 1807bd-bj",
                "6  何昱 1807bd-xa"
            )
    //        cbk2rbk(sc, list)
            cbk2gbk(sc, list)
    
            sc.stop()
        }
    
        def cbk2gbk(sc:SparkContext, list:List[String]): Unit = {
            val listRDD = sc.parallelize(list)
            println("分区个数:" + listRDD.getNumPartitions)
            val cid2Info:RDD[(String, String)] = listRDD.map { case (line) => {
                val fields = line.split("\\s+")
                (fields(2), line)
            }}
    
            val cbk2gbk:RDD[(String, ArrayBuffer[String])] = cid2Info.combineByKey(
                line => ArrayBuffer[String](line),
                (ab:ArrayBuffer[String], line:String) => {
                    ab.append(line)
                    ab
                },
                (ab1:ArrayBuffer[String], ab2:ArrayBuffer[String]) => {
                    ab1.appendAll(ab2)
                    ab1
                }
            )
            cbk2gbk.foreach(println)
        }
        /**
          * 使用combineByKey来模拟reduceByKey
          * @param sc
          * @param list
          */
        def cbk2rbk(sc:SparkContext, list:List[String]): Unit = {
            val listRDD = sc.parallelize(list)
            println("分区个数:" + listRDD.getNumPartitions)
            val cid2Info:RDD[(String, String)] = listRDD.map { case (line) => {
                val fields = line.split("\\s+")
                (fields(2), line)
            }}
            cid2Info.foreachPartition(patition => {
                patition.foreach(println)
            })
            val cid2Counts = cid2Info.combineByKey(createCombiner, mergeValue, mergeCombiners)
            cid2Counts.foreach(println)
        }
    
        /**
          * V是被聚合的rdd中k-v键值对中的value的类型
          * C是经过聚合操作之后又v转化成的类型
          * 当前方法,rdd中一个key,在一个分区中,只会创建/调用一次,做数据类型初始化
          * 比如:hello这个key,在partition0和partition1中都有出现
          * 在partition0中聚合的时候createCombiner之被调用一次
          *
          * @return
          */
        def createCombiner(line:String):Int = {
            val fields = line.split("\\s+")
            println("-----createCombiner---> " + fields(2))
            1
        }
    
        /**
          * scala代码,求1+。。。+10
          * var sum = 0
          * for(i <- 1 to 10) {
          *     sum = sum + i
          * }
          * println(sum)
          * mergeValue就类似于上面的代码
          * 合并同一个分区中,相同key对应的value数据,在一个分区中,相同的key会被调用多次
          * @return
          */
        def mergeValue(sum:Int, line:String):Int = {
            val fields = line.split("\\s+")
            println(">>>-----mergeValue---> " + fields(2))
            sum + 1
        }
    
        /**
          * 相同key对应分区间的数据进行合并
          * @return
          */
        def mergeCombiners(sum1:Int, sum2:Int):Int = {
            println(">>>-----mergeCombiners--->>> sum1: " + sum1 + "--->sum2: " + sum2)
            sum1 + sum2
        }
    }


11.aggregateByKey
/**
  * 使用combineByKey和aggregateByKey来模拟groupByKey和reduceByKey
  * 不管combineByKey还是aggregateByKey底层都是使用combineByKeyWithClassTag来实现的
  *
  * 这两个有啥区别?
  *
  * 1、本质上combineByKey和aggregateByKey都是通过combineByKeyWithClassTag来实现的,只不过实现的细节或者方式不大一样。
  * 2、combineByKey更适合做聚合前后数据类型不一样的操作,aggregateByKey更适合做聚合前后数据类型一致的操作
  *  因为我们可以在combineByKey提供的第一个函数中完成比较复杂的初始化操作,而aggregateByKey的第一个参数是一个值
  * 3、我们使用时最简单的版本,而在实际生产过程中,一般都是相对比较复杂的版本,还有其它参数的,比如partitioner,
  * mapSideCombine。
  *     partitioner制定并行度,
  *     mapSideCombine控制是否执行本地预聚合
  *
  */
object _03SparkTransformationOps {
    def main(args: Array[String]): Unit = {
        Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
        Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
        Logger.getLogger("org.project-spark").setLevel(Level.WARN)
        val conf = new SparkConf()
            .setMaster("local[2]")
            .setAppName(s"${_03SparkTransformationOps.getClass.getSimpleName}")
        val sc = new SparkContext(conf)

        val list = List(
            "5  刘帆 1807bd-xa",
            "2  王佳豪 1807bd-bj",
            "8  邢宏 1807bd-xa",
            "3  刘鹰 1807bd-sz",
            "4  宋志华 1807bd-wh",
            "1  郑祥楷 1807bd-bj",
            "7  张雨 1807bd-bj",
            "6  何昱 1807bd-xa"
        )
//        abk2rbk(sc, list)
        abk2gbk(sc, list)

        sc.stop()
    }

    def abk2gbk(sc:SparkContext, list:List[String]): Unit = {
        val listRDD = sc.parallelize(list)
        println("分区个数:" + listRDD.getNumPartitions)
        val cid2Info:RDD[(String, String)] = listRDD.map { case (line) => {
            val fields = line.split("\\s+")
            (fields(2), line)
        }}

        val abk2gbk:RDD[(String, ArrayBuffer[String])] = cid2Info.aggregateByKey(ArrayBuffer[String]())(
            (ab:ArrayBuffer[String], line:String) => {
                ab.append(line)
                ab
            },
            (ab1:ArrayBuffer[String], ab2:ArrayBuffer[String]) => {
                ab1.appendAll(ab2)
                ab1
            }
        )
//        abk2gbk.foreach(println)
    }

    /**
      * 使用aggregateByKey来模拟reduceByKey
      * @param sc
      * @param list
      */
    def abk2rbk(sc:SparkContext, list:List[String]): Unit = {
        val listRDD = sc.parallelize(list)
        println("分区个数:" + listRDD.getNumPartitions)
        val cid2Info:RDD[(String, String)] = listRDD.map { case (line) => {
            val fields = line.split("\\s+")
            (fields(2), line)
        }}

        val cid2rbkRDD:RDD[(String, Int)] = cid2Info.aggregateByKey(0)(
            (sum:Int, line:String) => {
                sum + 1
            },
            (sum1:Int, sum2:Int) => {
                sum1 + sum2
            }
        )
        cid2rbkRDD.foreach(println)
    }

}

12 action 类算子:

/*
    1、reduce:
        执行reduce操作,返回值是一个标量
    2、collect: 慎用
        将数据从集群中的worker上拉取到dirver中,所以在使用的过程中药慎用,意外拉取数据过大造成driver内存溢出OOM(OutOfMemory)
        NPE(NullPointerException)
        所以在使用的使用,尽量使用take,或者进行filter再拉取
    3、count:返回当前rdd中有多少条记录
        select count(*) from tbl;
    4、take:take(n)
        获取rdd中前n条记录
        如果rdd数据有序,可以通过take(n)求TopN
    5、first:take(1)
        获取rdd中的第一条记录
    6、saveAsTextFile:
        将rdd中的数据保存到文件系统中
    7、countByKey:和reduceByKey效果相同,但reduceByKey是一个Transformation
        统计相同可以出现的次数,返回值为Map[String, Long]
    8、foreach:略 遍历
 */
object _04SparkActionOps extends App {
//    Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
//    Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
//    Logger.getLogger("org.project-spark").setLevel(Level.WARN)
    val conf = new SparkConf()
        .setMaster("local[2]")
        .setAppName(s"${_03SparkTransformationOps.getClass.getSimpleName}")
    val sc = new SparkContext(conf)

    val list = List(
        "5  刘帆 1807bd-xa",
        "2  王佳豪 1807bd-bj",
        "8  邢宏 1807bd-xa",
        "3  刘鹰 1807bd-sz",
        "4  宋志华 1807bd-wh",
        "1  郑祥楷 1807bd-bj",
        "7  张雨 1807bd-bj",
        "6  何昱 1807bd-xa"
    )

    val listRDD: RDD[String] = sc.parallelize(list)
    val list1 = 1 to 9
    val listRDD1 = sc.parallelize(list1)
    //reduce:统计rdd中的和
    val 和 = listRDD1.reduce((v1, v2) => v1 + v2)
    println(和)
    //2 count
    println(s"listRDD1的条数:${listRDD1.count()}")
    // 3 take 获取rdd中的n条记录
    val take: Array[Int] = listRDD1.take(3)
    println(take.mkString("[", ",", "]"))
    private val first: Int = listRDD1.first()
    println(first)
    val top3:Array[Int] = listRDD1.takeOrdered(3)(new Ordering[Int]{
        override def compare(x: Int, y: Int) = {
            y.compareTo(x)
        }
    })
    println(top3.mkString("[", ",", "]"))
    println("---------------saveAsTextFile---------------------")
//    listRDD.saveAsTextFile("E:/data/spark/out")
    listRDD.saveAsObjectFile("E:/data/spark/out1")
    println("---------------saveAsHadoop/SequenceFile---------------------")
    val cid2InfoRDD:RDD[(String, String)] = listRDD.map{case line => {
        val fields = line.split("\\s+")
        (fields(2), line)
    }}

    /**
      * saveAsHadoopFile()  ---> org.apache.hadoop.mapred.OutputFormat 借口
      * saveAsNewAPIHadoopFile() --> org.apache.hadoop.mapreduce.OutputFormat 抽象类
      * job.setOutputFormat(xxx.classs)
      *
      *
      *  path: String   --->将rdd数据存储的目的地址
         keyClass: Class[_], mr中输出记录包含key和value,需要指定对应的class
         valueClass: Class[_],
         job.setOutputKeyClass()
         job.setOutputValueClass()
         outputFormatClass: Class[_ <: NewOutputFormat[_, _]] 指定对应的Format来完成数据格式化输出
         TextOutputFormat
      */
    cid2InfoRDD.saveAsNewAPIHadoopFile(
        "E:/data/spark/out2",
        classOf[Text],
        classOf[Text],
        classOf[TextOutputFormat[Text, Text]]
    )

    //countByKey作用就是记录每一个key出现的次数,作用同reduceByKey(_+_)
    private val cid2Count: collection.Map[String, Long] = cid2InfoRDD.countByKey()

    for((k, v) <- cid2Count) {
        println(k + "---" + v)
    }

    sc.stop()
}