大数据Spark学习之旅第三篇

「这是我参与11月更文挑战的第38天,活动详情查看:2021最后一次更文挑战」。

一、RDD 转换算子

RDD 根据数据处理方式的不同将算子整体上分为 Value 类型、双 Value 类型和 Key-Value类型

1、Value 类型

1.1、map

  1. 函数签名

    def map[U: ClassTag](f: T => U): RDD[U]
    复制代码
  2. 函数说明

    将处理的数据逐条进行映射转换,这里的转换可以是类型的转换,也可以是值的转换。

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - map
    
      val rdd = sc.makeRDD(List(1, 2, 3, 4))
      // 1,2,3,4
      // 2,4,6,8
    
      // 转换函数
      def mapFunction(num: Int): Int = {
        num * 2
      }
    
      //val mapRDD: RDD[Int] = rdd.map(mapFunction)
      //val mapRDD: RDD[Int] = rdd.map((num:Int)=>{num*2})
      //val mapRDD: RDD[Int] = rdd.map((num:Int)=>num*2)
      //val mapRDD: RDD[Int] = rdd.map((num)=>num*2)
      //val mapRDD: RDD[Int] = rdd.map(num=>num*2)
      val mapRDD: RDD[Int] = rdd.map(_ * 2)
    
      mapRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码
    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - map 分区
    
      // 1. rdd的计算一个分区内的数据是一个一个执行逻辑
      //    只有前面一个数据全部的逻辑执行完毕后,才会执行下一个数据。
      //    分区内数据的执行是有序的。
      // 2. 不同分区数据计算是无序的。
      val rdd = sc.makeRDD(List(1, 2, 3, 4), 2)
    
      val mapRDD = rdd.map(
        num => {
          println(">>>>>>>> " + num)
          num
        }
      )
      val mapRDD1 = mapRDD.map(
        num => {
          println("######" + num)
          num
        }
      )
    
      mapRDD1.collect()
    
      sc.stop()
    
    }
    复制代码
    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - map
      val rdd = sc.makeRDD(List(1, 2, 3, 4), 2)
      // 【1,2】,【3,4】
      rdd.saveAsTextFile("output")
      val mapRDD = rdd.map(_ * 2)
      // 【2,4】,【6,8】
      mapRDD.saveAsTextFile("output1")
    
      sc.stop()
    
    }
    复制代码
  3. 小功能:从服务器日志数据 apache.log 中获取用户请求 URL 资源路径

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - map
    
      val rdd = sc.textFile("datas/apache.log")
    
      // 长的字符串
      // 短的字符串
      val mapRDD: RDD[String] = rdd.map(
        line => {
          val datas = line.split(" ")
          datas(6)
        }
      )
      mapRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码

1.2、mapPartitions

  1. 函数签名

    def mapPartitions[U: ClassTag](
        f: Iterator[T] => Iterator[U],
        preservesPartitioning: Boolean = false): RDD[U]
    复制代码
  2. 函数说明

    将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据。

      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - mapPartitions
      val rdd = sc.makeRDD(List(1, 2, 3, 4), 2)
    
      // mapPartitions : 可以以分区为单位进行数据转换操作
      //                 但是会将整个分区的数据加载到内存进行引用
      //                 如果处理完的数据是不会被释放掉,存在对象的引用。
      //                 在内存较小,数据量较大的场合下,容易出现内存溢出。
      val mpRDD: RDD[Int] = rdd.mapPartitions(
        iter => {
          println(">>>>>>>>>>")
          iter.map(_ * 2)
        }
      )
      mpRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码
  3. 小功能:获取每个数据分区的最大值

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - mapPartitions
      val rdd = sc.makeRDD(List(1, 2, 3, 4), 2)
    
      // 【1,2】,【3,4】
      // 【2】,【4】
      val mpRDD = rdd.mapPartitions(
        iter => {
          List(iter.max).iterator
        }
      )
      mpRDD.collect().foreach(println)
      sc.stop()
    
    }
    复制代码

思考一个问题:map 和 mapPartitions 的区别?

  • 数据处理角度

    Map 算子是分区内一个数据一个数据的执行,类似于串行操作。而 mapPartitions 算子是以分区为单位进行批处理操作。

  • 功能的角度

    Map 算子主要目的将数据源中的数据进行转换和改变。但是不会减少或增多数据。MapPartitions 算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变,所以可以增加或减少数据

  • 性能的角度

    Map 算子因为类似于串行操作,所以性能比较低,而是 mapPartitions 算子类似于批处理,所以性能较高。但是 mapPartitions 算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用。使用 map 操作。

1.3、mapPartitionsWithIndex

  1. 函数签名

    def mapPartitionsWithIndex[U: ClassTag](
        f: (Int, Iterator[T]) => Iterator[U],
        preservesPartitioning: Boolean = false): RDD[U]
    复制代码
  2. 函数说明

    将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据,在处理时同时可以获取当前分区索引。

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - mapPartitionsWithIndex
      val rdd = sc.makeRDD(List(1, 2, 3, 4), 2)
    
      val mpiRDD = rdd.mapPartitionsWithIndex(
        (index, iter) => {
          // 1,   2,    3,   4
          iter.map(
            num => {
              (index, num)
            }
          )
        }
      )
    
      mpiRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码
  3. 小功能:获取第二个数据分区的数据

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - mapPartitionsWithIndex
      val rdd = sc.makeRDD(List(1, 2, 3, 4), 2)
      // 【1,2】,【3,4】
      val mpiRDD = rdd.mapPartitionsWithIndex(
        (index, iter) => {
          if (index == 1) {
            iter
          } else {
            Nil.iterator
          }
        }
      )
    
      mpiRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码

1.4、flatMap

  1. 函数签名

    def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]
    复制代码
  2. 函数说明

    将处理的数据进行扁平化后再进行映射处理,所以算子也称之为扁平映射

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - flatMap
    
      val rdd: RDD[String] = sc.makeRDD(List(
        "Hello Scala", "Hello Spark"
      ))
    
      val flatRDD: RDD[String] = rdd.flatMap(
        s => {
          s.split(" ")
        }
      )
    
      flatRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码
  3. 小功能:将 List(List(1,2),3,List(4,5))进行扁平化操作

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - flatMap
    
      val rdd = sc.makeRDD(List(List(1, 2), 3, List(4, 5)))
    
      //    val flatRDD = rdd.flatMap(
      //      data => {
      //        data match {
      //          case list: List[_] => list
      //          case dat => List(dat)
      //        }
      //      }
      //    )
    
      val flatRDD = rdd.flatMap {
        case list: List[_] => list
        case dat => List(dat)
      }
    
      flatRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码

1.5、glom

  1. 函数签名

    def glom(): RDD[Array[T]]
    复制代码
  2. 函数说明

    将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - glom
    
      val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
    
      // List => Int
      // Int => Array
      val glomRDD: RDD[Array[Int]] = rdd.glom()
    
      glomRDD.collect().foreach(data => println(data.mkString(",")))
    
      sc.stop()
    
    }
    复制代码
  3. 小功能:计算所有分区最大值求和(分区内取最大值,分区间最大值求和)

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - glom
    
      val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
    
      // 【1,2】,【3,4】
      // 【2】,【4】
      // 【6】
      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

  1. 函数签名

    def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
    复制代码
  2. 函数说明

将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合,我们将这样的操作称之为 shuffle。极限情况下,数据可能被分在同一个分区中,一个组的数据在一个分区中,但是并不是说一个分区中只有一个组。

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

  val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
  val sc = new SparkContext(sparkConf)

  // 算子 - groupBy

  val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)

  // groupBy会将数据源中的每一个数据进行分组判断,根据返回的分组key进行分组
  // 相同的key值的数据会放置在一个组中
  def groupFunction(num: Int) = {
    num % 2
  }

  val groupRDD: RDD[(Int, Iterable[Int])] = rdd.groupBy(groupFunction)

  groupRDD.collect().foreach(println)

  sc.stop()

}
```
复制代码
  1. 小功能:将 List("Hello", "hive", "hbase", "Hadoop")根据单词首写字母进行分组。

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - groupBy
    
      val rdd = sc.makeRDD(List("Hello", "Spark", "Scala", "Hadoop"), 2)
    
      // 分组和分区没有必然的关系
      val groupRDD = rdd.groupBy(_.charAt(0))
    
      groupRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码
  2. 小功能:从服务器日志数据 apache.log 中获取每个时间段访问量。

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - groupBy
      val rdd = sc.textFile("datas/apache.log")
    
      val timeRDD: RDD[(String, Iterable[(String, Int)])] = rdd.map(
        line => {
          val datas = line.split(" ")
          val time = datas(3)
          //time.substring(0, )
          val sdf = new SimpleDateFormat("dd/MM/yyyy:HH:mm:ss")
          val date: Date = sdf.parse(time)
          val sdf1 = new SimpleDateFormat("HH")
          val hour: String = sdf1.format(date)
          (hour, 1)
        }
      ).groupBy(_._1)
    
      timeRDD.map {
        case (hour, iter) => {
          (hour, iter.size)
        }
      }.collect.foreach(println)
    
      sc.stop()
    
    }
    复制代码

1.7、filter

  1. 函数签名

    def filter(f: T => Boolean): RDD[T]
    复制代码
  2. 函数说明

    将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃。当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜。

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - filter
      val rdd = sc.makeRDD(List(1, 2, 3, 4))
    
      val filterRDD: RDD[Int] = rdd.filter(num => num % 2 != 0)
    
      filterRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码
  3. 小功能:从服务器日志数据 apache.log 中获取 2015 年 5 月 17 日的请求路径

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - filter
      val rdd = sc.textFile("datas/apache.log")
    
      rdd.filter(
        line => {
          val datas = line.split(" ")
          val time = datas(3)
          time.startsWith("17/05/2015")
        }
      ).collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码

1.8、sample

  1. 函数签名

    def sample(
        withReplacement: Boolean,
        fraction: Double,
        seed: Long = Utils.random.nextLong): RDD[T]
    复制代码
  2. 函数说明

    根据指定的规则从数据集中抽取数据

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - sample
      val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
    
      // sample算子需要传递三个参数
      // 1. 第一个参数表示,抽取数据后是否将数据返回 true(放回),false(丢弃)
      // 2. 第二个参数表示,
      //         如果抽取不放回的场合:数据源中每条数据被抽取的概率,基准值的概念
      //         如果抽取放回的场合:表示数据源中的每条数据被抽取的可能次数
      // 3. 第三个参数表示,抽取数据时随机算法的种子
      //                    如果不传递第三个参数,那么使用的是当前系统时间
      //        println(rdd.sample(
      //            false,
      //            0.4
      //            //1
      //        ).collect().mkString(","))
    
      println(rdd.sample(
        true,
        2
        //1
      ).collect().mkString(","))
    
      sc.stop()
    
    }
    复制代码
  3. 思考一个问题:有啥用,抽奖吗?

1.9、distinct

  1. 函数签名

    def distinct()(implicit ord: Ordering[T] = null): RDD[T]
    def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
    复制代码
  2. 函数说明

    将数据集中重复的数据去重

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - distinct
      val rdd = sc.makeRDD(List(1, 2, 3, 4, 1, 2, 3, 4))
    
      val rdd1: RDD[Int] = rdd.distinct()
    
      rdd1.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码
  3. 思考一个问题:如果不用该算子,你有什么办法实现数据去重?

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - distinct
      val rdd = sc.makeRDD(List(1, 2, 3, 4, 1, 2, 3, 4))
    
      rdd.map(x => (x, null))
        .reduceByKey((x, _) => x, 2)
        .map(_._1)
        .collect()
        .foreach(println)
    
      sc.stop()
    
    }
    复制代码

1.10、coalesce

  1. 函数签名
def coalesce(numPartitions: Int, shuffle: Boolean = false,
    partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
    (implicit ord: Ordering[T] = null)
    : RDD[T]
复制代码
  1. 函数说明

    根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率当 spark 程序中,存在过多的小任务的时候,可以通过 coalesce 方法,收缩合并分区,减少分区的个数,减小任务调度成本

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - coalesce
      val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
    
      // coalesce方法默认情况下不会将分区的数据打乱重新组合
      // 这种情况下的缩减分区可能会导致数据不均衡,出现数据倾斜
      // 如果想要让数据均衡,可以进行shuffle处理
      //val newRDD: RDD[Int] = rdd.coalesce(2)
      val newRDD: RDD[Int] = rdd.coalesce(2, true)
    
      newRDD.saveAsTextFile("output")
    
      sc.stop()
    
    }
    复制代码
  2. 思考一个问题:我想要扩大分区,怎么办?

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - coalesce
      val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 2)
    
      // coalesce算子可以扩大分区的,但是如果不进行shuffle操作,是没有意义,不起作用。
      // 所以如果想要实现扩大分区的效果,需要使用shuffle操作
      // spark提供了一个简化的操作
      // 缩减分区:coalesce,如果想要数据均衡,可以采用shuffle
      // 扩大分区:repartition, 底层代码调用的就是coalesce,而且肯定采用shuffle
      //val newRDD: RDD[Int] = rdd.coalesce(3, true)
      val newRDD: RDD[Int] = rdd.repartition(3)
    
      newRDD.saveAsTextFile("output")
    
      sc.stop()
    
    }
    复制代码

1.11、repartition

  1. 函数签名

    def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
    复制代码
  2. 函数说明

    该操作内部其实执行的是 coalesce 操作,参数 shuffle 的默认值为 true。无论是将分区数多的RDD 转换为分区数少的 RDD,还是将分区数少的 RDD 转换为分区数多的 RDD,repartition操作都可以完成,因为无论如何都会经 shuffle 过程。

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - repartition
      val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 2)
    
      // coalesce算子可以扩大分区的,但是如果不进行shuffle操作,是没有意义,不起作用。
      // 所以如果想要实现扩大分区的效果,需要使用shuffle操作
      // spark提供了一个简化的操作
      // 缩减分区:coalesce,如果想要数据均衡,可以采用shuffle
      // 扩大分区:repartition, 底层代码调用的就是coalesce,而且肯定采用shuffle
      //val newRDD: RDD[Int] = rdd.coalesce(3, true)
      val newRDD: RDD[Int] = rdd.repartition(3)
    
      newRDD.saveAsTextFile("output")
    
      sc.stop()
    
    }
    复制代码
  3. 思考一个问题:coalesce 和 repartition 区别?

1.12、sortBy

  1. 函数签名

    def sortBy[K](
        f: (T) => K,
        ascending: Boolean = true,
        numPartitions: Int = this.partitions.length)
        (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
    复制代码
  2. 函数说明

    该操作用于排序数据。在排序之前,可以将数据通过 f 函数进行处理,之后按照 f 函数处理的结果进行排序,默认为升序排列。排序后新产生的 RDD 的分区数与原 RDD 的分区数一致。中间存在 shuffle 的过程

    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - sortBy
      val rdd = sc.makeRDD(List(6, 2, 4, 5, 3, 1), 2)
    
      val newRDD: RDD[Int] = rdd.sortBy(num => num)
    
      newRDD.saveAsTextFile("output")
    
      sc.stop()
    
    }
    复制代码
    def main(args: Array[String]): Unit = {
    
      val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
      val sc = new SparkContext(sparkConf)
    
      // 算子 - sortBy
      val rdd = sc.makeRDD(List(("1", 1), ("11", 2), ("2", 3)), 2)
    
      // sortBy方法可以根据指定的规则对数据源中的数据进行排序,默认为升序,第二个参数可以改变排序的方式
      // sortBy默认情况下,不会改变分区。但是中间存在shuffle操作
      val newRDD = rdd.sortBy(t => t._1.toInt, false)
    
      newRDD.collect().foreach(println)
    
      sc.stop()
    
    }
    复制代码

2、双 Value 类型

3、Key-Value 类型

二、友情链接

大数据Spark学习之旅第二篇

大数据Spark学习之旅第一篇

猜你喜欢

转载自juejin.im/post/7038991146009231367