五-中, Spark 算子吐血总结
 
5.1.4.3 RDD 转换算子(Transformation)
 
 
 什么是算子?
 
  
  在流处理、交互式查询中有个常用的概念是“算子”,在英文中被成为“Operation”,在数学上可以解释为一个函数空间到另一个函数空间上的映射O:X->X,其实就是一个处理单元,往往是指一个函数,在使用算子时往往会有输入和输出,算子则完成相应数据的转化,比如:Group、Sort等都是算子。
 
 
 
 
 
从大方向来说, Spark算子(RDD方法)大致可以分为以下两类:
 
-  Transformation 变换/转换算子 : 这种变换并不触发提交作业, 而是完成作业中间过程处理; Transformation 操作是延迟计算的, 也就是说从一个RDD转换为另一个RDD的转换操作不是马上执行, 需要等到有Action操作(行动算子)的时候才会真正触发运算;
- Action 行动算子: 这类算子会触发SparkContext 提交Job作业, 并将数据输出到Spark系统;
从小方向来说, Spark算子大致分为三类:
 
- Value数据类型的Transformation算子, 这种变换并不触发提交作业, 针对处理的数据项是Value型的数据;
- Key-Value数据类型的Transformation算子, 这种变换并不触发提交作业, 针对处理的数据项是Key-Value型的数据对;
- Action算子, 这类算子会触发SparkContext提交Job作业;
1. Value类型
 
1.1 map
 
| 函数签名 | 函数说明 | 
|---|
| def map[U: ClassTag](f: T => U): RDD[U] | 将待处理的数据逐条进行映射转换, 这里的转换可以是类型的转换, 也可以是值的转换 | 
|  | 其实就是Scala集合函数中的map((数据变量:数据类型) => {对每一条数据的映射操作}) | 
val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD1: RDD[Int] = dataRDD.map(num => {
  num * 2
})
val dataRDD2: RDD[String] = dataRDD1.map(num => {
  "" + num
})
 
 
 map的并行计算🌰:
 // 1. rdd的计算一个分区内的数据是一个一个执行逻辑
 // 只有前面一个数据全部的逻辑执行完毕后,才会执行下一个数据。
 // 分区内数据的执行是有序的。
 // 2. 不同分区数据计算是无序的。
 
 
1.2 mapPartitions
 
| 函数签名 | 函数说明 | 
|---|
| def mapPartitions[U: ClassTag](f: Iterator[T] => Iterator[U],preservesPartitioning: Boolean = false): RDD[U] | 将待处理的数据 以分区为单位发送到计算结点进行处理, 这里的处理是指可以进行任意的处理, 哪怕是过滤数据 | 
|  | mapPartition, 传递一个迭代器, 返回一个迭代器 | 
|  | 什么, 你的不是迭代器怎么办? 用List包装, 再获取集合的迭代器即可 | 
 
 栗子: 获取每个数据分区的最大值
 
 
def main(args: Array[String]): Unit = {
    
    val conf: SparkConf = new SparkConf()
    conf.setMaster("local[*]")
    conf.setAppName("memoryRDD")
    
    val sc: SparkContext = new SparkContext(conf)
    
    val rdd= sc.makeRDD(List(1,2,3,4,5,6,7,8,9), 5)
    
    
    
    
    val mapRDD:RDD[Int] = rdd.mapPartitions(
      
      
      iter => List(iter.max).iterator
    )
      
        
        
        
    
    mapRDD.collect().foreach(println)
    mapRDD.saveAsTextFile("output")
    
    sc.stop()
}
 
 
 Q: map 和 mapPartitions的区别 ?
 
 
| 角度 | 区别 | 
|---|
| 1.数据处理角度 | map算子是 分区内一个数据一个数据的执行, 类似于串行操作; 而 mapPartitions算子是以分区为单位进行批处理操作; | 
| 2.功能角度 | map算子主要目的是将数据源中的数据进行转换和改变, 但不会减少或增多数据; 而mapPatitiions算子需要传递一个迭代器,返回一个迭代器, 没有要求?元素个数保持不变, 所以可增加或减少数据; | 
| 3. 性能的角度 | map算子类似于串行操作, 性能较低; mapPartitions算子类似于批处理, 性能较高; 但是mapPartitions算子会长时间占用内存, 那么这样会导致内存可能不够用, 出现内存溢出错误, 内存有限时, 使用map而不是mapPartitions | 
1.3 mapPartitionsWithIndex
 
| 函数签名 | 函数说明 | 
|---|
| def mapPartitionsWithIndex[U: ClassTag](f: (Index, Iterator[T]) => Iterator[U], preservesPartitioning: Boolean = false): RDD[U] | 将 待处理的数据以分区为单位发送到计算节点进行处理, 这里的处理可以使任意处理哪怕是过滤数据, 在处理时同时可以获取当前分区索引 | 
|  | 入参是(分区索引, 迭代器), 出参是迭代器 | 
 
 举个🌰:
 
 
def main(args: Array[String]): Unit = {
    
    val conf: SparkConf = new SparkConf()
    conf.setMaster("local[*]")
    conf.setAppName("memoryRDD")
    
    val sc: SparkContext = new SparkContext(conf)
    
    val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4), 2)
    val rddWithIndex: RDD[Int] = rdd.mapPartitionsWithIndex(
      (index, iterator) => {
        index match {
          case 1 => iterator
          case _ => Nil.iterator
        }
      }
    )
    
    rddWithIndex.collect().foreach(println)
    rddWithIndex.saveAsTextFile("output")
    
    sc.stop()
  }
 
 
 如何直接打印数据所在的分区?
 
 
val mpiRDD = rdd.mapPartitionsWithIndex(
            (index, iter) => {
                
                
                iter.map(
                    num => {
                        (index, num)
                    }
                )
            }
        )
 
1.4 flatMap
 
| 函数签名 | 函数说明 | 
|---|
| def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] | 将处理的 数据进行扁平化后再进行映射处理, 扁平映射算子 | 
|  | 入参是T, 出参要求是一个可迭代的集合 | 
def main(args: Array[String]): Unit = {
    
    val conf: SparkConf = new SparkConf()
    conf.setMaster("local[*]")
    conf.setAppName("memoryRDD")
    
    val sc: SparkContext = new SparkContext(conf)
    
    val rdd = sc.makeRDD(List(List(1,2),3,List(4,5)))
    val rddMap = rdd.flatMap(dat => {
      dat match {
        case i: Int => List(i)
        case j: List[_] => j
      }
    })
    
    rddMap.saveAsTextFile("output")
    rddMap.collect().foreach(println)
    
    sc.stop()
}
 
1.5 glom
 
| 函数签名 | 函数说明 | 
|---|
| def glom(): RDD[Array[T]] | 将同一个分区的数据直接转换为相同类型的内存数组进行处理, 分区不变 | 
|  | 将同一个分区里的元素合并到一个array中 | 
 
 计算所有分区最大值求和(分区内取最大值,分区间最大值求和)
 
 
def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
        val sc = new SparkContext(sparkConf)
        
        val rdd : RDD[Int] = sc.makeRDD(List(1,2,3,4), 2)
        
        
        
        val glomRDD: RDD[Array[Int]] = rdd.glom()
        val maxRDD: RDD[Int] = glomRDD.map(
            array => {
                array.max
            }
        )
        println(maxRDD.collect().sum)
        sc.stop()
    }
}
 
1.6 groupBy
 
| 函数签名 | 函数说明 | 
|---|
| def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])] | 将数据根据指定的规则进行 分组, 分区默认不变, 但是数据会被打乱重新组合, 这就是shuffle. 极限情况下, 数据可能被分在同一个分区中; | 
|  | groupBy(f:elem => 对 elem的操作) | 
 
 一个组的数据在一个分区中, 但是并不是说一个分区中只有一个组;
 
 
- List(“Hello”, “hive”, “hbase”, “Hadoop”)根据单词首写字母进行分组。
sc.groupBy(_.charAt(0))
 
- 从服务器日志数据 apache.log 中获取每个时间段访问量。
//1. 配置文件对象
    val conf = new SparkConf();
    conf.setAppName("groupby")
    conf.setMaster("local[*]")
    //2. sparkcontext
    val sc = new SparkContext(conf)
    //3. 创建RDD
    val rddString: RDD[String] = sc.textFile("datas/apache.log")
    //4.
    val hourRDD: RDD[(String, Int)] = rddString.map(
      line => {
        //取出时间
        val strArr = line.split(" ")
        val time = strArr(3)
        //对time格式化
        //指定格式化模式
        val sdfOfDate = new SimpleDateFormat("dd/MM/yyyy:HH:mm:ss")
        //格式化时间
        val date: Date = sdfOfDate.parse(time)
        //指定格式化模式
        val sdfOfHour = new SimpleDateFormat("HH")
        //格式化时间, 获取到了小时
        val hour: String = sdfOfHour.format(date)
        (hour, 1)
      }
    )
    //5. 分组
    val hourOfGroupedRDD: RDD[(String, Iterable[(String, Int)])] = hourRDD.groupBy(_._1)
    //6. 汇总计算
    val res: RDD[(String, Int)] = hourOfGroupedRDD.map {
      case (hour: String, iter: Iterable[(String, Int)]) =>
        (hour, iter.size)
    }
    res.collect().foreach(println)
 

 
- WordCount
1.7 filter
 
| 函数签名 | 函数说明 | 
|---|
| def filter(f: T => Boolean): RDD[T] | 按照规则筛选过滤; 处理后, 分区不变, 但是分区内的数据可能不均衡, 生产环境, 可能会出现数据倾斜; | 
|  | 符合规则的数据保留,不符合规则的数据丢弃 | 
 
 简单例子:
 
 
val dataRDD = sparkContext.makeRDD(List(1,2,3,4),1)
val dataRDD1 = dataRDD.filter(_%2 == 0)
 
 
 从服务器日志数据 apache.log 中获取 2015 年 5 月 17 日的请求路径
 
 
    val conf = new SparkConf();
    conf.setAppName("groupby")
    conf.setMaster("local[*]")
    
    val sc = new SparkContext(conf)
    
    val rddString: RDD[String] = sc.textFile("datas/apache.log")
    
    val timeAndPathRDD: RDD[(String, String)] = rddString.map(
      line => {
        
        val strArr = line.split(" ")
        val timeAndPathTuple: Tuple2[String, String] = (strArr(3), strArr(6))
        
        
        val sdfOfDate = new SimpleDateFormat("dd/MM/yyyy:HH:mm:ss")
        
        val date: Date = sdfOfDate.parse(timeAndPathTuple._1)
        
        val sdfOfTime = new SimpleDateFormat("dd/MM/yyyy")
        
        val time: String = sdfOfTime.format(date)
        (time, timeAndPathTuple._2)
      }
    )
    
    val res = timeAndPathRDD.filter(_._1.equals("17/05/2015"))
    res.collect().foreach(println)
 
1.8 sample
 
| 函数签名 | 函数说明 | 
|---|
| def sample(withReplacement: Boolean,fraction:Double,seed: Long = Utils.random.nextLong): RDD[T] | 根据指定的规则从数据集中抽取数据 | 
| withReplacement | 是否放回(抽奖) | 
| fraction | 概率 | 
| seed | 种子 | 

 
val dataRDD = sparkContext.makeRDD(List(1,2,3,4),1)
    
    
    
    
    
    
    val dataRDD1 = dataRDD.sample(false, 0.5)
    
    
    
    
    val dataRDD2 = dataRDD.sample(true, 2)
 
1.9 distinct
 
| 函数签名 | 函数说明 | 
|---|
| def distinct()(implicit ord: Ordering[T] = null): RDD[T] | 将数据集中重复的数据去重 | 
| def distinct(numPartitions: Int)(implicit ord:Ordering[T] = null): RDD[T] |  | 
val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2),1)
val dataRDD1 = dataRDD.distinct()
val dataRDD2 = dataRDD.distinct(2)
 

 
1.10 coalesce
 
| 函数签名 | 函数说明 | 
|---|
| def coalesce(numPartitions: Int, shuffle: Boolean = false,partitionCoalescer: Option[PartitionCoalescer] = Option.empty)(implicit ord: Ordering[T] = null): RDD[T] | 根据数据量 缩减分区,用于大数据集过滤后, 提高小数据集的执行效率 | 
|  | 当spark程序中, 存在过多的小任务的时候, 可以通过coalesce方法, 收缩合并分区, 减少分区的个数, 减少任务调度成本 | 
 
 注意: coalesce 默认不会打乱分区中的数据. 缩减分区主要是单纯的进行分区间的合并, 为了避免可能的数据倾斜, 此方法的参数 shuffle = true, 通过shuffle去平衡数据;
 
 
val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2),6)
val dataRDD1 = dataRDD.coalesce(2)
 
 
 Q: 如何扩大分区??
 
 

 
 
1.11 repartition
 
| 函数签名 | 函数说明 | 
|---|
| def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] | 该操作内部其实执行的是coalesce, 参数shuffle的默认值为true | 
|  | 无论是将分区数多的RDD 转换为分区数少的 RDD,还是将分区数少的 RDD 转换为分区数多的 RDD,repartition操作都可以完成,因为无论如何都会经 shuffle 过程。 | 
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EVOB3uZv-1646825459606)(2022-03-08-16-45-38.png)]
 
 
 coalesce 和 repartition 区别?
 
 
 
1.12 sortBy
 
| 函数签名 | 函数说明 | 
|---|
| def sortBy[K](f: (T) => K, ascending: Boolean = true,numPartitions: Int = this.partitions.length)(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T] | 在排序之前,可以将数据通过 f 函数进行处理,之后 按照 f 函数处理的结果进行排序,默认为升序排列 | 
|  | 排序后新产生的 RDD 的分区数与原 RDD 的分区数一致。中间存在 shuffle 的过程 | 

 
2. 双Value类型
 
2.13 intersection
 
| 函数签名 | 函数说明 | 
|---|
| def intersection(other: RDD[T]): RDD[T] | 对源RDD和参数RDD求 交集后返回一个新的RDD | 
|  |  | 
2.14 union
 
| 函数签名 | 函数说明 | 
|---|
| def union(other: RDD[T]): RDD[T] | 对源RDD和参数RDD求 并集后返回一个新的RDD | 
2.15 subtract
 
| 函数签名 | 函数说明 | 
|---|
| def subtract(other: RDD[T]): RDD[T] | 以一个 RDD 元素为主,去除两个 RDD 中重复元素,将其他元素保留下来 | 
|  |  | 
2.16 zip
 
| 函数签名 | 函数说明 | 
|---|
| def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)] | 将两个 RDD 中的元素,以键值对(拉链)的形式进行合并. | 
|  | 键值对中的 Key 为第 1 个 RDD中的元素,Value 为第 2 个 RDD 中的相同位置的元素。 | 
 def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)
    
    
    
    val rdd1 = sc.makeRDD(List(1,2,3,4))
    val rdd2 = sc.makeRDD(List(3,4,5,6))
    val rdd7 = sc.makeRDD(List("3","4","5","6"))
    
    val rdd3: RDD[Int] = rdd1.intersection(rdd2)
    
    println(rdd3.collect().mkString(","))
    
    val rdd4: RDD[Int] = rdd1.union(rdd2)
    println(rdd4.collect().mkString(","))
    
    val rdd5: RDD[Int] = rdd1.subtract(rdd2)
    println(rdd5.collect().mkString(","))
    
    val rdd6: RDD[(Int, Int)] = rdd1.zip(rdd2)
    val rdd8 = rdd1.zip(rdd7)
    println(rdd6.collect().mkString(","))
    sc.stop()
  }
 
思考一个问题:如果两个 RDD 数据类型不一致怎么办?
 思考一个问题:如果两个 RDD 数据分区不一致怎么办?
 思考一个问题:如果两个 RDD 分区数据数量不一致怎么办?
 
 
 - 交集,并集和差集要求两个数据源数据类型保持一致
- 拉链操作两个数据源的类型可以不一致
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eyLK105S-1646825459608)(2022-03-08-17-00-31.png)]
 
3. Key-Value 类型
 
3.17 partitionBy
 
| 函数签名 | 函数说明 | 
|---|
| def partitionBy(partitioner: Partitioner): RDD[(K, V)] | 将数据按照Partitioner重新进行分区; Spark 默认的分区器是 HashPartitioner | 
|  |  | 

 
 
3.18 reduceByKey
 
| 函数签名 | 函数说明 | 
|---|
| def reduceByKey(func: (V, V) => V): RDD[(K, V)] | 可以将数据按照相同的Key对Value进行聚合 | 
| def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)] |  | 
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 = dataRDD1.reduceByKey(_+_)
val dataRDD3 = dataRDD1.reduceByKey(_+_, 2)
 
 
 Q: redeceByKey 和 groupBykey的区别?
 
  
  从shuffle 的角度:reduceByKey 和 groupByKey 都存在 shuffle 的操作,但是 reduceByKey可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而 groupByKey 只是进行分组,不存在数据量减少的问题,reduceByKey 性能比较高。
 从功能的角度:reduceByKey 其实包含分组和聚合的功能。GroupByKey 只能分组,不能聚合,所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用 groupByKey
 
 
 
 
3.19 groupByKey
 
| 函数签名 | 函数说明 | 
|---|
| def groupByKey(): RDD[(K, Iterable[V])] | 将数据源的数据根据key对value进行分组 | 
| def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])] |  | 
| def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])] |  | 
|  |  | 
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 = dataRDD1.groupByKey()
val dataRDD3 = dataRDD1.groupByKey(2)
val dataRDD4 = dataRDD1.groupByKey(new HashPartitioner(2))
 
3.20 aggregateByKey
 
| 函数签名 | 函数说明 | 
|---|
| def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)] | 将数据根据不同的规则进行 分区内计算和分区间计算 | 
|  |  | 
思考一个问题:分区内计算规则和分区间计算规则相同怎么办?
 
3.21 foldByKey
 
| 函数签名 | 函数说明 | 
|---|
| def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)] | 当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为 foldByKey | 
3.22 combineByKey
 
| 函数签名 | 函数说明 | 
|---|
| def combineByKey[C](createCombiner: V => C,mergeValue: (C, V) => C,mergeCombiners: (C, C) => C): RDD[(K, C)] | 最通用的对 key-value 型 rdd 进行聚集操作的聚集函数(aggregation function) | 
|  | 类似于arrregate(), combineByKey() 允许用户返回值的类型与输入不一致 | 
3.23 sortByKey
 
| 函数签名 | 函数说明 | 
|---|
| def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length): RDD[(K, V)] | 在一个(K,V)的 RDD 上调用,K 必须实现 Ordered 接口(特质),返回一个按照 key 进行排序 | 
| 的 |  | 
3.24 join
 
| 函数签名 | 函数说明 | 
|---|
| def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))] | 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素连接在一起的 | 
| (K,(V,W))的 RDD |  | 
3.25 leftOuterJoin
 
| 函数签名 | 函数说明 | 
|---|
| def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))] | 类似于 SQL 语句的左外连接 | 
3.26 cogroup
 
| 函数签名 | 函数说明 | 
|---|
| def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))] | 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD | 
5.1.4.4 RDD 行动算子 (Action)
 
1. reduce
 
| 函数签名 | 函数说明 | 
|---|
| def reduce(f: (T, T) => T): T | 聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据 | 
2. collect
 
| 函数签名 | 函数说明 | 
|---|
| def collect(): Array[T] | 在驱动程序中,以数组 Array 的形式返回数据集的所有元素 | 
3. count
 
| 函数签名 | 函数说明 | 
|---|
| def count(): Long | 返回 RDD 中元素的个数 | 
4. first
 
| 函数签名 | 函数说明 | 
|---|
| def first(): T | 返回 RDD 中的第一个元素 | 
|  |  | 
5. take
 
| 函数签名 | 函数说明 | 
|---|
| def take(num: Int): Array[T] | 返回一个由 RDD 的前 n 个元素组成的数组 | 
|  |  | 
6. takeOrdered
 
| 函数签名 | 函数说明 | 
|---|
| def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] | 返回该 RDD 排序后的前 n 个元素组成的数组 | 
|  |  | 
7. aggregate
 
| 函数签名 | 函数说明 | 
|---|
| def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U | 分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合 | 
8. fold
 
| 函数签名 | 函数说明 | 
|---|
| def fold(zeroValue: T)(op: (T, T) => T): T | 统计每种 key 的个数 | 
9. countByKey
 
| 函数签名 | 函数说明 | 
|---|
| def countByKey(): Map[K, Long] | 折叠操作,aggregate 的简化版操作 | 
10. save相关的算子
 
| 函数签名 | 函数说明 | 
|---|
| def saveAsTextFile(path: String): Unit |  | 
| def saveAsSequenceFile(path: String, codec: Option[Class[_ <: CompressionCodec]] = None): Unit | 将数据保存到不同格式的文件中 | 
| def saveAsObjectFile(path: String): Unit |  | 
| def saveAsSequenceFile(path: String,codec: Option[Class[_ <: CompressionCodec]] = None): Unit |  | 
11. foreach