spark_core小结

441 阅读22分钟

1. RDD概述

1.1 什么是RDD?

RDD (Resilient Distributed Dataset ) : 弹性分布式数据集 ,是spark中最基本的数据抽象。

  1. 弹性

    存储:内存与磁盘的动态切换

    计算:计算出错重试机制

    容错:数据丢失可以自动恢复

    分片:可根据需要从新分片

  2. 分布式

    数据存储在大数据集群不同节点上

  3. 数据集

    RDD封装了数据集,并不保存数据

  4. 数据抽象

    RDD是个抽象类,需要子类具体实现

  5. 不可变

    RDD封装了计算逻辑,是不可变的,需要改变,只能通过新的RDD,在新的RDD里面封装新的计算逻辑

    data => RDD(计算逻辑)=》rdd(新的计算逻辑)

  6. 可分区并行计算

 * Internally, each RDD is characterized by five main properties:
 *
 *  - A list of partitions
 *  - A function for computing each split
 *  - A list of dependencies on other RDDs
 *  - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
 *  - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
 *    an HDFS file)

2. RDD创建

2.1 从集合中创建RDD

  • spark主要提供2种函数,parallelize和makeRDD

    // parallelize函数
    val rdd: RDD[Int] = sc.parallelize(Array(1, 2, 3, 4, 5, 6, 7, 8))
    //makeRDD函数
    val rdd1: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5, 6, 7, 8))
    
  • makeRdd底层依然调用了parallelize方法

    /*
    defaultParallelism: 创建RDD没有指定分区的时候,默认就是当前系统核数就是分区数
      override def defaultParallelism(): Int =
        scheduler.conf.getInt("spark.default.parallelism", totalCores)
    */
    def makeRDD[T: ClassTag](
          seq: Seq[T],
          numSlices: Int = defaultParallelism): RDD[T] = withScope {
        parallelize(seq, numSlices)
      }
    
  
  

## 2.2 从外部存储系统的数据集创建

- 本地文件系统的数据

  ```scala
  var fileRdd: RDD[String] = sc.textFile("input")
  println(fileRdd.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).collect().mkString(","))
  • Hadoop支持的数据集,包括hdfs和hbase

    var hdfsRdd = sc.textFile("hdfs://mayi101:9000/wc/input/wc.txt")
    println(hdfsRdd.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).collect().mkString(","))
    

2.3 从其它RDD 创建

3. 转换算子以及分区规则

3.1 默认分区源码(RDD数据从集合中创建)

分区源码解读:

3.2 分区源码(指定分区,RDD数据从集合中创建)

//指定3个分区
val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5), 3)

分区源码解读:

3.3 分区源码(从文件读取后创建)

源码

  def textFile(
      path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
        assertNotStopped()
        hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
        minPartitions).map(pair => pair._2.toString).setName(path)
  }

//默认的最小分区 
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
//defaultParallelism默认就是CPU的核数
override def defaultParallelism(): Int =
    scheduler.conf.getInt("spark.default.parallelism", totalCores)
//获取切片分区规划
override def getPartitions: Array[Partition] = {
    val jobConf = getJobConf()
    // add the credentials here as this can be called before SparkContext initialized
    SparkHadoopUtil.get.addCredentials(jobConf)
    val inputFormat = getInputFormat(jobConf)
    val inputSplits = inputFormat.getSplits(jobConf, minPartitions)
    val array = new Array[Partition](inputSplits.size)
    for (i <- 0 until inputSplits.size) {
      array(i) = new HadoopPartition(id, i, inputSplits(i))
    }
    array
  }
//注意:getSplits文件返回的是切片规划,真正读取是在compute方法中创建LineRecordReader读取的,有两个关键变量
//start=split.getStart()	   
//end = start + split.getLength

long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
//切片大小
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
      FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
long blockSize = file.getBlockSize();
//最终的切片大小
  protected long computeSplitSize(long goalSize, long minSize, long blockSize) {
    return Math.max(minSize, Math.min(goalSize, blockSize));
  }
/** 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 {
          if (LOG.isDebugEnabled()) {
            // Log only if the file is big enough to be splitted
            if (length > Math.min(file.getBlockSize(), minSize)) {
              LOG.debug("File is not splittable so no parallelization "
                  + "is possible: " + file.getPath());
            }
          }
          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()]);
  }

总结:

  1. 默认分区规则:

    -从集合中创建RDD

    ​ 取决于分配给应用的CPU的核数

    -读取外部文件创建RDD

    ​ math.min(取决于分配给应用的CPU的核数,2)

  2. 从集合中创建分区,指定分区数

    • 指定分区数
  3. 从文件中创建分区,指定的是最小分区数

    *   -默认分区规则
    *       math.min(分配给应用的CPU核数,2)
    *   -指定分区
    *     >1.在textFile方法中,第二个参数minPartitions,表示最小分区数
    *       注意:是最小,不是实际的分区个数
    *     >2.在实际计算分区个数的时候,**会根据文件的总大小和最小分区数进行相除运算**
    *       &如果余数为0
    *         那么最小分区数,就是最终实际的分区数
    *       &如果余数不为0
    *         那么实际的分区数,要计算
    
    # 说明 
    	1.切片采用的是Hadoop的切片规则
    	2.RecoderReader记录读取器,默认也是按行读取,所以下面的例子,虽然切片规划是这样的,但是分区中读取的数据略微有点不一样
    
    *
    *   -原始数据
    *       0 	1 	2 	3 	4 	5   6 	7 	8
    *       a	b 	c	d 	e 	f 	g 	X	X
    *
    *       9 	10 	11	12 	13	14
    *       h 	i 	j 	k	X 	X
    *
    *       15 	16 	17 	18	19
    *       l 	m 	n 	X 	X
    *
    *       20 	21
    *       o 	p
    *
    *       设置最小切片数为3
    *
    *
    *       切片规划 FileInputFormat->getSplits
    *       0 = {FileSplit@5103} "file:/D:/dev/workspace/bigdata-0105/spark-0105/input/test.txt:0+7"
    *       1 = {FileSplit@5141} "file:/D:/dev/workspace/bigdata-0105/spark-0105/input/test.txt:7+7"
    *       2 = {FileSplit@5181} "file:/D:/dev/workspace/bigdata-0105/spark-0105/input/test.txt:14+7"
    *       3 = {FileSplit@5237} "file:/D:/dev/workspace/bigdata-0105/spark-0105/input/test.txt:21+1"
    *
    *
    *       最终分区数据
    *       0 分区
    *       abcdefg
    *       1 分区
    *       hijk
    *       2 分区
    *       lmn
    *       op
    *       3 分区
    *

    切片源码:

    // totalSize:文件大小,numSplits指定的最小分区数
    //计算每个分区放入的数据大小goalSize
    long goalSize = totalSize / (long)(numSplits == 0 ? 1 : numSplits);
    //分区的最小大小,这个我们可以自己指定
    long minSize = Math.max(job.getLong("mapreduce.input.fileinputformat.split.minsize", 1L), 			   this.minSplitSize);
    //块大小
    long blockSize = file.getBlockSize();
    
    //正在计算切片的大小
    long splitSize = this.computeSplitSize(goalSize, minSize, blockSize);
    //计算切片大小的算法
    protected long computeSplitSize(long goalSize, long minSize, long blockSize) {
            return Math.max(minSize, Math.min(goalSize, 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;
    }
    
    //通过文件,存储系统创建RDD
    sc.textFile()
    //Hadoop 获取之前的分区规划信息,创建分区,从这里也能看到一个切片一个分区
    override def getPartitions: Array[Partition] = {
        val jobConf = getJobConf()
        // add the credentials here as this can be called before SparkContext initialized
        SparkHadoopUtil.get.addCredentials(jobConf)
        val inputFormat = getInputFormat(jobConf)
        //这个就是上面说的获取的切片规划信息
        val inputSplits = inputFormat.getSplits(jobConf, minPartitions)
        val array = new Array[Partition](inputSplits.size)
        for (i <- 0 until inputSplits.size) {
          //创建分区
          array(i) = new HadoopPartition(id, i, inputSplits(i))
        }
        array
      }
    
    //分区计算逻辑,知道分区中有哪些东西,
    override def compute(theSplit: Partition, context: TaskContext): InterruptibleIterator[(K, V)] = {
      private def updateBytesRead(): Unit = {
            getBytesReadCallback.foreach { getBytesRead =>
              inputMetrics.setBytesRead(existingBytesRead + getBytesRead())
            }
          }
    		
      		//记录读取器
          private var reader: RecordReader[K, V] = null
          private val inputFormat = getInputFormat(jobConf)
          HadoopRDD.addLocalConfiguration(
            new SimpleDateFormat("yyyyMMddHHmmss", Locale.US).format(createTime),
            context.stageId, theSplit.index, context.attemptNumber, jobConf)
    			
          reader =
            try {
              //获取recorderReader
              inputFormat.getRecordReader(split.inputSplit.value, jobConf, Reporter.NULL)
            } catch {
              case e: IOException if ignoreCorruptFiles =>
                logWarning(s"Skipped the rest content in the corrupted file: ${split.inputSplit}", e)
                finished = true
                null
            }
    }
    
    

    练习:

    1.一份文件a,内容如下
    0	 1  2  3  4
    a  b  c  X  X
    
    5  6  7  8
    e  f  X  X
    
    9 10 11 
    g  X  X
    
    12 13 14 15
    h  j  X  X
    
    16 17 18
    k  l  m
    
    假设设置最小分区数是5,那么,实际分区是多少,每个分区的内容是什么?
    
    答案:
    //实际分区是:
    		实际分区应该是7//分析
    //a.当前的a文件总大小19
    每一行,默认是有换行符+回车符(最后一行没有这两个),也就是相当于每行多2个字节,所以上面文件a,应该是11+8=19
    
    goalSize= totalSize/numSplites  => 19/5 = 3
    blockSize:默认32M
    minSize=1
    根据算法:Math.max(minSize,Math.min(goalSize,blockSize))=>splitSize= 3
    //b.获取到splitSize3,规划分区就可以开始计算了
    最小分区是5,
    totalSize - minPartition*splitSize = 4 //还剩下4个长度内容没有规划切片分区
    4/3 = 1 ,因为有余数,所以应该是1 + 1=2个分区
    ===》经过上面的推导,共计5+2 = 7个分区。
    
    //分区内容如下:
    0号分区:0+3						
    abc
    1号分区:3+3			
    ef
    2号分区:6+3		
    g
    3号分区:9+3		
    hj
    4号分区:12+3   
    klm
    5号分区:15+36号分区:18+32.
    abcde
    最小分区数是3,实际分区是多少?
    
    3.
    有一个文本大小37个字节,最小分区数是2,实际分区数是多少?
    
    

4.1 Value类型

4.1.1 map映射

分析图

具体实现

object value01_map {
    def main(args: Array[String]): Unit = {
        //1.创建SparkConf并设置App名称
        val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc = new SparkContext(conf)
        //3具体业务逻辑
        // 3.1 创建一个RDD
        val rdd: RDD[Int] = sc.makeRDD(1 to 4,2)
        // 3.2 调用map方法,每个元素乘以2
        val mapRdd: RDD[Int] = rdd.map(_ * 2)
        // 3.3 打印修改后的RDD中数据
        mapRdd.collect().foreach(println)
        //4.关闭连接
        sc.stop()
    }
}

4.1.2 mapPartitions以分区为单位执行map

mapPartitions算子

//以分区为单位,对RDD中的元素进行映射
//一般适用于批处理的操作,比如:将RDD中的元素插入到数据库中,需要数据库连接,
// 如果每一个元素都创建一个连接,效率很低;可以对每个分区的元素,创建一个连接

分析图

案例

val rdd: RDD[Int] = sc.makeRDD(1 to 4, 2)
// 3.2 调用mapPartitions方法,每个元素乘以2
val rdd1 = rdd.mapPartitions(x=>x.map(_*2))
// 3.3 打印修改后的RDD中数据
rdd1.collect().foreach(println)

4.1.3 map与mapPartitions的区别

4.1.4 mapPartitionsWithIndex 带分区号

//3具体业务逻辑
// 3.1 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(1 to 4, 2)
// 3.2 创建一个RDD,使每个元素跟所在分区号形成一个元组,组成一个新的RDD
val indexRdd = rdd.mapPartitionsWithIndex( (index,items)=>{items.map( (index,_) )} )
//扩展功能:第二个分区元素*2,其余分区不变
// 3.3 打印修改后的RDD中数据
indexRdd.collect().foreach(println)

map相关(个人理解):

无论是map,mapPartition,还是mapPartitionsWithIndex(),经过参数中定义的方法映射处理以后,最后的返回结果都是一个iterator迭代器,经过map的时候,map每次处理一个元素时,都会将处理后的元素放到一个迭代器中,当遍历结束后,迭代器就是最终的结果。

4.1.5 flatmap 压平

个人理解:

flatmap偏平化的处理,适合于所有集合里面的元素,每个元素还是集合,然后将子元素集合中所有的元素全部放到一个集合中,让子集合中的元素时单纯的元素,不在是集合。相当于敲破所有子元素集合,把子元素集合的元素弄出来。

原理图

实例

//3具体业务逻辑
// 3.1 创建一个RDD
val listRDD=sc.makeRDD(List(List(1,2),List(3,4),List(5,6),List(7)), 2)
// 3.2 把所有子集合中数据取出放入到一个大的集合中
listRDD.flatMap(list=>list).collect.foreach(println)

4.1.6 glom分区转换数组

原理图

实例

//3具体业务逻辑
// 3.1 创建一个RDD
val rdd = sc.makeRDD(1 to 4, 2)
// 3.2 求出每个分区的最大值  0->1,2   1->3,4
val maxRdd: RDD[Int] = rdd.glom().map(_.max)
// 3.3 求出所有分区的最大值的和 2 + 4
println(maxRdd.collect().sum)

4.1.7 groupBy分组

个人理解

groupBy:按照传入函数的返回值进行分组,groupBy的参数,相当于是定义一个分组规则

//    val rdd: RDD[Int] = sc.makeRDD(1 to 10, 3)
//    println(rdd.groupBy(_%2).collect().mkString(","))
//    (0,CompactBuffer(2, 4, 6, 8, 10)),(1,CompactBuffer(1, 3, 5, 7, 9))

    val rdd = sc.makeRDD(Array("江西 南昌 上饶", "河北 石家庄 大同 上饶","南昌 大同 上饶"),3)
    val flatMapRdd = rdd.flatMap(_.split(" ")).map((_,1))
//    val str: String = flatMapRdd.groupBy(_.contains("上饶")).collect().mkString(",")
    val str: String = flatMapRdd.groupBy(_._1).collect().mkString(",")
/*
(河北,CompactBuffer((河北,1))),(石家庄,CompactBuffer((石家庄,1))),(南昌,CompactBuffer((南昌,1), (南昌,1))),(江西,CompactBuffer((江西,1))),(上饶,CompactBuffer((上饶,1), (上饶,1), (上饶,1))),(大同,CompactBuffer((大同,1), (大同,1)))
*/

原理图

实例

// 3.1 创建一个RDD
val rdd = sc.makeRDD(1 to 4, 2)
// 3.2 将每个分区的数据放到一个数组并收集到Driver端打印
rdd.groupBy(_ % 2).collect().foreach(println)
// 3.3 创建一个RDD
val rdd1: RDD[String] = sc.makeRDD(List("hello","hive","hadoop","spark","scala"))
// 3.4 按照首字母第一个单词相同分组
rdd1.groupBy(str=>str.substring(0,1)).collect().foreach(println)

**备注:**groupBy会存在shuffle的过程

shuffle:将不同的分区数据打乱重组的过程,shuffle一定会落盘

4.1.8 groupBy之wordCount

// 3.1 创建一个RDD
val strList: List[String] = List("Hello Scala", "Hello Spark", "Hello World")
val rdd = sc.makeRDD(strList)
// 3.2 将字符串拆分成一个一个的单词
val wordRdd: RDD[String] = rdd.flatMap(str=>str.split(" "))
// 3.3 将单词结果进行转换:word=>(word,1)
val wordToOneRdd: RDD[(String, Int)] = wordRdd.map(word=>(word, 1))
// 3.4 将转换结构后的数据分组
val groupRdd: RDD[(String, Iterable[(String, Int)])] = wordToOneRdd.groupBy(t=>t._1)
// 3.5 将分组后的数据进行结构的转换
val wordToSum: RDD[(String, Int)] = groupRdd.map {
    case (word, list) => {
        (word, list.size)
    }
}

wordToSum.collect().foreach(println)

复杂版的wordCount

val rdd: RDD[(String, Int)] = sc.makeRDD(List(("Hello Scala, 2), ("Hello Spark, 3), (Hello World, 2)))

//以下方式是比较好的一种实现方式
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("Hello Scala", 2), ("Hello Spark", 3), ("Hello World", 2)))
//对RDD中的元素进行扁平映射
val flatMapRDD: RDD[(String, Int)] = rdd.flatMap {
  case (words, count) => {
    words.split(" ").map(word => (word, count))
  }
}
//按照单词对RDD中的元素进行分组     (Hello,CompactBuffer((Hello,2), (Hello,3), (Hello,2)))
val groupByRDD: RDD[(String, Iterable[(String, Int)])] = flatMapRDD.groupBy(_._1)
//对RDD的元素重新进行映射
val resRDD: RDD[(String, Int)] = groupByRDD.map {
  case (word, datas) => {
    (word, datas.map(_._2).sum)
  }
}

resRDD.collect().foreach(println)

4.1.9 filter 算子

个人理解

filter其实和groupBy是类似的,groupBy是按照参数中的函数规则,对元素进行分类,filter也是利用每个元素,和定义的方法规则,如果满足规则,也就是true,这些元素将会留下,放进新的RDD中,否则丢弃不满足条件的数据

原理图

实例:

val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4),2)
//过滤出符合条件的数据
val filterRdd: RDD[Int] = rdd.filter(_ % 2 == 0)

4.1.10 sample 采样 ?

原理图

实例

// 3.1 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(1 to 10)
// 3.2 打印放回抽样结果
rdd.sample(true, 0.4, 2).collect().foreach(println)
// 3.3 打印不放回抽样结果
rdd.sample(false, 0.2, 3).collect().foreach(println)

扩展小结:

object Transformation_sample {
  def main(args: Array[String]): Unit = {
    //创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

    //创建SparkContext,该对象是提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)


    //val rdd: RDD[Int] = sc.makeRDD(1 to 10)

    /*
      -withReplacement  是否抽样放回
        true    抽样放回
        false   抽样不放回
      -fraction
        withReplacement=true   表示期望每一个元素出现的次数  >0
        withReplacement=false  表示RDD中每一个元素出现的概率[0,1]
      -seed 抽样算法初始值
        一般不需要指定
    */
    //从rdd中随机抽取一些数据(抽样放回)
    //val newRDD: RDD[Int] = rdd.sample(true,3)


    //从rdd中随机抽取一些数据(抽样不放回)
    //val newRDD: RDD[Int] = rdd.sample(false,0.6)
    //newRDD.collect().foreach(println)


    val stds: List[String] = List("张三""李四","王五")

    val nameRDD: RDD[String] = sc.makeRDD(stds)

    //从上面RDD中抽取一名幸运同学进行连麦

    val luckyMan: Array[String] = nameRDD.takeSample(false,1)

    luckyMan.foreach(println)


    // 关闭连接
    sc.stop()
  }
}

4.1.11 distinct 算子

源码

  def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    map(x => (x, null)).reduceByKey((x, y) => x, numPartitions).map(_._1)
  }

原理图

实例

// 3.1 创建一个RDD
val distinctRdd: RDD[Int] = sc.makeRDD(List(1,2,1,5,2,9,6,1))
// 3.2 打印去重后生成的新RDD
distinctRdd.distinct().collect().foreach(println)
// 3.3 对RDD采用多个Task去重,提高并发度
distinctRdd.distinct(2).collect().foreach(println)

4.1.12 coalesce 重新分区

源码:

def coalesce(numPartitions: Int, shuffle: Boolean = false,
             partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
            (implicit ord: Ordering[T] = null)
    : RDD[T] = withScope {
  require(numPartitions > 0, s"Number of partitions ($numPartitions) must be positive.")
  //默认是不执行shuffle,一般用于缩减分区
  if (shuffle) {
    /** Distributes elements evenly across output partitions, starting from a random partition. */
    val distributePartition = (index: Int, items: Iterator[T]) => {
      var position = (new Random(index)).nextInt(numPartitions)
      items.map { t =>
        // Note that the hash code of the key will just be the key itself. The HashPartitioner
        // will mod it with the number of total partitions.
        position = position + 1
        (position, t)
      }
    } : Iterator[(Int, T)]

    // include a shuffle step so that our upstream tasks are still distributed
    new CoalescedRDD(
      new ShuffledRDD[Int, T, T](mapPartitionsWithIndex(distributePartition),
      new HashPartitioner(numPartitions)),
      numPartitions,
      partitionCoalescer).values
  } else {
    new CoalescedRDD(this, numPartitions, partitionCoalescer)
  }
}

小结:

  • -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] = numRDD.coalesce(2)

//扩大分区 //注意:默认情况下,如果使用coalesce扩大分区是不起作用的 。因为底层没有执行shuffle //val newRDD: RDD[Int] = numRDD.coalesce(4)

Coalesce算子包括:配置执行Shuffle和配置不执行Shuffle两种方式。

4.1.12.1 不执行shuffle方式

//3.创建一个RDD
//val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4), 4)
//3.1 缩减分区
//val coalesceRdd: RDD[Int] = rdd.coalesce(2)
//4. 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5, 6), 3)
//4.1 缩减分区
val coalesceRdd: RDD[Int] = rdd.coalesce(2)

4.1.12.2 执行shuffle方式

//3. 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5, 6), 3)
//3.1 执行shuffle
val coalesceRdd: RDD[Int] = rdd.coalesce(2, true)

4.1.12.3 shuffle

4.1.13 repartition 重新分区

//3. 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5, 6), 3)
//3.1 缩减分区
//val coalesceRdd: RDD[Int] = rdd.coalesce(2,true)
//3.2 重新分区
val repartitionRdd: RDD[Int] = rdd.repartition(2)

4.1.14 coalesce和repairtition的区别

1)coalesce重新分区,可以选择是否进行shuffle过程。由参数shuffle: Boolean = false/true决定。

2)repartition实际上是调用的coalesce,进行shuffle。源码如下:

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
}

3)coalesce一般为缩减分区,如果扩大分区,不使用shuffle是没有意义的,repartition扩大分区执行shuffle。

4.1.15 sortBy排序

原理图

sortByKey:

按照k做排序,默认是正序,true-正序,false-倒叙

利用sortByKey倒序

val rdd = sc.makeRDD(List("java is best php is scala is best language"))
val tuples = rdd.flatMap(x => {
  x.split(" ").map((_, 1))
}).groupBy(_._1).map({
  case (index, datas) => {
    (index, datas.size)
  }
}).map(x=>(x._2,x._1)).sortByKey(false)

实例

//3具体业务逻辑
// 3.1 创建一个RDD
val rdd: RDD[Int] = sc.makeRDD(List(2, 1, 3, 4, 6, 5))
// 3.2 默认是升序排
val sortRdd: RDD[Int] = rdd.sortBy(num => num)
sortRdd.collect().foreach(println)
// 3.3 配置为倒序排
val sortRdd2: RDD[Int] = rdd.sortBy(num => num, false)
sortRdd2.collect().foreach(println)
// 3.4 创建一个RDD
val strRdd: RDD[String] = sc.makeRDD(List("1", "22", "12", "2", "3"))
// 3.5 按照字符的int值排序
strRdd.sortBy(num => num.toInt).collect().foreach(println)
// 3.5 创建一个RDD
val rdd3: RDD[(Int, Int)] = sc.makeRDD(List((2, 1), (1, 2), (1, 1), (2, 2)))
// 3.6 先按照tuple的第一个值排序,相等再按照第2个值排
rdd3.sortBy(t=>t).collect().foreach(println)

4.1.16 pipe调用脚本

4.2 双Value类型交互

4.2.1 union 并集

//3.1 创建第一个RDD
val rdd1: RDD[Int] = sc.makeRDD(1 to 4)
//3.2 创建第二个RDD
val rdd2: RDD[Int] = sc.makeRDD(4 to 8)
//3.3 计算两个RDD的并集
rdd1.union(rdd2).collect().foreach(println)

4.2.2 subtract 差集

//3.1 创建第一个RDD
val rdd: RDD[Int] = sc.makeRDD(1 to 4)
//3.2 创建第二个RDD
val rdd1: RDD[Int] = sc.makeRDD(4 to 8)
//3.3 计算第一个RDD与第二个RDD的差集并打印
rdd.subtract(rdd1).collect().foreach(println)

4.2.3 intersection 交集

//3.1 创建第一个RDD
val rdd1: RDD[Int] = sc.makeRDD(1 to 4)
//3.2 创建第二个RDD
val rdd2: RDD[Int] = sc.makeRDD(4 to 8)
//3.3 计算第一个RDD与第二个RDD的差集并打印
rdd1.intersection(rdd2).collect().foreach(println)

4.2.4 zip拉链

//3.1 创建第一个RDD
val rdd1: RDD[Int] = sc.makeRDD(Array(1,2,3),3)
//3.2 创建第二个RDD
val rdd2: RDD[String] = sc.makeRDD(Array("a","b","c"),3)
//3.3 第一个RDD组合第二个RDD并打印
rdd1.zip(rdd2).collect().foreach(println)
//3.4 第二个RDD组合第一个RDD并打印
rdd2.zip(rdd1).collect().foreach(println)
//3.5 创建第三个RDD(与1,2分区数不同)
val rdd3: RDD[String] = sc.makeRDD(Array("a","b"),3)
//3.6 元素个数不同,不能拉链
// Can only zip RDDs with same number of elements in each partition
rdd1.zip(rdd3).collect().foreach(println)
//3.7 创建第四个RDD(与1,2分区数不同)
val rdd4: RDD[String] = sc.makeRDD(Array("a","b","c"),2)
//3.8 分区数不同,不能拉链
// Can't zip RDDs with unequal numbers of partitions: List(3, 2)
rdd1.zip(rdd4).collect().foreach(println)

备注:

​ 元素个数不同,不能拉链

​ 分区数不同,不能拉链

小结:

val rdd = sc.makeRDD(1 to 4)
val rdd1 = sc.makeRDD(3 to 6)
//    println(rdd.union(rdd1).collect().mkString(",")) //并集  2个RDD所有元素
//    println(rdd.subtract(rdd1).collect().mkString(",")) //差集 rdd-rdd1,rdd剩余元素
//    println(rdd.intersection(rdd1).collect().mkString(",")) //交集 rdd与rdd1都有的元素
println(rdd.zip(rdd1).collect().mkString(",")) //拉链 组成(k,v)

4.3 key-value类型

4.3.1 partitionBy 按k重新分区

0)理解:

默认分区器是HashPartitioner,自定义的分区器,需要继承Partitioner,重写getPartition方法,该方法就是定义key按照里面的规则分配到指定的分区,该方法返回值就是分区号,也就是对应key分配的分区。

1)原理图

2)实例

val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1,"aaa"),(2,"bbb"),(3,"ccc")),3)
//3.2 对RDD重新分区
val rdd2: RDD[(Int, String)] = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))
//3.3 查看新RDD的分区数
println(rdd2.partitions.size)

3)HashPartitioner源码解读:

class HashPartitioner(partitions: Int) extends Partitioner {
    require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")
    
    def numPartitions: Int = partitions
    
    def getPartition(key: Any): Int = key match {
        case null => 0
        case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
    }
    
    override def equals(other: Any): Boolean = other match {
        case h: HashPartitioner =>
            h.numPartitions == numPartitions
        case _ =>
            false
    }
    
    override def hashCode: Int = numPartitions
}

4) 自定义分区器

object KeyValue01_partitionBy {

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

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        //3具体业务逻辑
        //3.1 创建第一个RDD
        val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "aaa"), (2, "bbb"), (3, "ccc")), 3)
        //3.2 自定义分区
        val rdd3: RDD[(Int, String)] = rdd.partitionBy(new MyPartitioner(2))

        //4 打印查看对应分区数据
        val indexRdd: RDD[(Int, String)] = rdd3.mapPartitionsWithIndex(
            (index, datas) => {
                // 打印每个分区数据,并带分区号
                datas.foreach(data => {
                    println(index + "=>" + data)
                })
                // 返回分区的数据
                datas
            }
        )

        indexRdd.collect()

        //5.关闭连接
        sc.stop()
    }
}

// 自定义分区
class MyPartitioner(num: Int) extends Partitioner {
    // 设置的分区数
    override def numPartitions: Int = num

    // 具体分区逻辑
    override def getPartition(key: Any): Int = {

        if (key.isInstanceOf[Int]) {

            val keyInt: Int = key.asInstanceOf[Int]
            if (keyInt % 2 == 0)
                0
            else
                1
        }else{
            0
        }
    }
}

4.3.2 reduceByKey 按K重新分区

原理图

看源码可以知道,它底层调用的是combineByKeyWithClassTag,并有combiner操作,分区内和分区间执行相同的业务逻辑。

/*  - `createCombiner`, which turns a V into a C (e.g., creates a one-element list)
*  - `mergeValue`, to merge a V into a C (e.g., adds it to the end of a list)
*  - `mergeCombiners`, to combine two C's into a single one.
*/

案例:

//3.1 创建第一个RDD
val rdd = sc.makeRDD(List(("a",1),("b",5),("a",5),("b",2)))
//3.2 计算相同key对应值的相加结果
val reduce: RDD[(String, Int)] = rdd.reduceByKey((x,y) => x+y)
//3.3 打印结果
reduce.collect().foreach(println

4.3.3 groupByKey 按k重新分组

原理图

看源码可以得出:

它没有进行combiner操作,分区内相同key的v,放进一个CompactBuffer里面,分区间的CompactBuffer进行合并,组成一个大的CompactBuffer,而且CompactBuffer放的都是每个key里面的value

需求说明:创建一个pairRDD,将相同key对应值聚合到一个seq中,并计算相同key对应值的相加结果

//3.1 创建第一个RDD
val rdd = sc.makeRDD(List(("a",1),("b",5),("a",5),("b",2)))
//3.2 将相同key对应值聚合到一个Seq中
val group: RDD[(String, Iterable[Int])] = rdd.groupByKey()
//3.3 打印结果
group.collect().foreach(println)
//3.4 计算相同key对应值的相加结果
group.map(t=>(t._1,t._2.sum)).collect().foreach(println)

4.3.4 reduceByKey与groupByKey的区别

1)reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。

2)groupByKey:按照key进行分组,直接进行shuffle。

3)开发指导:在不影响业务逻辑的前提下,优先选用reduceByKey。求和操作不影响业务逻辑,求平均值影响业务逻辑。

4.3.5 aggregateByKey 分区内与分区间

按照key处理分区内和分区间的逻辑

原理分析图

案例分析

//3.1 创建第一个RDD
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 3), ("a", 2), ("c", 4), ("b", 3), ("c", 6), ("c", 8)), 2)
//3.2 取出每个分区相同key对应值的最大值,然后相加
rdd.aggregateByKey(0)(math.max(_, _), _ + _).collect().foreach(println)

4.3.6 foldByKey 分区内与分区间相同

和aggregateByKey不同的是,它分区内与分区间的逻辑是相同的

实例:

//3.1 创建第一个RDD
val list: List[(String, Int)] = List(("a",1),("a",1),("a",1),("b",1),("b",1),("b",1),("b",1),("a",1))
val rdd = sc.makeRDD(list,2)
//3.2 求wordcount
//rdd.aggregateByKey(0)(_+_,_+_).collect().foreach(println)
rdd.foldByKey(0)(_+_).collect().foreach(println)

4.3.7 combinByKey

不好理解,要多去理解

原理

案例分析

需求说明:创建一个pairRDD,根据key计算每种key的均值。(先计算每个key对应值的总和以及key出现的次数,再相除得到结果)

//3.1 创建第一个RDD
val list: List[(String, Int)] = List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))
val input: RDD[(String, Int)] = sc.makeRDD(list, 2)
//3.2 将相同key对应的值相加,同时记录该key出现的次数,放入一个二元组
val combineRdd: RDD[(String, (Int, Int))] = input.combineByKey(
    (_, 1),
    (acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1),
    (acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
)
//3.3 打印合并后的结果
combineRdd.collect().foreach(println)
//3.4 计算平均值
combineRdd.map {
    case (key, value) => {
        (key, value._1 / value._2.toDouble)
    }
}.collect().foreach(println)

4.3.8 上面几个聚合的区别

reduceByKey, aggregateByKey, foldByKey和combinByKey

4.3.9 sortByKey 按k排序

实例:

//3.1 创建第一个RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((3,"aa"),(6,"cc"),(2,"bb"),(1,"dd")))

//3.2 按照key的正序(默认顺序)
rdd.sortByKey(true).collect().foreach(println)

//3.3 按照key的倒序
rdd.sortByKey(false).collect().foreach(println)

4.3.10 mapValues 只对v进行操作

相当于对一个RDD数据集进行遍历,对每个元素中的value做操作

原理图

实例

//3.1 创建第一个RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (1, "d"), (2, "b"), (3, "c")))
//3.2 对value添加字符串"|||"
rdd.mapValues(_ + "|||").collect().foreach(println)

4.3.11 join 连接

将相同key对应的多个value关联在一起

//3.1 创建第一个RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
//3.2 创建第二个pairRDD
val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (4, 6)))
//3.3 join操作并打印结果
rdd.join(rdd1).collect().foreach(println)

4.3.12 cogroupBy

类似全连接,但是在同一个RDD中对key进行聚合

操作两个RDD中的KV元素,每个RDD中相同key中的元素分别聚合成一个集合

//3.1 创建第一个RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c")))
//3.2 创建第二个RDD
val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1,4),(2,5),(3,6)))
//3.3 cogroup两个RDD并打印结果
rdd.cogroup(rdd1).collect().foreach(println)

4.4 案例操作(广告点击top3)

测试数据:链接: pan.baidu.com/s/14qwNw9Is… 提取码: 9mbr

实现

object Demo_top3 {

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

        //1. 初始化Spark配置信息并建立与Spark的连接
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Test")
        val sc = new SparkContext(sparkConf)

        //2. 读取日志文件,获取原始数据
        val dataRDD: RDD[String] = sc.textFile("input/agent.log")

        //3. 将原始数据进行结构转换string =>(prv-adv,1)
        val prvAndAdvToOneRDD: RDD[(String, Int)] = dataRDD.map {
            line => {
                val datas: Array[String] = line.split(" ")
                (datas(1) + "-" + datas(4), 1)
            }
        }

        //4. 将转换结构后的数据进行聚合统计(prv-adv,1)=>(prv-adv,sum)
        val prvAndAdvToSumRDD: RDD[(String, Int)] = prvAndAdvToOneRDD.reduceByKey(_ + _)

        //5. 将统计的结果进行结构的转换(prv-adv,sum)=>(prv,(adv,sum))
        val prvToAdvAndSumRDD: RDD[(String, (String, Int))] = prvAndAdvToSumRDD.map {
            case (prvAndAdv, sum) => {
                val ks: Array[String] = prvAndAdv.split("-")
                (ks(0), (ks(1), sum))
            }
        }

        //6. 根据省份对数据进行分组:(prv,(adv,sum)) => (prv, Iterator[(adv,sum)])
        val groupRDD: RDD[(String, Iterable[(String, Int)])] = prvToAdvAndSumRDD.groupByKey()

        //7. 对相同省份中的广告进行排序(降序),取前三名
        val mapValuesRDD: RDD[(String, List[(String, Int)])] = groupRDD.mapValues {
            datas => {
                datas.toList.sortWith(
                    (left, right) => {
                        left._2 > right._2
                    }
                ).take(3)
            }
        }

        //8. 将结果打印
        mapValuesRDD.collect().foreach(println)

        //9.关闭与spark的连接
        sc.stop()
    }
}

5 . Action算子

行动算子是触发了整个作业的执行。因为转换算子都是懒加载,并不会立即执行。

5.1 reduce 聚合

1)函数签名:def reduce(f: (T, T) => T): T

2)功能说明:f函数聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据。

//需求说明:创建一个RDD,将所有元素聚合得到结果
//3.1 创建第一个RDD
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))

//3.2 聚合数据
val reduceResult: Int = rdd.reduce(_+_)
println(reduceResult)

5.2 collect 以数组的形式返回数据集

1)函数签名:def collect(): Array[T]

2)功能说明:在驱动程序中,以数组Array的形式返回数据集的所有元素。

//需求说明:创建一个RDD,并将RDD内容收集到Driver端打印        
//3.1 创建第一个RDD
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
//3.2 收集数据到Driver
rdd.collect().foreach(println)

5.3 count 返回RDD中元素个数

//统计改RDD的个数
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
//3.2 返回RDD中元素的个数
val countResult: Long = rdd.count()
println(countResult)

5.4 first 返回RDD中第一个元素

1)函数签名: def first(): T

2)功能说明:返回RDD中的第一个元素

5.5 take 返回RDD中前N个元素

1)函数签名: def take(num: Int): Array[T]

2)功能说明:返回一个由RDD的前n个元素组成的数组

//取前2的元素
//3.1 创建第一个RDD
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
//3.2 返回RDD中元素的个数
val takeResult: Array[Int] = rdd.take(2)

5.6 takeOrdered

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

1)函数签名: def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]

2)功能说明:返回该RDD排序后的前n个元素组成的数组

源代码定义

def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope {
    ......
    if (mapRDDs.partitions.length == 0) {
        Array.empty
    } else {
        mapRDDs.reduce { (queue1, queue2) =>
          queue1 ++= queue2
          queue1
        }.toArray.sorted(ord)
    }
}

  //3.1 创建第一个RDD
  val rdd: RDD[Int] = sc.makeRDD(List(1,3,2,4))
  //3.2 返回RDD中元素的个数
  val result: Array[Int] = rdd.takeOrdered(2)

5.7 aggregate

//需求说明:创建一个RDD,将所有元素相加得到结果
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4),8)
//3.2 将该RDD所有元素相加得到结果
//val result: Int = rdd.aggregate(0)(_ + _, _ + _)
val result: Int = rdd.aggregate(10)(_ + _, _ + _)

5.8 fold

//需求说明:创建一个RDD,将所有元素相加得到结果
//3.1 创建第一个RDD
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
//3.2 将该RDD所有元素相加得到结果
val foldResult: Int = rdd.fold(0)(_+_)
println(foldResult)

5.9 countByKey 统计每个key的个数

1)函数签名:def countByKey(): Map[K, Long]

2)功能说明:统计每种key的个数

//需求说明:创建一个PairRDD,统计每种key的个数
//3.1 创建第一个RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(List((1, "a"), (1, "a"), (1, "a"), (2, "b"), (3, "c"), (3, "c")))
//3.2 统计每种key的个数
val result: collection.Map[Int, Long] = rdd.countByKey()
println(result)

5.10 save相关算子

  1. saveAsTextFile(path)保存成Text文件

​ (1)函数签名

​ (2)功能说明:将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本

2)保存成Sequencefile文件

​ (1)函数签名

​ (2)功能说明:将数据集中的元素以Hadoop Sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。

注意:只有kv类型RDD有该操作,单值的没有

3)saveAsObjectFile(path)序列化成对象保存到文件

(1)函数签名

(2)功能说明:用于将RDD中的元素序列化成对象,存储到文件中。

5.11 foreach 遍历RDD中每个元素

foreach.png

//需求说明:创建一个RDD,对每个元素进行打印       
//3.1 创建第一个RDD
// val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
//3.2 收集后打印
rdd.map(num=>num).collect().foreach(println)
println("****************")
//3.3 分布式打印
rdd.foreach(println)

6. RDD序列化

在实际开发中我们往往需要自己定义一些对于RDD的操作,那么此时需要注意的是,初始化工作是在Driver端进行的,而实际运行程序是在Executor端进行的,这就涉及到了跨进程通信,是需要序列化的。下面我们看几个例子:

6.1 闭包引入

object serializable01_object {

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

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        //3.创建两个对象
        val user1 = new User()
        user1.name = "zhangsan"

        val user2 = new User()
        user2.name = "lisi"

				val userRDD1: RDD[User] = sc.makeRDD(List(user1, user2))
        //3.1 打印,ERROR报java.io.NotSerializableException
        //userRDD1.foreach(user => println(user.name))
        

        //3.2 打印,RIGHT
        val userRDD2: RDD[User] = sc.makeRDD(List())
        //userRDD2.foreach(user => println(user.name))

        //3.3 打印,ERROR Task not serializable 注意:没执行就报错了
        userRDD2.foreach(user => println(user1.name))

        //4.关闭连接
        sc.stop()
    }
}

//class User {
//    var name: String = _
//}
class User extends Serializable {
    var name: String = _
}

闭包检查源码

6.2 序列化方法和属性

1)说明

Driver:算子以外的代码都是在Driver端执行

Executor:算子里面的代码都是在Executor端执行

6.3 kryo 序列化框架

序列化,详细查看doc文档

7. RDD依赖关系

7.1 查看血缘关系

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

7.1.1 血缘关系查看图

7.1.2 实例

val fileRDD: RDD[String] = sc.textFile("input/1.txt")
println(fileRDD.toDebugString)
println("----------------------")

val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
println(wordRDD.toDebugString)
println("----------------------")

val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
println(mapRDD.toDebugString)
println("----------------------")

val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
println(resultRDD.toDebugString)

结果:

注意:圆括号中的数字表示RDD的并行度,也就是有几个分区

(2) input/1.txt MapPartitionsRDD[1] at textFile at Lineage01.scala:15 []
 |  input/1.txt HadoopRDD[0] at textFile at Lineage01.scala:15 []
----------------------
(2) MapPartitionsRDD[2] at flatMap at Lineage01.scala:19 []
 |  input/1.txt MapPartitionsRDD[1] at textFile at Lineage01.scala:15 []
 |  input/1.txt HadoopRDD[0] at textFile at Lineage01.scala:15 []
----------------------
(2) MapPartitionsRDD[3] at map at Lineage01.scala:23 []
 |  MapPartitionsRDD[2] at flatMap at Lineage01.scala:19 []
 |  input/1.txt MapPartitionsRDD[1] at textFile at Lineage01.scala:15 []
 |  input/1.txt HadoopRDD[0] at textFile at Lineage01.scala:15 []
----------------------
(2) ShuffledRDD[4] at reduceByKey at Lineage01.scala:27 []
 +-(2) MapPartitionsRDD[3] at map at Lineage01.scala:23 []
    |  MapPartitionsRDD[2] at flatMap at Lineage01.scala:19 []
    |  input/1.txt MapPartitionsRDD[1] at textFile at Lineage01.scala:15 []
    |  input/1.txt HadoopRDD[0] at textFile at Lineage01.scala:15 []

7.2 查看依赖关系

val fileRDD: RDD[String] = sc.textFile("input/1.txt")
println(fileRDD.dependencies)
println("----------------------")

val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
println(wordRDD.dependencies)
println("----------------------")

val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
println(mapRDD.dependencies)
println("----------------------")

val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
println(resultRDD.dependencies)

结果:

List(org.apache.spark.OneToOneDependency@f2ce6b)
----------------------
List(org.apache.spark.OneToOneDependency@692fd26)
----------------------
List(org.apache.spark.OneToOneDependency@627d8516)
----------------------
List(org.apache.spark.ShuffleDependency@a518813)

OneToOneDependency定义:

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
    override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

7.3 窄依赖

父RDD的每一个partition被子的一个partition处理就是窄依赖,父的每一个partition,如何在子RDD中,被子用多个分区来处理父的那个一个partition,那么就是宽依赖

7.4 宽依赖

7.5 spark中的job调度

​ 一个Spark应用包含一个驱动进程(driver process,在这个进程中写Spark的逻辑代码)和多个执行器进程(executor process,跨越集群中的多个节点)。Spark 程序自己是运行在驱动节点, 然后发送指令到执行器节点

一个Spark集群可以同时运行多个Spark应用, 这些应用是由集群管理器(cluster manager)来调度。

Spark应用可以并发的运行多个job, job对应着给定的应用内的在RDD上的每个 action操作。

7.5.1 spark应用

​ a) 一个Spark应用可以包含多个Spark job, Spark job是在驱动程序中由SparkContext 来定义的。

​ b) 当启动一个 SparkContext 的时候, 就开启了一个 Spark 应用。 一个驱动程序被启动了, 多个执行器在集群中的多个工作节点(worker nodes)也被启动了。 一个执行器就是一个 JVM, 一个执行器不能跨越多个节点, 但是一个节点可以包括多个执行器。

​ c ) 一个 RDD 会跨多个执行器被并行计算. 每个执行器可以有这个 RDD 的多个分区, 但是一个分区不能跨越多个执行器.

7.5.2 Spark Job的划分

​ a) 由于Spark的懒执行, 在驱动程序调用一个action之前, Spark 应用不会做任何事情,针对每个action,Spark 调度器就创建一个执行图(execution graph)和启动一个 Spark job。

​ b) 每个 job 由多个stages 组成, 这些 stages 就是实现最终的 RDD 所需的数据转换的步骤。一个宽依赖划分一个stage。每个 stage 由多个 tasks 来组成, 这些 tasks 就表示每个并行计算, 并且会在多个执行器上执行。

7.6 stage任务划分

1)DAG有向无环图

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

(1)Application:初始化一个SparkContext即生成一个Application;

(2)Job:一个Action算子就会生成一个Job;

(3)Stage:Stage等于宽依赖的个数加1;

(4)Task:一个Stage阶段中,最后一个RDD的分区个数就是Task的个数。

注意:Application->Job->Stage->Task每一层都是1对n的关系。 实例

//3. 创建RDD
val dataRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4,1,2),2)
//3.1 聚合
val resultRDD: RDD[(Int, Int)] = dataRDD.map((_,1)).reduceByKey(_+_)
// Job:一个Action算子就会生成一个Job;
//3.2 job1打印到控制台
resultRDD.collect().foreach(println)
//3.3 job2输出到磁盘
resultRDD.saveAsTextFile("output")
Thread.sleep(1000000)

任务划分以及源码分析:

8. RDD持久化

9. 键值对RDD数据分区

10.数据读取与保存

11. 累加器

12. 广播变量

13.spark项目实战