Spark学习复习(5.核心编程)

文章目录

Spark核心编程

Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于处理不同的应用场景。三大数据结构分别是:

  • RDD : 弹性分布式数据集
  • 累加器:分布式共享只写变量
  • 广播变量:分布式共享只读变量

接下来我们一起看看这三大数据结构是如何在数据处理中使用的。

1. RDD

RDD与IO
一、IO

在这里插入图片描述

二、RDD

在这里插入图片描述

  1. RDD的数据处理方式类似于IO流,也有装饰者设计模式
  2. RDD的数据只有在调用collect方法时,才会真正执行业务逻辑操作。之前的封装全部都是功能的扩展
  3. RDD是不保存数据的,但是IO可以临时保存一部分数据
什么是 RDD?

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。

  1. 弹性
    • 存储的弹性:内存与磁盘的自动切换;
    • 容错的弹性:数据丢失可以自动恢复;
    • 计算的弹性:计算出错重试机制;
    • 分片的弹性:可根据需要重新分片。
  2. 分布式:数据存储在大数据集群不同节点上
  3. 数据集:RDD 封装了计算逻辑,并不保存数据
  4. 数据抽象:RDD 是一个抽象类,需要子类具体实现
  5. 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD 里面封装计算逻辑
  6. 可分区、并行计算

在这里插入图片描述

RDD核心属性

在这里插入图片描述

一、分区列表

RDD 数据结构中存在分区列表,用于执行任务时并行计算,是实现分布式计算的重要属性。
在这里插入图片描述

二、分区计算函数

Spark 在计算时,是使用分区函数对每一个分区进行计算

在这里插入图片描述

三、RDD 之间的依赖关系

RDD 是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个 RDD 建立依赖关系

在这里插入图片描述

四、分区器(可选)

当数据为 KV 类型数据时,可以通过设定分区器自定义数据的分区
在这里插入图片描述

五、首选位置(可选)

计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算
在这里插入图片描述

在这里插入图片描述

RDD执行原理

从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。

Spark 框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。

RDD 是 Spark 框架中用于数据处理的核心模型,接下来我们看看,在 Yarn 环境中,RDD 的工作原理:

一、启动 Yarn 集群环境

在这里插入图片描述

二、Spark 通过申请资源创建调度节点和计算节点

在这里插入图片描述

三、Spark 框架根据需求将计算逻辑根据分区划分成不同的任务

在这里插入图片描述

四、调度节点将任务根据计算节点状态发送到对应的计算节点进行计算

在这里插入图片描述

从以上流程可以看出 RDD 在整个流程中主要用于将逻辑进行封装,并生成 Task 发送给Executor 节点执行计算,接下来我们就一起看看 Spark 框架中RDD 是具体是如何进行数据处理的。

RDD基础编程

在 Spark 中创建RDD 的创建方式可以分为四种:

  1. 从集合(内存)中创建 RDD
  2. 从外部存储(文件)创建RDD
  3. 从其他 RDD 创建
  4. 直接创建 RDD(new)
一、从集合(内存)中创建 RDD

从底层代码实现来讲,makeRDD 方法其实就是parallelize 方法,因为makeRDD见名知意我们常用makeRDD

/**
  * @Date 2021/4/24 18:43
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDMemory {
  def main(args: Array[String]): Unit = {
    // 环境准备
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
    val sc = new SparkContext(sparkConf)

    // 将内存中集合的数据作为处理的数据源
    val list = List(1, 2, 3, 4)

    // 从内存中创建一个RDD,parallelize : 并行
    val rdd1: RDD[Int] = sc.parallelize(list)
    rdd1.collect().foreach(println)

    // makeRDD底层调用的还是 parallelize(seq, numSlices)
    val rdd2: RDD[Int] = sc.makeRDD(list)
    rdd2.collect().foreach(println)
      
    sc.stop()
  }
}

从底层代码实现来讲,makeRDD 方法其实就是parallelize 方法

def makeRDD[T: ClassTag](seq: Seq[T],numSlices: Int = defaultParallelism): RDD[T] = withScope {
          parallelize(seq, numSlices)
}
二、从外部存储(文件)创建RDD

由外部存储系统的数据集创建RDD 包括:本地的文件系统,所有Hadoop 支持的数据集, 比如HDFS、HBase 等。

import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/4/24 18:59
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDFile01 {
  def main(args: Array[String]): Unit = {
    // 环境准备
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
    val sc = new SparkContext(sparkConf)

    // 从文件中创建RDD,将文件中的数据作为处理的数据源


    // path路径默认以当前环境的根路径为基准。可以写绝对路径,也可以写相对路径
    // val rdd = sc.textFile("data/word.txt")
    // val rdd = sc.textFile("E:\\ScalaCode\\spark-study\\data\\word.txt")
    // path路径可以是文件的具体路径,也可以目录名称
    // val rdd = sc.textFile("data")

    // path路径还可以使用通配符 *
    val rdd = sc.textFile("data/*.txt")

    /// path还可以是分布式存储系统路径HDFS
    // val rdd = sc.textFile("hdfs://hadoop:8020/word.txt")
    rdd.collect().foreach(println)

    sc.stop()
  }
}
三、从其他 RDD 创建

主要是通过一个RDD 运算完后,再产生新的RDD。详情请参考后续章节

四、直接创建 RDD(new)

使用 new 的方式直接构造RDD,一般由Spark 框架自身使用。

RDD 并行度与分区☆

默认情况下,Spark 可以将一个作业切分多个任务后,发送给 Executor 节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在构建RDD 时指定。记住,这里的并行执行的任务数量,并不是指的切分任务的数量,不要混淆了。

一、从内存中读取
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/4/24 19:39
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDMemoryPar01 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")

    // 设置默认并行度
    sparkConf.set("spark.default.parallelism", "5")
    val sc = new SparkContext(sparkConf)

    /** 创建RDD
      * RDD的并行度 & 分区,有区别,某种情况相同
      * makeRDD方法可以传递第二个参数,这个参数表示分区的数量
      * 第二个参数可以不传递的,那么makeRDD方法会使用默认值 : defaultParallelism(默认并行度)
      * scheduler.conf.getInt("spark.default.parallelism", totalCores)
      *  conf就是sparkConf
      * spark在默认情况下,从配置对象中获取配置参数:spark.default.parallelism
      * 如果获取不到,那么使用totalCores属性,这个属性取值为当前运行环境的最大可用核数
      * val rdd = sc.makeRDD(List(1,2,3,4),4)
      */
    val rdd = sc.makeRDD(List(1,2,3,4),2)

    // 将处理的数据保存成分区文件
    rdd.saveAsTextFile("output")

    // 关闭环境
    sc.stop()
  }
}

读取内存数据时,数据可以按照并行度的设定进行数据的分区操作,数据分区规则的Spark 核心源码如下:

   def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
      (0 until numSlices).iterator.map { i =>
        val start = ((i * length) / numSlices).toInt
        val end = (((i + 1) * length) / numSlices).toInt
        (start, end)
      }
    }
   // ....
  case _ =>
        val array = seq.toArray
        positions(array.length, numSlices).map { case (start, end) =>
            array.slice(start, end).toSeq
        }.toSeq

 override def slice(from : scala.Int, until : scala.Int) : scala.Array[T] = { /* compiled code */ }

结合上面源码对下面案例进行分析:

/**
  * @Date 2021/4/24 19:44
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDMemoryPar02 {
  def main(args: Array[String]): Unit = {
    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
    val sc = new SparkContext(sparkConf)

    /**创建RDD
      * 【1,2】,【3,4】
      * val rdd = sc.makeRDD(List(1,2,3,4), 2)
      * 【1】,【2】,【3,4】
      * val rdd = sc.makeRDD(List(1,2,3,4), 3)
      * 【1】,【2,3】,【4,5】
      */
    val rdd = sc.makeRDD(List(1,2,3,4,5), 3)

    // 将处理的数据保存成分区文件
    rdd.saveAsTextFile("output")

    // 关闭环境
    sc.stop()
  }
}

这里我们详细分析一下为什么 val rdd = sc.makeRDD(List(1,2,3,4,5), 3)的结果为【1】,【2,3】,【4,5】

length = 5
numSlices = 3
数据为 1  2  3  4 5
分区为 0  1  2
有源码可知slice(from : scala.Int, until : scala.Int)包前不包后
0 => (0,1) => 1
1 => (1,3) => 2,3
2 => (3,5) => 4,5
所以结果为:【1】,【2,3】,【4,5
二、从文件中读取
/**
  * @Date 2021/4/24 20:06
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDFilePar01 {
  def main(args: Array[String]): Unit = {

    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
    val sc = new SparkContext(sparkConf)

 
    /** 创建RDD
      * textFile可以将文件作为数据处理的数据源,默认也可以设定分区。
      *  minPartitions : 最小分区数量
      *  math.min(defaultParallelism, 2)
      *
      * val rdd = sc.textFile("data/num.txt")
      * 如果不想使用默认的分区数量,可以通过第二个参数指定分区数
      * Spark读取文件,底层其实使用的就是Hadoop的读取方式
      * 分区数量的计算方式:
      *     totalSize = 7
      *     goalSize =  7 / 2 = 3(byte)
      *     7 / 3 = 2余1 (1.1) + 1 = 3(分区)
      */
    val rdd = sc.textFile("data/num.txt",3)

    rdd.saveAsTextFile("output")


    // 关闭环境
    sc.stop()
  }
}

源码如下:

  /** Splits files returned by {@link #listStatus(JobConf)} when
   * they're too big.*/ 
  public InputSplit[] getSplits(JobConf job, int numSplits)
    throws IOException {
    StopWatch sw = new StopWatch().start();
    FileStatus[] files = listStatus(job);
    
    // Save the number of input files for metrics/loadgen
    job.setLong(NUM_INPUT_FILES, files.length);
    long totalSize = 0;                           // compute total size
    for (FileStatus file: files) {                // check we have valid files
      if (file.isDirectory()) {
        throw new IOException("Not a file: "+ file.getPath());
      }
      totalSize += file.getLen();
    }

    long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
    long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
      FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);

    // generate splits
    ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
    NetworkTopology clusterMap = new NetworkTopology();
    for (FileStatus file: files) {
      Path path = file.getPath();
      long length = file.getLen();
      if (length != 0) {
        FileSystem fs = path.getFileSystem(job);
        BlockLocation[] blkLocations;
        if (file instanceof LocatedFileStatus) {
          blkLocations = ((LocatedFileStatus) file).getBlockLocations();
        } else {
          blkLocations = fs.getFileBlockLocations(file, 0, length);
        }
        if (isSplitable(fs, path)) {
          long blockSize = file.getBlockSize();
          long splitSize = computeSplitSize(goalSize, minSize, blockSize);

          long bytesRemaining = length;
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
            String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,
                length-bytesRemaining, splitSize, clusterMap);
            splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                splitHosts[0], splitHosts[1]));
            bytesRemaining -= splitSize;
          }

          if (bytesRemaining != 0) {
            String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
                - bytesRemaining, bytesRemaining, clusterMap);
            splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
                splitHosts[0], splitHosts[1]));
          }
        } else {
          String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap);
          splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1]));
        }
      } else { 
        //Create empty hosts array for zero length files
        splits.add(makeSplit(path, 0, length, new String[0]));
      }
    }
    sw.stop();
    if (LOG.isDebugEnabled()) {
      LOG.debug("Total # of splits generated by getSplits: " + splits.size()
          + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
    }
    return splits.toArray(new FileSplit[splits.size()]);
  }

数据分区的分配

/**
  * @Date 2021/4/24 20:15
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDFilePar02 {
  def main(args: Array[String]): Unit = {

    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
    val sc = new SparkContext(sparkConf)

    /**
      * 数据分区的分配
      * 1. 数据以行为单位进行读取
      *    spark读取文件,采用的是hadoop的方式读取,所以一行一行读取,和字节数没有关系
      * 2. 数据读取时以偏移量为单位,偏移量不会被重复读取
      *
      *   @@表示换行
      *   1@@   => 012
      *   2@@   => 345
      *   3     => 6
      *
      * 3. 数据分区的偏移量范围的计算
      *   0 => [0, 3]  => 1 2
      *   1 => [3, 6]  => 3
      *   2 => [6, 7]  =>
      *   【1,2】,【3】,【】
      */
    val rdd = sc.textFile("data/num.txt", 2)

    rdd.saveAsTextFile("output")


    // 关闭环境
    sc.stop()
  }
}

课堂练习

/**
  * @Date 2021/4/24 20:20
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDFilePar03 {
  def main(args: Array[String]): Unit = {

    // 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
    val sc = new SparkContext(sparkConf)

    /** ☆☆☆☆☆
      * 14byte / 2 = 7byte
      *  14 / 7 = 2(分区)
      *
      * 1234567@@  => 012345678
      * 89@@       => 9101112
      * 0          => 13
      *
      * [0, 7]   => 1234567
      * [7, 14]  => 890
      *
      * TODO 如果数据源为多个文件,那么计算分区时以文件为单位进行分区
      */

    val rdd = sc.textFile("data/num.txt", 2)

    rdd.saveAsTextFile("output")


    // 关闭环境
    sc.stop()
  }
}
RDD转换算子

在这里插入图片描述

为什么RDD中的方法叫算子呢?
在这里插入图片描述

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

一、value类型
map

def map[U: ClassTag](f: T => U): RDD[U]

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

/**
  * @Date 2021/4/25 14:47
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator01Transform01 {
  def main(args: Array[String]): Unit = {

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

    val sc = new SparkContext(sparkConf)

    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)

    // Scala的自减原则
    // 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()

  }
}

小功能:从服务器日志数据 apache.log 中获取用户请求 URL 资源路径

/**
  * @Date 2021/4/25 15:26
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator01Transform02 {
  def main(args: Array[String]): Unit = {

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

    /**
      * 小功能:从服务器日志数据 apache.log 中获取用户请求 URL 资源路径
      */
    val rdd1: RDD[String] = sc.textFile("data/apache.log")

    val rdd2: RDD[String] = rdd1.map(line => {
      val datas = line.split(" ")
      datas(6)
    })

    rdd2.collect().foreach(println)

    sc.stop()
  }
}

不同的分区map算子的使用

/**
  * @Date 2021/4/25 15:34
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator01Transform03Par {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    /**
      * 1. rdd的计算一个分区内的数据是一个一个执行逻辑
      *     只有前面一个数据全部的逻辑执行完毕后,才会执行下一个数据。
      *     分区内数据的执行是有序的。
      * 2. 不同分区数据计算是无序的。
     */
    val rdd1 = sc.makeRDD(List(1, 2, 3, 4), 2)
    val rdd2 = rdd1.map(item => {
      println("☆☆☆☆☆☆" + item)
      item
    })

    val rdd3 = rdd2.map(item => {
      println("△△△△△△" + item)
      item
    })


    rdd3.collect()

    sc.stop()
  }
}
mapPartitions

def mapPartitions[U: ClassTag]( f: Iterator[T] => Iterator[U],preservesPartitioning: Boolean = false): RDD[U]

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

/**
  * @Date 2021/4/25 15:45
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator02Transform01 {
  def main(args: Array[String]): Unit = {

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

    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()
  }
}

小功能:获取每个数据分区的最大值

import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/4/25 15:51
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator02Transform02 {
  def main(args: Array[String]): Unit = {

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

    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 操作。

完成比完美更重要

mapPartitionsWithIndex

def mapPartitionsWithIndex[U: ClassTag]( f: (Int, Iterator[T]) => Iterator[U],preservesPartitioning: Boolean = false): RDD[U]

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

/**
  * @Date 2021/4/25 15:58
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator03Transform01 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(1,2,3,4), 2)

    /**
      * mapPartitionsWithIndex 中的index从0开始
      * 【1,2】,【3,4】
     */
    val mpiRDD = rdd.mapPartitionsWithIndex(
      (index, iter) => {
        if ( index == 0 ) {
          iter
        } else {
          Nil.iterator
        }
      }
    )

    mpiRDD.collect().foreach(println)
    sc.stop()

  }
}

小功能:获取第二个数据分区的数据

/**
  * @Date 2021/4/25 16:00
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator03Transform02 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    val mpiRDD = rdd.mapPartitionsWithIndex(
      (index, iter) => {
        /**
          *结果: (0,1)(1,2),(2,3),(3,4)
          */
        iter.map(
          num => {
            (index, num)
          }
        )
      }
    )

    mpiRDD.collect().foreach(println)


    sc.stop()
  }
}
flatMap

def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]

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

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/4/27 15:29
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator04Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - flatMap
    val rdd: RDD[List[Int]] = sc.makeRDD(List(
      List(1, 2), List(3, 4)
    ))

    val flatRDD: RDD[Int] = rdd.flatMap(
      list => {
        list
      }
    )

    flatRDD.collect().foreach(println)
    sc.stop()
  }
}

RDDOperator04Transform02

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

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

    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()
  }
}

小功能:将 List(List(1,2),3,List(4,5))进行扁平化操作

/**
  * @Date 2021/4/27 15:37
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator04Transform03 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    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)
        }
      }
    )
    flatRDD.collect().foreach(println)
    sc.stop()
  }
}
glom

def glom(): RDD[Array[T]]

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

/**
  * @Date 2021/4/27 16:00
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator05Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - glom
    val rdd : RDD[Int] = sc.makeRDD(List(1,2,3,4), 2)

    // List => Int
    // Int => Array
    val glomRDD: RDD[Array[Int]] = rdd.glom()

    // 1,2
    // 3,4
    glomRDD.collect().foreach(data=> println(data.mkString(",")))

    sc.stop()
  }
}

小功能:计算所有分区最大值求和(分区内取最大值,分区间最大值求和)

/**
  * @Date 2021/4/27 16:01
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator05Transform02 {
  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)

    // 【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()
  }
}
groupBy

def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]

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

/**
  * @Date 2021/4/27 16:09
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator06Transform01 {
  def main(args: Array[String]): Unit = {

    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)
      
    // TODO 算子 - 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)

    //(0,CompactBuffer(2, 4))
    //(1,CompactBuffer(1, 3))
    groupRDD.collect().foreach(println)
    sc.stop()
  }
}

小功能:将 List(“Hello”, “Spark”, “Scala”, “Hadoop”)根据单词首写字母进行分组。

/**
  * @Date 2021/4/27 16:21
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator06Transform02 {
  def main(args: Array[String]): Unit = {

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

    // 小功能:将 List("Hello", "Spark", "Scala", "Hadoop")根据单词首写字母进行分组。
    val rdd  = sc.makeRDD(List("Hello", "Spark", "Scala", "Hadoop"))

    // TODO 分组和分区没有必然的关系
    val groupRDD = rdd.groupBy(_.charAt(0))

    //(H,CompactBuffer(Hello, Hadoop))
    //(S,CompactBuffer(Spark, Scala))
    groupRDD.collect().foreach(println)

    sc.stop()
  }
}

小功能:从服务器日志数据 apache.log 中获取每个时间段访问量。

/**
  * @Date 2021/4/27 16:23
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator06Transform03 {
  def main(args: Array[String]): Unit = {

    // 小功能:从服务器日志数据 apache.log 中获取每个时间段访问量。
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd = sc.textFile("data/apache.log")

    val rdd1 = rdd.map(line => {
      val lines = line.split(" ")
      val time = lines(3)
      val sdf1 = new SimpleDateFormat("dd/MM/yyyy:HH:mm:ss")
      val date: Date = sdf1.parse(time)
      val sdf2 = new SimpleDateFormat("HH")
      val hour: String = sdf2.format(date)
      (hour, 1)
    })
    val rdd2 = rdd1.groupBy(_._1)

    val rdd3 = rdd2.map {
      case (hour, iter) => (hour, iter.size)
    }

    rdd3.collect().foreach(println)

    sc.stop()
  }
}

在这里插入图片描述

filter

def filter(f: T => Boolean): RDD[T]

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

/**
  * @Date 2021/4/27 17:04
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator07Transform01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // TODO 算子 - filter
    val rdd = sc.makeRDD(List(1, 2, 3, 4))
      
    val filterRDD: RDD[Int] = rdd.filter(num => num % 2 != 0)
      
    // 1
    // 3 
    filterRDD.collect().foreach(println)
      
    sc.stop()
  }
}

小功能:从服务器日志数据 apache.log 中获取 2015 年 5 月 17 日的请求路径

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

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

    // 小功能:从服务器日志数据 apache.log 中获取 2015年5月17日的请求路径
    val rdd = sc.textFile("data/apache.log")

    rdd.filter(
      line => {
        val datas = line.split(" ")
        val time = datas(3)
        time.startsWith("17/05/2015")
      }
    ).collect().foreach(println)

    sc.stop()
  }
}
sample

def sample( withReplacement: Boolean, fraction: Double,seed: Long = Utils.random.nextLong): RDD[T]

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

/**
  * @Date 2021/4/27 19:09
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator08Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - 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,
      0.5
      //1
    ).collect().mkString(","))
    sc.stop()
  }
}

在这里插入图片描述

distinct

def distinct()(implicit ord: Ordering[T] = null): RDD[T]

def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

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

/**
  * @Date 2021/4/27 19:16
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator09Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - distinct
    val rdd = sc.makeRDD(List(1,2,3,4,1,2,3,4),1)

    // (1, null),(2, null),(3, null),(4, null),(1, null),(2, null),(3, null),(4, null)
    // (1, null)(1, null)(1, null)
    // (null, null) => null
    // (1, null) => 1
    val rdd1: RDD[Int] = rdd.distinct()

    // 思考一个问题:如果不用该算子,你有什么办法实现数据去重?
    // map(x => (x, null)).reduceByKey((x, _) => x, numPartitions).map(_._1)

    rdd1.collect().foreach(println)

    sc.stop()
  }
}
coalesce
def coalesce(
    numPartitions: Int, shuffle: Boolean = false,
    partitionCoalescer: Option[PartitionCoalescer] = Option.empty
			) (implicit ord: Ordering[T] = null): RDD[T]

根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率

当 spark 程序中,存在过多的小任务的时候,可以通过 coalesce 方法,收缩合并分区,减少分区的个数,减小任务调度成本。

/**
  * @Date 2021/4/27 20:12
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator10Transform01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // TODO 算子 - 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()
  }
}

思考一个问题:我想要扩大分区,怎么办?

repartition

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

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

/**
  * @Date 2021/4/27 20:14
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator11Transform01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // TODO 算子 - repartition
    val rdd = sc.makeRDD(List(1,2,3,4,5,6), 2)

    /**
      * coalesce算子可以扩大分区的,但是如果不进行shuffle操作,是没有意义,不起作用。
      *  所以如果想要实现扩大分区的效果,需要使用shuffle操作
      *  spark提供了一个简化的操作
      *   缩减分区:coalesce,如果想要数据均衡,可以采用shuffle
      *   扩大分区:repartition, 底层代码调用的就是coalesce,而且肯定采用shuffle
      * def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
      *   coalesce(numPartitions, shuffle = true)
      * }
      */
    val newRDD: RDD[Int] = rdd.repartition(3)

    newRDD.saveAsTextFile("output")
    
    sc.stop()
  }
}
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 函数处理的结果进行排序,默认为升序排列true。排序后新产生的 RDD 的分区数与原RDD 的分区数一致。中间存在 shuffle 的过程。

/**
  * @Date 2021/4/27 20:17
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator12Transform01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // TODO 算子 - 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()
  }
}

sortBy方法可以根据指定的规则对数据源中的数据进行排序,默认为升序,第二个参数可以改变排序的方式。sortBy默认情况下,不会改变分区,但是中间存在shuffle操作

/**
  * @Date 2021/4/27 20:20
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator12Transform02 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(("1", 1), ("11", 2), ("2", 3)), 2)

    /**
      * sortBy方法可以根据指定的规则对数据源中的数据进行排序,默认为升序,
      * 第二个参数可以改变排序的方式
      * sortBy默认情况下,不会改变分区,但是中间存在shuffle操作
      */
    val newRDD = rdd.sortBy(t=>t._1.toInt, false)

    //(11,2)
    //(2,3)
    //(1,1)
    newRDD.collect().foreach(println)

    sc.stop()
  }
}
二、双value类型
intersection交集

def intersection(other: RDD[T]): RDD[T]

对源RDD 和参数RDD 求交集后返回一个新的RDD

union并集

def union(other: RDD[T]): RDD[T]

对源RDD 和参数RDD 求并集后返回一个新的RDD

subtract差集

def subtract(other: RDD[T]): RDD[T]

以一个 RDD 元素为主,去除两个 RDD 中重复元素,将其他元素保留下来。求差集

zip拉链

def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)]

将两个 RDD 中的元素,以键值对的形式进行合并。其中,键值对中的Key 为第1个RDD中的元素,Value为第2个 RDD 中的相同位置的元素。

/**
  * @Date 2021/4/27 20:22
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator13Transform01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    /** 算子 - 双Value类型
      * 交集,并集和差集要求两个数据源数据类型保持一致
      * 拉链操作两个数据源的类型可以不一致
      */

    val rdd1 = sc.makeRDD(List(1, 2, 3, 4))
    val rdd2 = sc.makeRDD(List(3, 4, 5, 6))

    // TODO 算子 - intersection 交集
    val rdd3 = rdd1.intersection(rdd2)
    println(rdd3.collect().mkString("->"))

    // TODO 算子 - union 并集
    val rdd4 = rdd1.union(rdd2)
    println(rdd4.collect().mkString("->"))
    // TODO 算子 - subtract 差集
    val rdd5 = rdd1.subtract(rdd2)
    println(rdd5.collect().mkString("->"))
    // TODO 算子 - zip 拉链
    val rdd6 = rdd1.zip(rdd2)


    println(rdd6.collect().mkString("->"))
    val rdd7 = sc.makeRDD(List("aa", "bb", "cc", "dd"))

    val rdd8 = rdd1.zip(rdd7)
    // (1,aa)->(2,bb)->(3,cc)->(4,dd)
    println(rdd8.collect().mkString("->"))

    // val rdd9 = rdd1.union(rdd7) 编译报错
    sc.stop()
  }
}
细节:
  1. 如果两个RDD 数据类型不一致怎么办?

    • 交集,并集和差集要求两个数据源数据类型保持一致
    • 拉链操作两个数据源的类型可以不一致
  2. 对拉链来说:如果两个RDD 数据分区不一致怎么办?

  3. 对拉链来说:如果两个RDD 分区数据数量不一致怎么办?

/**
  * @Date 2021/4/27 20:33
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator13Transform02 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - 双Value类型
    // Can't zip RDDs with unequal numbers of partitions: List(2, 4)
    // 两个数据源要求分区数量要保持一致
    // Can only zip RDDs with same number of elements in each partition
    // 两个数据源要求分区中数据数量保持一致
    val rdd1 = sc.makeRDD(List(1,2,3,4,5,6),2)
    val rdd2 = sc.makeRDD(List(3,4,5,6),2)

    val rdd3: RDD[(Int, Int)] = rdd1.zip(rdd2)
    println(rdd3.collect().mkString(","))

    sc.stop()
  }
}
三、key-value类型
partitionBy

def partitionBy(partitioner: Partitioner): RDD[(K, V)]

将数据按照指定Partitioner 重新进行分区。Spark 默认的分区器是HashPartitioner

/**
  * @Date 2021/4/27 20:35
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator14Transform01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // TODO 算子 - (Key - Value类型) partitionBy
    val rdd = sc.makeRDD(List(1,2,3,4),2)

    val mapRDD:RDD[(Int, Int)] = rdd.map((_,1))
    // RDD => PairRDDFunctions
    // 隐式转换(二次编译)

    // partitionBy根据指定的分区规则对数据进行重分区
    val newRDD = mapRDD.partitionBy(new HashPartitioner(2))

    val resRDD = newRDD.partitionBy(new HashPartitioner(2))

    newRDD.saveAsTextFile("output1")

    // 与上面一样,再次使用partitionBy不会再有改变
    resRDD.saveAsTextFile("output2")

    sc.stop()
  }
}
  1. 思考一个问题:如果重分区的分区器和当前RDD 的分区器一样怎么办?
  2. 思考一个问题:Spark 还有其他分区器吗?
  3. 思考一个问题:如果想按照自己的方法进行数据分区怎么办?
reduceByKey

def reduceByKey(func: (V, V) => V): RDD[(K, V)]

def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]

可以将数据按照相同的Key 对Value 进行聚合

/**
  * @Date 2021/4/28 13:08
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator15Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - (Key - Value类型) reduceByKey

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("a", 3), ("b", 4)
    ))

    /**
      * reduceByKey : 相同的key的数据进行value数据的聚合操作
      *  scala语言中一般的聚合操作都是两两聚合,spark基于scala开发的,所以它的聚合也是两两聚合
      *  【1,2,3】
      *  【3,3】
      *  【6】
      * reduceByKey中如果key的数据只有一个,是不会参与运算的。
      */
    val reduceRDD: RDD[(String, Int)] = rdd.reduceByKey( (x:Int, y:Int) => {
      println(s"x = ${x}, y = ${y}")
      x + y
    } )

    reduceRDD.collect().foreach(println)

    sc.stop()
  }
}
groupByKey

def groupByKey(): RDD[(K, Iterable[V])]

def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]

def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]

将数据源的数据根据 key 对 value 进行分组

/**
  * @Date 2021/4/28 13:11
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator16Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - (Key - Value类型) groupByKey

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("a", 3), ("b", 4)
    ))

    /**
      * groupByKey : 将数据源中的数据,相同key的数据分在一个组中,形成一个对偶元组
      *               元组中的第一个元素就是key,
      *               元组中的第二个元素就是相同key的value的集合
      */
    val groupRDD1: RDD[(String, Iterable[Int])] = rdd.groupByKey()

    groupRDD1.collect().foreach(println)

    val groupRDD2: RDD[(String, Iterable[(String, Int)])] = rdd.groupBy(_._1)
    groupRDD2.collect().foreach(println)

    /** 注意groupByKey和groupBy的区别:
      * (a,CompactBuffer(1, 2, 3))
      * (b,CompactBuffer(4))
      *
      * (a,CompactBuffer((a,1), (a,2), (a,3)))
      * (b,CompactBuffer((b,4)))
      */
    sc.stop()
  }
}
reduceByKey和groupByKey的区别?

从 shuffle 的角度:

  • reduceByKey 和 groupByKey 都存在 shuffle 的操作,但是reduceByKey 可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而groupByKey 只是进行分组,不存在数据量减少的问题,reduceByKey 性能比较高。
    -在这里插入图片描述

从功能的角度:

  • reduceByKey 其实包含分组和聚合的功能。groupByKey 只能分组,不能聚合,所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用groupByKey
  • 在这里插入图片描述
aggregateByKey

def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)]

将数据根据不同的规则进行分区内计算和分区间计算

/**
  * @Date 2021/4/28 13:28
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator17Transform01 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // TODO 算子 - (Key - Value类型) aggregateByKey

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("a", 3), ("a", 4)
    ),2)

    /** (a,【1,2】), (a, 【3,4】)
      * (a, 2), (a, 4)
      * (a, 6)
      * aggregateByKey存在函数柯里化,有两个参数列表
      *  第一个参数列表,需要传递一个参数,表示为初始值
      *        主要用于当碰见第一个key的时候,和value进行分区内计算
      *  第二个参数列表需要传递2个参数
      *       第一个参数表示分区内计算规则
      *       第二个参数表示分区间计算规则
      *  math.min(x, y)
      *  math.max(x, y)
      */
    rdd.aggregateByKey(0)(
      (x, y) => math.max(x, y),
      (x, y) => x + y
    ).collect.foreach(println)

    sc.stop()
  }
}

取出每个分区内相同 key 的最大值然后分区间相加

/**
  * @Date 2021/4/28 13:37
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator17Transform02 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("b", 3),
      ("b", 4), ("b", 5), ("a", 6)),2)

    //(a,11)
    //(b,10)
    rdd.aggregateByKey(5)(
      (x, y) => math.max(x, y),
      (x, y) => x + y
    ).collect.foreach(println)
    println("***********************")

    //(a,9)
    //(b,12)
    rdd.aggregateByKey(0)(
      (x, y) => x + y,
      (x, y) => x + y
    ).collect.foreach(println)

    //(a,9)
    //(b,12)
    rdd.aggregateByKey(0)(_+_, _+_).collect.foreach(println)

    sc.stop()
  }
}

在这里插入图片描述

/**
  * @Date 2021/4/28 13:53
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator17Transform03 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("b", 3),
      ("b", 4), ("b", 5), ("a", 6)
    ),2)

    /**
      * aggregateByKey最终的返回数据结果应该和初始值的类型保持一致
      * val aggRDD: RDD[(String, String)] = rdd.aggregateByKey("")(_ + _, _ + _)
      * aggRDD.collect.foreach(println)
      *
      * RDD[(String, (Int, Int))]:
      *       a       sum  count
      */
    val newRDD : RDD[(String, (Int, Int))] = rdd.aggregateByKey( (0,0) )(
      // 参数一:分区内的计算规则,这里的t是分区内的第一个参数,即(0,0),这样的话就很好理解了
      ( t, v ) => {
        (t._1 + v, t._2 + 1)
      },
      // 参数二:分区间的计算规则
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2)
      }
    )

    val resultRDD: RDD[(String, Int)] = newRDD.mapValues {
      case (num, cnt) => {
        num / cnt
      }
    }
    // 获取相同key的数据的平均值 => (a, 3),(b, 4)
    resultRDD.collect().foreach(println)
    
    sc.stop()
  }
}

在这里插入图片描述

分区内计算规则和分区间计算规则相同怎么办?

foldByKey

def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]

当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为foldByKey

/**
  * @Date 2021/4/28 13:50
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator18Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - (Key - Value类型) foldByKey
    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("b", 3),
      ("b", 4), ("b", 5), ("a", 6)
    ),2)

    /**
      * rdd.aggregateByKey(0)(_+_, _+_).collect.foreach(println)
      * 如果聚合计算时,分区内和分区间计算规则相同,spark提供了简化的方法
      */
    rdd.foldByKey(0)(_+_).collect.foreach(println)

    sc.stop()
  }
}
combineByKey
def combineByKey[C]( createCombiner: V => C, 
                    mergeValue: (C, V) => C,
                    mergeCombiners: (C, C) => C): RDD[(K, C)]

最通用的对key-value 型 rdd 进行聚集操作的聚集函数(aggregation function)。类似于aggregate(),combineByKey()允许用户返回值的类型与输入不一致。
在这里插入图片描述

小练习:将数据List( ("a", 1), ("a", 2), ("b", 3),("b", 4), ("b", 5), ("a", 6))求每个 key 的平均值。

/**
  * @Date 2021/4/28 14:07
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator19Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - (Key - Value类型) combineByKey
    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("b", 3),
      ("b", 4), ("b", 5), ("a", 6)
    ),2)

    /**
      * combineByKey : 方法需要三个参数
      *  第一个参数表示:将相同key的第一个数据进行结构的转换,实现操作
      *  第二个参数表示:分区内的计算规则
      *  第三个参数表示:分区间的计算规则
      */
    val newRDD : RDD[(String, (Int, Int))] = rdd.combineByKey(
      v => (v, 1),
      ( t:(Int, Int), v ) => {
        (t._1 + v, t._2 + 1)
      },
      (t1:(Int, Int), t2:(Int, Int)) => {
        (t1._1 + t2._1, t1._2 + t2._2)
      }
    )

    val resultRDD: RDD[(String, Int)] = newRDD.mapValues {
      case (num, cnt) => {
        num / cnt
      }
    }
    resultRDD.collect().foreach(println)

    sc.stop()
  }
}
reduceByKey、foldByKey、aggregateByKey、combineByKey 的区别?

reduceByKey: 相同 key 的第一个数据不进行任何计算,分区内和分区间计算规则相同

aggregateByKey:相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则可以不相同

foldByKey: 相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则相同

combineByKey:当计算时,发现数据结构不满足要求时,可以让第一个数据转换结构。分区内和分区间计算规则不相同。

/**
  * @Date 2021/4/28 14:10
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator19Transform02 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    // TODO 算子 - (Key - Value类型) reduceByKey aggregateByKey foldByKey combineByKey
    val rdd = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("b", 3),
      ("b", 4), ("b", 5), ("a", 6)
    ),2)

    /**
    reduceByKey:
         combineByKeyWithClassTag[V](
             (v: V) => v, // 第一个值不会参与计算
             func, // 分区内计算规则
             func, // 分区间计算规则
             )

    aggregateByKey :

        combineByKeyWithClassTag[U](
            (v: V) => cleanedSeqOp(createZero(), v), // 初始值和第一个key的value值进行的分区内数据操作
            cleanedSeqOp, // 分区内计算规则
            combOp,       // 分区间计算规则
            )

    foldByKey:

        combineByKeyWithClassTag[V](
            (v: V) => cleanedFunc(createZero(), v), // 初始值和第一个key的value值进行的分区内数据操作
            cleanedFunc,  // 分区内计算规则
            cleanedFunc,  // 分区间计算规则
            )

    combineByKey :

        combineByKeyWithClassTag(
            createCombiner,  // 相同key的第一条数据进行的处理函数
            mergeValue,      // 表示分区内数据的处理函数
            mergeCombiners,  // 表示分区间数据的处理函数
            )

     */

    rdd.reduceByKey(_+_) // wordcount
    rdd.aggregateByKey(0)(_+_, _+_) // wordcount
    rdd.foldByKey(0)(_+_) // wordcount
    rdd.combineByKey(v=>v,(x:Int,y:Int)=>x+y,(x:Int,y:Int)=>x+y) // wordcount

    sc.stop()
  }
}
sortByKey

def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length): RDD[(K, V)]

在一个(K,V)的 RDD上调用,K 必须实现 Ordered 接口(特质),返回一个按照 key 进行排序的

join

def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]

在类型为(K,V)和(K,W)的RDD 上调用,返回一个相同 key 对应的所有元素连接在一起的(K,(V,W))的RDD

package com.atguigu.sparkstudy.core.rdd.operator.transform

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/4/28 14:15
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator20Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - (Key - Value类型) sortByKey join

    val dataRDD1 = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3)))
    val sortRDD1: RDD[(String, Int)] = dataRDD1.sortByKey(true)
    val sortRDD2: RDD[(String, Int)] = dataRDD1.sortByKey(false)
    sortRDD1.collect().foreach(println)
    sortRDD2.collect().foreach(println)
    //(a,1)
    //(b,2)
    //(c,3)
    //(c,3)
    //(b,2)
    //(a,1)
      
    println("-----------------------------------")
    val rdd1 = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("c", 3)
    ))

    val rdd2 = sc.makeRDD(List(
      ("a", 5), ("c", 6), ("a", 4)
    ))

    /**
      * join : 两个不同数据源的数据,相同的key的value会连接在一起,形成元组
      * 如果两个数据源中key没有匹配上,那么数据不会出现在结果中
      * 如果两个数据源中key有多个相同的,会依次匹配,可能会出现笛卡尔乘积,数据量会几何性增长,会导致性能降低。
      * 类似于MySql中的join
      */
    val joinRDD: RDD[(String, (Int, Int))] = rdd1.join(rdd2)

    //(a,(1,5))
    //(a,(1,4))
    //(a,(2,5))
    //(a,(2,4))
    //(c,(3,6))
    joinRDD.collect().foreach(println)

    sc.stop()
  }
}

如果 key 存在不相等呢?如果两个数据源中key没有匹配上,那么数据不会出现在结果中

leftOuterJoin & rightOuterJoin

def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]

类似于 SQL 语句的左外连接

/**
  * @Date 2021/4/28 14:21
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator21Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - (Key - Value类型) leftOuterJoin rightOuterJoin
    val rdd1 = sc.makeRDD(List(
      ("a", 1), ("b", 2), ("c", 3)
    ))

    val rdd2 = sc.makeRDD(List(
      ("a", 4), ("b", 5),("c", 6)
    ))
    val leftJoinRDD = rdd1.leftOuterJoin(rdd2)
    val rightJoinRDD = rdd1.rightOuterJoin(rdd2)

    //(a,(1,Some(4)))
    //(b,(2,Some(5)))
    //(c,(3,Some(6)))
    leftJoinRDD.collect().foreach(println)

    println("-----------------------------------")

    //(a,(Some(1),4))
    //(b,(Some(2),5))
    //(c,(Some(3),6))
    rightJoinRDD.collect().foreach(println)

    sc.stop()
  }
}
cogroup

def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]

在类型为(K,V)和(K,W)的RDD 上调用,返回一个(K,(Iterable<V>,Iterable<W>))类型的 RDD

/**
  * @Date 2021/4/28 14:23
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator22Transform01 {
  def main(args: Array[String]): Unit = {

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

    // TODO 算子 - (Key - Value类型) cogroup : connect + group (分组,连接)
    val rdd1 = sc.makeRDD(List(
      ("a", 1), ("b", 2) , ("c", 3)
    ))

    val rdd2 = sc.makeRDD(List(
      ("a", 4), ("b", 5), ("c", 6), ("c", 7)
    ))

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

    //(a,(CompactBuffer(1),CompactBuffer(4)))
    //(b,(CompactBuffer(2),CompactBuffer(5)))
    //(c,(CompactBuffer(3),CompactBuffer(6, 7)))
    cgRDD.collect().foreach(println)

    sc.stop()
  }
}
四、案例实操
1. 数据准备

agent.log:时间戳 省份 城市 用户 广告 中间字段使用空格分隔。

2. 需求描述

统计出每一个省份每个广告被点击数量排行的 Top3

3. 需求分析

在这里插入图片描述

4. 功能实现
/**
  * @Date 2021/4/28 16:15
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator23Req {
  def main(args: Array[String]): Unit = {

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

    // TODO 案例实操
    // 1. 获取原始数据:时间戳,省份,城市,用户,广告
    val rdd1 = sc.textFile("data/agent.log")

    // 2. 将原始数据进行结构的转换。方便统计
    //    时间戳,省份,城市,用户,广告
    //    =>
    //    ( ( 省份,广告 ), 1 )
    val rdd2 = rdd1.map(line => {
      val lines = line.split(" ")
      ((lines(1), lines(4)), 1)
    })

    // 3. 将转换结构后的数据,进行分组聚合
    //    ( ( 省份,广告 ), 1 ) => ( ( 省份,广告 ), sum )
    val rdd3 = rdd2.reduceByKey(_ + _)

    // 4. 将聚合的结果进行结构的转换
    //    ( ( 省份,广告 ), sum ) => ( 省份, ( 广告, sum ) )
    val rdd4 = rdd3.map {
      case (t, sum) => {
        (t._1, (t._2, sum))
      }
    }

    // 5. 将转换结构后的数据根据省份进行分组
    //    ( 省份, 【( 广告A, sumA ),( 广告B, sumB )】 )
    val rdd5 = rdd4.groupByKey()

    // 6. 将分组后的数据组内排序(降序),取前3名
    val resRDD = rdd5.mapValues(
      iter => {
        iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
      }
    )

    println("省份\t TOP3......")
    // 7. 采集数据打印在控制台
    resRDD.collect().foreach(println)

    sc.stop()
  }
}
五、WordCount

Spark十一种实现WordCount的方式。

package com.atguigu.sparkstudy.core.wc

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable

/**
  * @Date 2021/4/30 17:06
  * @Version 10.21
  * @Author DuanChaojie
  */
object WordCount03 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("WordCount")
    val sc = new SparkContext(sparConf)

    val words: RDD[String] = sc.textFile("data/word.txt")

    wordCount91011(words)

    sc.stop()
  }

  /**
    * groupBy
    * @param rdd
    */
  def wordCount01(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    val groupRDD: RDD[(String, Iterable[String])] = words.groupBy(word => word)
    // groupRDD.mapValues(iter => iter.size)
    // 在mapValues指的是对一个RDD的每一行的value值进行处理
    // flatMapValues指的是对一个RDD的每一行的value值进行处理 处理完后,还会将返回值进行拉伸 所以返回值一般会是 List Array 或者Some
    val wordCount: RDD[(String, Int)] = groupRDD.mapValues(iter => iter.size)

    wordCount.collect().foreach(println)
  }

  /**
    * groupByKey
    * @param rdd
    */
  def wordCount02(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    val mapRDD = words.map((_, 1))
    val groupByKeyRDD: RDD[(String, Iterable[Int])] = mapRDD.groupByKey()
    val wordCount = groupByKeyRDD.mapValues(_.size)

    wordCount.collect().foreach(println)
  }


  /**
    * reduceByKey
    * @param rdd
    */
  def wordCount03(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    val mapRDD = words.map((_, 1))
    val reduceByKeyRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_ + _)

    reduceByKeyRDD.collect().foreach(println)
  }


  /**
    * aggregateByKey
    * @param rdd
    */
  def wordCount04(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    val mapRDD = words.map((_, 1))
    // 函数的柯理化
    val aggregateByKeyRDD: RDD[(String, Int)] = mapRDD.aggregateByKey(0)(_ + _, _ + _)

    aggregateByKeyRDD.collect().foreach(println)
  }

  /**
    * foldByKey
    * @param rdd
    */
  def wordCount05(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    val mapRDD = words.map((_, 1))
    // 函数的柯理化
    val foldByKeyRDD: RDD[(String, Int)] = mapRDD.foldByKey(0)(_ + _)
    foldByKeyRDD.collect().foreach(println)
  }


  /**
    * combineByKey
    * @param rdd
    */
  def wordCount06(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    val mapRDD = words.map((_, 1))
    val combineByKeyRDD: RDD[(String, Int)] = mapRDD.combineByKey(
      v => v
      , (x: Int, y: Int) => {
        x + y
      }
      , (x: Int, y: Int) => {
        x + y
      })

    combineByKeyRDD.collect().foreach(println)
  }

  /**
    * countByKey
    * @param rdd
    */
  def wordCount07(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    val mapRDD = words.map((_, 1))
    // countByKey行动算子
    val countByKey: collection.Map[String, Long] = mapRDD.countByKey()

    println(countByKey)
  }


  /**
    * countByValue
   */
  def wordCount08(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    // countByKey行动算子
    val countByValue: collection.Map[String, Long] = words.countByValue()
    println(countByValue)
  }

  /**
    * reduce aggregate fold
    * @param rdd
    */
  def wordCount91011(rdd: RDD[String]) = {
    val words = rdd.flatMap(_.split(" "))
    val mapWord = words.map(word => {
      mutable.Map[String, Long]((word, 1))
    })

    val wordCount = mapWord.reduce((map1, map2) => {
      map2.foreach {
        case (word, count) => {
          val newCount = map1.getOrElse(word, 0L) + count
          map1.update(word, newCount)
        }
      }
      map1
    })

    println(wordCount)
  }

}
行动算子
reduce

def reduce(f: (T, T) => T): T

聚集RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 聚合数据
val reduceResult: Int = rdd.reduce(_+_)
collect

def collect(): Array[T]

在驱动程序中,以数组Array 的形式返回数据集的所有元素

/**
  * @Date 2021/4/28 20:27
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator01Action01 {
  def main(args: Array[String]): Unit = {

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

    // TODO - 行动算子 collect
    val rdd = sc.makeRDD(List(1,2,3,4))

    /**
      * 所谓的行动算子,其实就是触发作业(Job)执行的方法
      * 底层代码调用的是环境对象的runJob方法
      * 底层代码中会创建ActiveJob,并提交执行。
      */
    rdd.collect()

    sc.stop()
  }
}
count

def count(): Long

返回RDD 中元素的个数

first

def first(): T

返回RDD 中的第一个元素

take

def take(num: Int): Array[T]

返回一个由RDD 的前 n 个元素组成的数组

takeOrdered

def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]

返回该 RDD 排序后的前 n 个元素组成的数组

/**
  * @Date 2021/4/28 20:29
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator02Action01 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(1, 2, 3, 4))

    // TODO - 行动算子 reduce collect count first take takeOrdered

    // val i: Int = rdd.reduce(_+_)
    // println(i) // 10

    // collect : 方法会将不同分区的数据按照分区顺序采集到Driver端内存中,形成数组
    // val ints: Array[Int] = rdd.collect()
    // println(ints.mkString(","))

    // count : 数据源中数据的个数
    val cnt = rdd.count()
    println(cnt)

    // first : 获取数据源中数据的第一个
    val first = rdd.first()
    println(first)

    // take : 获取N个数据
    val ints: Array[Int] = rdd.take(3)
    println(ints.mkString(","))

    // takeOrdered : 数据排序后,取N个数据
    val rdd1 = sc.makeRDD(List(4, 2, 3, 1))
    val ints1: Array[Int] = rdd1.takeOrdered(3)
    println(ints1.mkString(","))

    sc.stop()
  }
}
aggregate

def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U

分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合

注意与aggregateByKey的区别

fold

def fold(zeroValue: T)(op: (T, T) => T): T

折叠操作,aggregate 的简化版操作


/**
  * @Date 2021/4/28 20:32
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator03Action01 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(1, 2, 3, 4), 2)

    // TODO - 行动算子 aggregate fold

    /**
      * 10 + 13 + 17 = 40
      * aggregateByKey : 初始值只会参与分区内计算
      * aggregate : 初始值会参与分区内计算,并且和参与分区间计算
      */
    val result1 = rdd.aggregate(10)(_ + _, _ + _)

    // 分区内计算逻辑和分区间计算逻辑相同时使用fold
    val result2 = rdd.fold(10)(_ + _)

    println(result1)
    println("-----------------------")
    println(result2)

    sc.stop()
  }
}
countByKey & countByValue

def countByKey(): Map[K, Long]

统计每种 key/Value的个数

/**
  * @Date 2021/4/28 20:35
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator04Action01 {
  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, 1, 1, 4), 2)
    val rdd2 = sc.makeRDD(List(
      ("a", 1), ("a", 2), ("a", 3)
    ))

    // TODO - 行动算子 countByKey countByValue
    val values: collection.Map[Int, Long] = rdd1.countByValue()
    println(values)

    val keys: collection.Map[String, Long] = rdd2.countByKey()
    println(keys)

    sc.stop()
  }
}
save 相关算子

def saveAsTextFile(path: String): Unit

def saveAsObjectFile(path: String): Unit

def saveAsSequenceFile(path: String,codec: Option[Class[_ <: CompressionCodec]] = None): Unit

将数据保存到不同格式的文件中


/**
  * @Date 2021/4/28 20:37
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator05Action01 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(
      ("a", 1),("a", 2),("a", 3)
    ))

    // TODO - 行动算子 saveAsTextFile saveAsObjectFile saveAsSequenceFile
    rdd.saveAsTextFile("output1")

    rdd.saveAsObjectFile("output2")

    // saveAsSequenceFile方法要求数据的格式必须为K-V类型
    rdd.saveAsSequenceFile("output3")

    sc.stop()
  }
}
foreach
def foreach(f: T => Unit): Unit = withScope { val cleanF = sc.clean(f)
    sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}

分布式遍历RDD 中的每一个元素,调用指定函数

/**
  * @Date 2021/4/28 20:40
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDOperator06Action01 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(1,2,3,4))

    // foreach其实是Driver端内存集合的循环遍历方法
    rdd.collect().foreach(println)

    println("--------------------")

    // foreach 其实是Executor端内存数据打印
    rdd.foreach(println)
    /**
      * 算子 : Operator(操作)
      *         RDD的方法和Scala集合对象的方法不一样
      *         集合对象的方法都是在同一个节点的内存中完成的。
      *         RDD的方法可以将计算逻辑发送到Executor端(分布式节点)执行
      *         为了区分不同的处理效果,所以将RDD的方法称之为算子。
      *         RDD的方法外部的操作都是在Driver端执行的,而方法内部的逻辑代码是在Executor端执行。
      */
    sc.stop()
  }
}

在这里插入图片描述

RDD序列化
一、闭包检查

从计算的角度, 算子以外的代码都是在Driver 端执行, 算子里面的代码都是在 Executor 端执行。那么在 scala 的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给Executor 端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测。

Scala2.12 版本后闭包编译方式发生了改变。

/**
  * @Date 2021/4/30 16:38
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDSerialization01 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(1, 2, 3, 4))

    val user = new User()

    // Exception in thread "main" org.apache.spark.SparkException: Task not serializable
    // java.io.NotSerializableException: com.atguigu.sparkstudy.core.rdd.operator.action.RDDOperator07Action01$User

    // RDD算子中传递的函数是会包含闭包操作,那么就会进行检测功能
    // 闭包检测
    rdd.foreach(
      num => {
        println("age = " + (user.age + num))
      }
    )

    sc.stop()
  }

  //class User extends Serializable
  // 样例类在编译时,会自动混入序列化特质(实现可序列化接口)
  //case class User()
  class User extends Serializable {
    var age: Int = 30
  }

}

在这里插入图片描述

二、序列化方法和属性

从计算的角度, 算子以外的代码都是在Driver 端执行, 算子里面的代码都是在 Executor端执行

/**
  * @Date 2021/4/30 16:38
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDSerialization02 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("WordCount")
    val sc = new SparkContext(sparConf)

    val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello spark", "hive", "atguigu"))

    val search = new Search("h")

    // Exception in thread "main" org.apache.spark.SparkException: Task not serializable
    // search.getMatch1(rdd).collect().foreach(println)

    // 正确
    search.getMatch2(rdd).collect().foreach(println)

    sc.stop()
  }

  /**
    * 查询对象
    * 类的构造参数其实是类的属性, 构造参数需要进行闭包检测,其实就等同于类进行闭包检测
    * @param query
    */
  class Search(query:String){

    def isMatch(s: String): Boolean = {
      s.contains(this.query)
    }

    // 函数序列化案例
    def getMatch1 (rdd: RDD[String]): RDD[String] = {
      rdd.filter(isMatch)
    }

    // 属性序列化案例
    def getMatch2(rdd: RDD[String]): RDD[String] = {
      //rdd.filter(x => x.contains(query)) 上面会报错 org.apache.spark.SparkException: Task not serializable

      val s = query
      rdd.filter(x => x.contains(s))
    }
  }
}
三、Kryo 序列化框架
  1. 参考地址: https://github.com/EsotericSoftware/kryo

  2. Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。Spark 出于性能的考虑,Spark2.0 开始支持另外一种Kryo 序列化机制。Kryo 速度是 Serializable 的 10 倍。当 RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型已经在 Spark 内部使用 Kryo 来序列化。

  3. 注意:即使使用Kryo 序列化,也要继承Serializable 接口。

 val conf: SparkConf = new SparkConf().setAppName("SerDemo").setMaster("local[*]")
// 替换默认的序列化机制
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
// 注册需要使用 kryo 序列化的自定义类
.registerKryoClasses(Array(classOf[Searcher])) 

val sc = new SparkContext(conf)
RDD依赖关系

在这里插入图片描述

一、RDD 血缘关系

RDD 只支持粗粒度转换,即在大量记录上执行的单个操作。将创建 RDD 的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD 的Lineage 会记录RDD 的元数据信息和转换行为,当该RDD 的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/4/30 17:02
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDDepend01 {
  def main(args: Array[String]): Unit = {

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

    // TODO lines.toDebugString
    val lines: RDD[String] = sc.textFile("data/word.txt")
    println(lines.toDebugString)
    println("*************************")

    val words: RDD[String] = lines.flatMap(_.split(" "))
    println(words.toDebugString)
    println("*************************")

    val wordToOne = words.map(word=>(word,1))
    println(wordToOne.toDebugString)
    println("*************************")

    val wordToSum: RDD[(String, Int)] = wordToOne.reduceByKey(_+_)
    println(wordToSum.toDebugString)
    println("*************************")

    val array: Array[(String, Int)] = wordToSum.collect()
    array.foreach(println)

    sc.stop()
  }
}

打印结果为:

(2) data/word.txt MapPartitionsRDD[1] at textFile at RDDDepend01.scala:18 []
 |  data/word.txt HadoopRDD[0] at textFile at RDDDepend01.scala:18 []
*************************
(2) MapPartitionsRDD[2] at flatMap at RDDDepend01.scala:22 []
 |  data/word.txt MapPartitionsRDD[1] at textFile at RDDDepend01.scala:18 []
 |  data/word.txt HadoopRDD[0] at textFile at RDDDepend01.scala:18 []
*************************
(2) MapPartitionsRDD[3] at map at RDDDepend01.scala:26 []
 |  MapPartitionsRDD[2] at flatMap at RDDDepend01.scala:22 []
 |  data/word.txt MapPartitionsRDD[1] at textFile at RDDDepend01.scala:18 []
 |  data/word.txt HadoopRDD[0] at textFile at RDDDepend01.scala:18 []
*************************
(2) ShuffledRDD[4] at reduceByKey at RDDDepend01.scala:30 []
 +-(2) MapPartitionsRDD[3] at map at RDDDepend01.scala:26 []
    |  MapPartitionsRDD[2] at flatMap at RDDDepend01.scala:22 []
    |  data/word.txt MapPartitionsRDD[1] at textFile at RDDDepend01.scala:18 []
    |  data/word.txt HadoopRDD[0] at textFile at RDDDepend01.scala:18 []
*************************
(Flink,1)
(Hello,5)
(Java,1)
(Scala,1)
(Spark,2)
二、RDD 依赖关系

这里所谓的依赖关系,其实就是两个相邻 RDD 之间的关系

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/5/1 15:15
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDDepend02 {
  def main(args: Array[String]): Unit = {

    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Dep")
    val sc = new SparkContext(sparConf)
    // TODO lines.dependencies
    val lines: RDD[String] = sc.textFile("data/word.txt")
    println(lines.dependencies)
    println("*************************")
    val words: RDD[String] = lines.flatMap(_.split(" "))
    println(words.dependencies)
    println("*************************")
    val wordToOne = words.map(word => (word, 1))
    println(wordToOne.dependencies)
    println("*************************")
    val wordToSum: RDD[(String, Int)] = wordToOne.reduceByKey(_ + _)
    println(wordToSum.dependencies)
    println("*************************")
    val array: Array[(String, Int)] = wordToSum.collect()
    array.foreach(println)

    sc.stop()
  }
}

打印结果为:

List(org.apache.spark.OneToOneDependency@250a500b)
*************************
List(org.apache.spark.OneToOneDependency@4c58255)
*************************
List(org.apache.spark.OneToOneDependency@5a1c3cb4)
*************************
List(org.apache.spark.ShuffleDependency@748f93bb)
*************************
(Flink,1)
(Hello,5)
(Java,1)
(Scala,1)
(Spark,2)

在这里插入图片描述

三、RDD 窄依赖

窄依赖表示每一个父(上游)RDD 的 Partition 最多被子(下游)RDD 的一个 Partition 使用, 窄依赖我们形象的比喻为独生子女。

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd)

在这里插入图片描述

四、RDD 宽依赖

宽依赖表示同一个父(上游)RDD 的 Partition 被多个子(下游)RDD 的 Partition 依赖,会引起 Shuffle,总结:宽依赖我们形象的比喻为多生。

class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
 @transient private val _rdd: RDD[_ <: Product2[K, V]],
 val partitioner: Partitioner,
 val serializer: Serializer = SparkEnv.get.serializer,
 val keyOrdering: Option[Ordering[K]] = None,
 val aggregator: Option[Aggregator[K, V, C]] = None,
 val mapSideCombine: Boolean = false)
 extends Dependency[Product2[K, V]]

在这里插入图片描述

五、RDD阶段划分

DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向, 不会闭环。例如,DAG 记录了RDD 的转换过程和任务的阶段。

在这里插入图片描述

RDD 阶段划分源码
....

在这里插入图片描述

在这里插入图片描述

六、RDD 任务划分

RDD 任务切分中间分为:Application、Job、Stage 和 Task

  • Application:初始化一个 SparkContext 即生成一个Application;
  • Job:一个Action 算子就会生成一个Job;
  • Stage:Stage 等于宽依赖(ShuffleDependency)的个数加 1;
  • Task:一个 Stage 阶段中,最后一个RDD 的分区个数就是Task 的个数。

注意:Application->Job->Stage->Task 每一层都是 1 对 n 的关系。
在这里插入图片描述

RDD 任务划分源码
...
RDD持久化

在这里插入图片描述


/**
  * @Date 2021/5/1 16:50
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDPersist02 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Persist")
    val sc = new SparkContext(sparConf)

    val list = List("Hello Scala", "Hello Spark")

    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      println("☆☆☆☆☆")
      (word,1)
    })

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    reduceRDD.collect().foreach(println)

    println("**************************************")
    val groupRDD = mapRDD.groupByKey()
    groupRDD.collect().foreach(println)


    /** 结果说明:flatRDD.map()走了两遍
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * (Spark,1)
      * (Hello,2)
      * (Scala,1)
      * **************************************
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * (Spark,CompactBuffer(1))
      * (Hello,CompactBuffer(1, 1))
      * (Scala,CompactBuffer(1))
      */
    sc.stop()
  }
}

在这里插入图片描述

一、RDD Cache 缓存

RDD 通过Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算子时,该RDD 将会被缓存在计算节点的内存中,并供后面重用。

import org.apache.spark.rdd.RDD
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/5/1 16:56
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDPersist03 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
    val sc = new SparkContext(sparConf)

    val list = List("Hello Scala", "Hello Spark")

    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      println("☆☆☆☆☆")
      (word,1)
    })

    // cache默认持久化的操作,只能将数据保存到内存中,如果想要保存到磁盘文件,需要更改存储级别
    //mapRDD.cache()

    // 持久化操作必须在行动算子执行时完成的。
    mapRDD.persist(StorageLevel.DISK_ONLY)

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    reduceRDD.collect().foreach(println)
    println("**************************************")
    val groupRDD = mapRDD.groupByKey()
    groupRDD.collect().foreach(println)

    /**
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * (Spark,1)
      * (Hello,2)
      * (Scala,1)
      * **************************************
      * (Spark,CompactBuffer(1))
      * (Hello,CompactBuffer(1, 1))
      * (Scala,CompactBuffer(1))
      */

    sc.stop()
  }
}

在这里插入图片描述

缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除,RDD 的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD 的一系列转换,丢失的数据会被重算,由于RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可, 并不需要重算全部Partition。

Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作(比如:reduceByKey)。这样做的目的是为了当一个节点 Shuffle 失败了避免重新计算整个输入。但是,在实际使用的时候,如果想重用数据,仍然建议调用 persist 或 cache。

二、RDD CheckPoint 检查点

所谓的检查点其实就是通过将RDD 中间结果写入磁盘

由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。

对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发。

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/5/1 17:02
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDPersist04 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
    val sc = new SparkContext(sparConf)
    sc.setCheckpointDir("cp")

    val list = List("Hello Scala", "Hello Spark")

    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      println("☆☆☆☆☆")
      (word,1)
    })

    /**
      * checkpoint 需要落盘,需要指定检查点保存路径
      * 检查点路径保存的文件,当作业执行完毕后,不会被删除
      * 一般保存路径都是在分布式存储系统:HDFS
      */
    mapRDD.checkpoint()

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    reduceRDD.collect().foreach(println)
    println("**************************************")

    val groupRDD = mapRDD.groupByKey()
    groupRDD.collect().foreach(println)

    /**
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * ☆☆☆☆☆
      * (Spark,1)
      * (Hello,2)
      * (Scala,1)
      * **************************************
      * (Spark,CompactBuffer(1))
      * (Hello,CompactBuffer(1, 1))
      * (Scala,CompactBuffer(1))
      */
    sc.stop()
  }
}

在这里插入图片描述

三、缓存和检查点区别
  1. Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint 检查点切断血缘依赖。
  2. Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint 的数据通常存储在HDFS 等容错、高可用的文件系统,可靠性高。
  3. 建议对checkpoint()的RDD 使用Cache 缓存,这样 checkpoint 的 job 只需从 Cache 缓存中读取数据即可,否则需要再从头计算一次RDD。
/**
  * @Date 2021/5/1 17:08
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDPersist06 {
  def main(args: Array[String]): Unit = {
    /**
      * cache : 将数据临时存储在内存中进行数据重用
      *         会在血缘关系中添加新的依赖。一旦,出现问题,可以重头读取数据
      * persist : 将数据临时存储在磁盘文件中进行数据重用
      *           涉及到磁盘IO,性能较低,但是数据安全
      *           如果作业执行完毕,临时保存的数据文件就会丢失
      * checkpoint : 将数据长久地保存在磁盘文件中进行数据重用
      *           涉及到磁盘IO,性能较低,但是数据安全
      *           为了保证数据安全,所以一般情况下,会独立执行作业
      *           为了能够提高效率,一般情况下,是需要和cache联合使用
      *           执行过程中,会切断血缘关系。重新建立新的血缘关系
      *           checkpoint等同于改变数据源
      */

    val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
    val sc = new SparkContext(sparConf)
    sc.setCheckpointDir("cp")

    val list = List("Hello Scala", "Hello Spark")

    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
      (word,1)
    })

    //mapRDD.cache()
    mapRDD.checkpoint()
    println(mapRDD.toDebugString)

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    reduceRDD.collect().foreach(println)

    println("**************************************")
    println(mapRDD.toDebugString)

    /**
      * (1) MapPartitionsRDD[2] at map at RDDPersist06.scala:37 []
      * |  MapPartitionsRDD[1] at flatMap at RDDPersist06.scala:35 []
      * |  ParallelCollectionRDD[0] at makeRDD at RDDPersist06.scala:33 []
      * (Spark,1)
      * (Hello,2)
      * (Scala,1)
      * **************************************
      * (1) MapPartitionsRDD[2] at map at RDDPersist06.scala:37 []
      * |  ReliableCheckpointRDD[4] at collect at RDDPersist06.scala:46 []
      */
    sc.stop()
  }
}
RDD 分区器

Spark 目前支持Hash 分区和 Range 分区,和用户自定义分区。

Hash 分区为当前的默认分区。分区器直接决定了RDD 中分区的个数、RDD 中每条数据经过Shuffle 后进入哪个分区,进而决定了Reduce 的个数。

只有Key-Value 类型的RDD 才有分区器,非 Key-Value 类型的RDD 分区的值是 None

每个RDD 的分区 ID 范围:0 ~ (numPartitions - 1),决定这个值是属于那个分区的。

一、Hash 分区

对于给定的 key,计算其hashCode,并除以分区个数取余

二、Range 分区

将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序

三、自定义分区
/**
  * @Date 2021/5/1 17:15
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDPartitioner01 {
  def main(args: Array[String]): Unit = {

    val sparConf = new SparkConf().setMaster("local").setAppName("WordCount")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List(
      ("nba", "xxxxxxxxx"),
      ("cba", "xxxxxxxxx"),
      ("wnba", "xxxxxxxxx"),
      ("nba", "xxxxxxxxx"),
    ),3)

    val partRDD: RDD[(String, String)] = rdd.partitionBy( new MyPartitioner )

    partRDD.saveAsTextFile("output")

    sc.stop()
  }
}

/**
  * 自定义分区器
  * 1. 继承Partitioner
  * 2. 重写方法
  */
class MyPartitioner extends Partitioner {
  // 分区数量
  override def numPartitions: Int = 3

  // 根据数据的key值返回数据所在的分区索引(从0开始)
  override def getPartition(key: Any): Int = {
    key match {
      case "nba" => 0
      case "cnba" => 1
      case _ => 2
    }
  }
}
RDD 文件读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。

  1. 文件格式分为:text 文件、csv 文件、sequence 文件以及Object 文件;
    1. SequenceFile 文件是Hadoop 用来存储二进制形式的key-value 对而设计的一种平面文件(Flat File)。在 SparkContext 中,可以调用sequenceFile[keyClass, valueClass](path)。
    2. 对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。可以通过objectFile[T: ClassTag](path)函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。
  2. 文件系统分为:本地文件系统、HDFS、HBASE 以及数据库。
一、保存save
/**
  * @Date 2021/5/1 20:05
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDIOSave {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("RDDIOSave")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(
      List(
        ("a", 1),
        ("b", 2),
        ("c", 3)
      )
    )

    rdd.saveAsTextFile("output1")
    rdd.saveAsObjectFile("output2")
    rdd.saveAsSequenceFile("output3")

    sc.stop()
  }
}
二、读取load
/**
  * @Date 2021/5/1 20:04
  * @Version 10.21
  * @Author DuanChaojie
  */
object RDDIOLoad {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local").setAppName("RDDIOLoad")
    val sc = new SparkContext(sparConf)

    val rdd = sc.textFile("output1")
    println(rdd.collect().mkString(","))

    val rdd1 = sc.objectFile[(String, Int)]("output2")
    println(rdd1.collect().mkString(","))

    val rdd2 = sc.sequenceFile[String, Int]("output3")
    println(rdd2.collect().mkString(","))

    sc.stop()
  }
}

在这里插入图片描述

1. 累加器

累加器用来把Executor 端变量信息聚合到Driver 端。在Driver 程序中定义的变量,在Executor 端的每个Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后, 传回Driver 端进行 merge。
在这里插入图片描述

/**
  * @Date 2021/5/5 16:56
  * @Version 10.21
  * @Author DuanChaojie
  */
object Accumulator01 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Accumulator")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List(1, 2, 3, 4))

    /**
      * reduce : 分区内计算,分区间计算
      * val i: Int = rdd.reduce(_+_)
      * println(i)
      */
    var sum = 0
    rdd.foreach(
      num => {
        sum += num
      }
    )

    // sum = 0
    println("sum = " + sum)

    sc.stop()
  }
}

使用累加器

/**
  * @Date 2021/5/5 16:59
  * @Version 10.21
  * @Author DuanChaojie
  */
object Accumulator02 {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("Accumulator")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List(1,2,3,4))

    /**
      * 获取系统累加器
      * Spark默认就提供了简单数据聚合的累加器
      * 如:
      * sc.doubleAccumulator
      * sc.collectionAccumulator
      */
    val sumAcc = sc.longAccumulator("sum")

    rdd.foreach(
      num => {
        // 使用累加器
        sumAcc.add(num)
      }
    )

    // 获取累加器的值
    println(sumAcc.value)

    sc.stop()
  }
}

使用累加器注意点:

package com.atguigu.sparkstudy.core.acc

import org.apache.spark.{SparkConf, SparkContext}

/**
  * @Date 2021/5/5 17:01
  * @Version 10.21
  * @Author DuanChaojie
  */
object Accumulator03 {
  def main(args: Array[String]): Unit = {

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

    val rdd = sc.makeRDD(List(1,2,3,4))

    /**
      * 获取系统累加器
      * Spark默认就提供了简单数据聚合的累加器
      *
      * sc.doubleAccumulator
      * sc.collectionAccumulator
      */
    val sumAcc = sc.longAccumulator("sum")


    val mapRDD = rdd.map(
      num => {
        // 使用累加器
        sumAcc.add(num)
        num
      }
    )

    /**
      * 获取累加器的值
      *  少加:转换算子中调用累加器,如果没有行动算子的话,那么不会执行
      *  多加:转换算子中调用累加器,如果没有行动算子的话,那么不会执行
      *  一般情况下,累加器会放置在行动算子进行操作
      */
    mapRDD.collect()
    mapRDD.collect()
    println(sumAcc.value)

    sc.stop()
  }
}
自定义累加器
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2

import scala.collection.mutable

/**
  * @Date 2021/5/5 19:06
  * @Version 10.21
  * @Author DuanChaojie
  */
object Accumulator04WordCount {
  def main(args: Array[String]): Unit = {
    val sparConf = new SparkConf().setMaster("local[*]").setAppName("AccumulatorWordCount")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List("hello", "spark", "hello"))

    /**
      * 累加器 : WordCount
      * 创建累加器对象
      */
    val wcAcc = new MyAccumulator()

    // 向Spark进行注册
    sc.register(wcAcc, "wordCountAcc")

    rdd.foreach(
      word => {
        // 数据的累加(使用累加器)
        wcAcc.add(word)
      }
    )

    // 获取累加器累加的结果:Map(spark -> 1, hello -> 2)
    println(wcAcc.value)

    sc.stop()
  }
}


/**
  * 自定义数据累加器:WordCount
  * 1. 继承AccumulatorV2, 定义泛型
  * IN : 累加器输入的数据类型 String
  * OUT : 累加器返回的数据类型 mutable.Map[String, Long]
  * 2. 重写方法(6)
  */
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Long]] {

  private var wcMap = mutable.Map[String, Long]()

  /**
    * 判断是否初始状态
    *
    * @return
    */
  override def isZero: Boolean = {
    wcMap.isEmpty
  }

  override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
    new MyAccumulator()
  }

  override def reset(): Unit = {
    wcMap.clear()
  }

  /**
    * 获取累加器需要计算的值
    *
    * @param word
    */
  override def add(word: String): Unit = {
    val newCount = wcMap.getOrElse(word, 0L) + 1
    wcMap.update(word, newCount)
  }

  /**
    * Driver合并多个累加器
    *
    * @param other
    */
  override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {
    val map1 = this.wcMap
    val map2 = other.value

    map2.foreach {
      case (word, count) => {
        val newCount = map1.getOrElse(word, 0L) + count
        map1.update(word, newCount)
      }
    }
  }

  /**
    * 累加器结果:
    *
    * @return
    */
  override def value: mutable.Map[String, Long] = {
    wcMap
  }
}

3. 广播变量

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表, 广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark 会为每个任务分别发送。

/**
  * @Date 2021/5/5 19:34
  * @Version 10.21
  * @Author DuanChaojie
  */
object Broadcast01 {
  def main(args: Array[String]): Unit = {

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

    val rdd1 = sc.makeRDD(List(
      ("a", 1),("b", 2),("c", 3)
    ))

    //        val rdd2 = sc.makeRDD(List(
    //            ("a", 4),("b", 5),("c", 6)
    //        ))

    val map = mutable.Map(("a", 4),("b", 5),("c", 6))


    /**
      * join会导致数据量几何增长,并且会影响shuffle的性能,不推荐使用
      * val joinRDD: RDD[(String, (Int, Int))] = rdd1.join(rdd2)
      * joinRDD.collect().foreach(println)
      *
      * (a, 1),    (b, 2),    (c, 3)
      * (a, (1,4)),(b, (2,5)),(c, (3,6))
      */
    rdd1.map {
      case (w, c) => {
        val l: Int = map.getOrElse(w, 0)
        (w, (c, l))
      }
    }.collect().foreach(println)



    sc.stop()
  }
}

使用广播变量
在这里插入图片描述


import org.apache.spark.broadcast.Broadcast
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable

/**
  * @Date 2021/5/5 19:38
  * @Version 10.21
  * @Author DuanChaojie
  */
object Broadcast02 {
  def main(args: Array[String]): Unit = {

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

    val rdd1 = sc.makeRDD(List(
      ("a", 1),("b", 2),("c", 3)
    ))

    val map = mutable.Map(("a", 4),("b", 5),("c", 6))

    // 封装广播变量
    val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)

    rdd1.map {
      case (w, c) => {
        // 方法广播变量
        val l: Int = bc.value.getOrElse(w, 0)
        (w, (c, l))
      }
    }.collect().foreach(println)

    sc.stop()
  }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱敲代码的小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值