Spark - 运行架构&原理

Spark运行架构

运行架构

Spark框架的核心是一个计算引擎,整体来说,它采用了标准 master-slave 的结构。如下图所示,它展示了一个 Spark执行时的基本结构。图形中的Driver表示master,负责管理整个集群中的作业任务调度。图形中的Executor 则是 slave,负责实际执行任务。
在这里插入图片描述

核心组件

Driver

Spark驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作。Driver在Spark作业执行时主要负责:

  1. 将用户程序转化为作业(job)
  2. 在Executor之间调度任务(task)
  3. 跟踪Executor的执行情况
  4. 通过UI展示查询运行情况

实际上,我们无法准确地描述Driver的定义,因为在整个的编程过程中没有看到任何有关Driver的字眼。所以简单理解,所谓的Driver就是驱使整个应用运行起来的程序,也称之为Driver类。

Executor

Spark Executor是集群中工作节点(Worker)中的一个JVM进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有Executor节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他Executor节点上继续运行。
Executor有两个核心功能 :

  1. 负责运行组成Spark应用的任务,并将结果返回给驱动器进程
  2. 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算。

Master & Worker

  • Spark集群的独立部署环境中,不需要依赖其他的资源调度框架,自身就实现了资源调度的功能,所以环境中还有其他两个核心组件:Master和Worker,这里的Master是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责,类似于Yarn环境中的RM, 而Worker呢,也是进程,一个Worker运行在集群中的一台服务器上,由Master分配资源对数据进行并行的处理和计算,类似于Yarn环境中NM。

ApplicationMaster

  • Hadoop用户向YARN集群提交应用程序时,提交程序中应该包含ApplicationMaster,用于向资源调度器申请执行任务的资源容器Container,运行用户自己的程序任务job,监控整个任务的执行,跟踪整个任务的状态,处理任务失败等异常情况。
  • 简单来讲,ResourceManager(资源)和Driver(计算)之间的解耦合靠的就是ApplicationMaster。

核心概念

Executor与Core

Spark Executor是集群中运行在工作节点(Worker)中的一个JVM进程,是整个集群中的专门用于计算的节点。在提交应用中,可以提供参数指定计算节点的个数,以及对应的资源。这里的资源一般指的是工作节点Executor的内存大小和使用的虚拟CPU核(Core)数量。
应用程序相关启动参数如下:
在这里插入图片描述

并行度 (Parallelism)

在分布式计算框架中一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正地实现多任务并行执行,记住,这里是并行,而不是并发。这里我们将整个集群并行执行任务的数量称之为并行度。那么一个作业到底并行度是多少呢?这个取决于框架的默认配置。应用程序也可以在运行过程中动态修改。

有向无环图 (DAG)

在这里插入图片描述
大数据计算引擎框架我们根据使用方式的不同一般会分为四类,其中第一类就是Hadoop所承载的MapReduce,它将计算分为两个阶段,分别为 Map阶段 和 Reduce阶段。对于上层应用来说,就不得不想方设法去拆分算法,甚至于不得不在上层应用实现多个 Job 的串联,以完成一个完整的算法,例如迭代计算。 由于这样的弊端,催生了支持 DAG 框架的产生。因此,支持 DAG 的框架被划分为第二代计算引擎。如 Tez 以及更上层的 Oozie。这里我们不去细究各种 DAG 实现之间的区别,不过对于当时的 Tez 和 Oozie 来说,大多还是批处理的任务。接下来就是以 Spark 为代表的第三代的计算引擎。第三代计算引擎的特点主要是 Job 内部的 DAG 支持(不跨越 Job),以及实时计算。
这里所谓的有向无环图,并不是真正意义的图形,而是由Spark程序直接映射成的数据流的高级抽象模型。简单理解就是将整个程序计算的执行过程用图形表示出来,这样更直观,更便于理解,可以用于表示程序的拓扑结构。
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。

提交流程

所谓的提交流程,其实就是我们开发人员根据需求写的应用程序通过Spark客户端提交给Spark运行环境执行计算的流程。在不同的部署环境中,这个提交过程基本相同,但是又有细微的区别,我们这里不进行详细的比较,但是因为国内工作中,将Spark引用部署到Yarn环境中会更多一些,所以本课程中的提交流程是基于Yarn环境的。
在这里插入图片描述
Spark应用程序提交到Yarn环境中执行的时候,一般会有两种部署执行的方式:Client和Cluster。两种模式,主要区别在于:Driver程序的运行节点。
Yarn Client模式
  Client模式将用于监控和调度的Driver模块在客户端执行,而不是Yarn中,所以一般用于测试。

  • Driver在任务提交的本地机器上运行
  • Driver启动后会和ResourceManager通讯申请启动ApplicationMaster
  • ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,负责向ResourceManager申请Executor内存
  • ResourceManager接到ApplicationMaster的资源申请后会分配container,然后ApplicationMaster在资源分配指定的NodeManager上启动Executor进程
  • Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行main函数
  • 之后执行到Action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。
    Yarn Cluster模式
      Cluster模式将用于监控和调度的Driver模块启动在Yarn集群资源中执行。一般应用于实际生产环境。
  • 在YARN Cluster模式下,任务提交后会和ResourceManager通讯申请启动ApplicationMaster,
  • 随后ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster就是Driver。
  • Driver启动后向ResourceManager申请Executor内存,ResourceManager接到ApplicationMaster的资源申请后会分配container,然后在合适的NodeManager上启动Executor进程
  • Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行main函数,
  • 之后执行到Action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。

Spark核心编程

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

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

RDD

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

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

核心属性

在这里插入图片描述
分区列表
RDD数据结构中存在分区列表,用于执行任务时并行计算,是实现分布式计算的重要属性。
在这里插入图片描述
分区计算函数
Spark在计算时,是使用分区函数对每一个分区进行计算
在这里插入图片描述
RDD之间的依赖关系
RDD是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD建立依赖关系
在这里插入图片描述
分区器(可选)
当数据为KV类型数据时,可以通过设定分区器自定义数据的分区
在这里插入图片描述
首选位置(可选)
计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算
在这里插入图片描述

执行原理

从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。
Spark框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。
RDD是Spark框架中用于数据处理的核心模型,接下来我们看看,在Yarn环境中,RDD的工作原理:

RDD工作原理

  1. 启动Yarn集群环境
  2. Spark通过申请资源创建调度节点和计算节点
  3. Spark框架根据需求将计算逻辑根据分区划分成不同的任务
  4. 调度节点将任务根据计算节点状态发送到对应的计算节点进行计算
    在这里插入图片描述
    RDD在整个流程中主要用于将逻辑进行封装,并生成Task发送给Executor节点执行计算

基础编程

RDD创建

从集合(内存)中创建RDD

	val sparkConf: SparkConf = new SparkConf().setMaster("local").setAppName("wordCount")
	val sc = new SparkContext(sparkConf)
	
	// 从内存中创建RDD - makeRDD参数解读
	// 第一个参数 : 数据源
	// 第二个参数 : 并行度 , 等同于RDD分区
	//     并行度首先会从spark配置信息中获取spark.default.parallelism值
	//     如果获取不到指定参数, 则会采用默认值totalCores(机器的总核数)
	//     机器的总核数取决于当前环境中可用的核数
	//     local → 单核(单线程)
	//     local[4] → 四核(4线程)
	//     local[*] → 最大核数(取决于物理机核数)
	
	// 从内存中创建RDD
	// 方式一 :
	val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4))
	println(rdd1.collect().mkString(", "))
	
	// 方式二 : 底层就是调用的parallelize方法, 原理一样
	val rdd2: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
	println(rdd2.collect().mkString(", "))
	
    // 将RDD的处理后的数据保存到分区文件中
    rdd.saveAsTextFile("output")
	
	sc.stop()

从外部存储(文件)创建RDD

	val sparkConf: SparkConf = new SparkConf().setMaster("local").setAppName("wordCount")
	val sc = new SparkContext(sparkConf)
	
	// 从磁盘创建RDD
	val fileRdd: RDD[String] = sc.textFile("input")
	println(fileRdd.
	  flatMap(line => line.split(" ")).
	  groupBy(word => word).
	  map(kv => (kv._1, kv._2.size)).
	  collect().
	  mkString(", "))
	// 默认使用Hadoop读取文件的规则, 一行一行读取
	// 读取指定文件、文件目录、读取HDFS, 支持通配符
	
	sc.stop()

从其他RDD创建
主要是通过一个RDD运算完后,再产生新的RDD
直接创建RDD(new)
使用new的方式直接构造RDD,一般由Spark框架自身使用

RDD并行度与分区

内存(集合)数据分区规则

	// =============== RDD并行度 与 分区 ==================]
	// 并行度 : 能够并行计算的任务数量
	// 由于一个分区对应一个任务, 所以, 在数值上 并行度 = 分区数
	// 该数值可以在创建RDD时设定
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark - Parallelism")
	val sc = new SparkContext(sparkConf)
	
	// makeRDD 第一个参数 : 数据源
	// makeRDD 第二个参数 : 默认并行度(分区数)
	
	// 数据分区规则(内存) :
	//     首先采用创建RDD时的传值numSlices
	//     否则默认会从spark配置信息中获取spark.default.parallelism
	//     如果获取不到指定参数, 则会采用默认值totalCores(总核数)
	//     totalCores取决于当前可用的总核数
	//        setMaster("local")    → 表示单核
	//        setMaster("local[2]") → 表示双核
	//        setMaster("local[*]") → 表示最大核数(取决于物理机核数)
	
	// ① 首先将内存中的数据按照平均分的方式进行分区处理
	val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
	// (seq.length = 6, numSlices = 3)
	// → 分区数 : 3
	// → 分区数据 : 6 / 3 = 2
	// → 分区编号(0, 1, 2)
	//   List(1, 2, 3, 4, 5, 6) => Array(1, 2, 3 ,4, 5, 6)
	//   Array.slice => 切分数组 => (start until end)
	//   val start = ((i * length) / numSlices).toInt // i指分区号
	//   val end = (((i + 1) * length) / numSlices).toInt
	// → 0 → [0, 2) → 1, 2
	// → 1 → [2, 4) → 3, 4
	// → 2 → [4, 6) → 5, 6
	
	// ② 当内存中的数据不能均分时, 会将剩余的数据按照一个算法实现分配
	val rdd2: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5), 3)
	// (seq.length = 5, numSlices = 3)
	// → 分区数 : 3
	// → 分区数据 : 5 / 3 = 1...2
	// → 分区编号(0, 1, 2)
	//   List(1, 2, 3, 4, 5, 6) => Array(1, 2, 3 ,4, 5, 6)
	//   Array.slice => 切分数组 => (start(from), end(until))
	//   val start = ((i * length) / numSlices).toInt // i指分区号
	//   val end = (((i + 1) * length) / numSlices).toInt
	// → 0 → [0, 1) → 1
	// → 1 → [1, 3) → 2, 3
	// → 2 → [3, 5) → 4, 5
	
	//    rdd.collect().foreach(println)
	// 将RDD的处理后的数据保存到分区文件中
	rdd1.saveAsTextFile("output")
	rdd2.saveAsTextFile("output")
	
	sc.stop()

磁盘(单文件)数据分区规则

	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark - Parallelism")
	val sc = new SparkContext(sparkConf)
	
	// textFile 第一个参数 : 数据源路径
	// textFile 第二个参数 : 最小分区数
	// 默认值为 : math.min(defaultParallelism, 2) => defaultParallelism : 总核数(totalCore)
	// minPartitions => numSplits
	
	// 数据分区规则(磁盘/单个文件) :
	
	// 读取文件数据时, 是按照Hadoop文件读取的规则进行切片分区, 而切片规则和数据读取的规则有些差异
	// 文件切片规则 : 按字节切片
	// ① 文件字节数(totalSize) = 10byte, 预计切片数量(numSplits) = 2(默认值)
	// goalSize = totalSize / (numSplits == 0 ? 1 : numSplits) = 10 / 2 = 5 => 2个分区
	// 数据分区(读取)规则 : 按行读取
	// splitSize = Math.max(minSize, Math.min(goalSize, blockSize))
	// minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize)
	val rdd1: RDD[String] = sc.textFile("input/word1.txt")
	rdd1.saveAsTextFile("output1") // 12, 34
	
	// 所谓的最小分区数, 取决于总的字节数是否能整除分区数并且剩余的字节达到一个比率(1.1)
	// 实际产生的分区数量可能大于最小分区数
	// ② 文件字节数(totalSize) = 10byte, 预计切片数量(numSplits) = 4(minPartitions)
	// 10 / 4 = 2byte...2byte => 5个分区 (由于剩余数据 2 / 4 > 1.1, 则增加一个分区)
	// 分区号 : (0, 1, 2, 3, 4)
	// > 文件切片规则 : (按字节切片)
	// [start to end)
	// 0 => [0, 2]
	// 1 => [2, 4]
	// 2 => [4, 6]
	// 3 => [6, 8]
	// 4 => [8, 10]
	// > 数据分区规则 : (按行读取 + 偏移量)
	//     数据按行读取时, 还会考虑偏移量(数据的offset)的设置
	// data  offset
	// 1\n => 012
	// 2\n => 345
	// 3\n => 678
	// 4   => 9
	// > 最终数据分区规则 : 文件切片规则 + 按行读取 + 偏移量
	// 0 => [0, 2]  => (012) => 1\n
	// 1 => [2, 4]  => (345) => 2\n
	// 2 => [4, 6]  => (678) => 3\n
	// 3 => [6, 8]  => 空
	// 4 => [8, 10] => (9)   => 4
	// 小结 : 按行读取(不允许一行数据被切开)的基础上, 考虑偏移量, 文件切片, 分别读取到每个分区。
	val rdd2: RDD[String] = sc.textFile("input/word1.txt", 4)
	rdd2.saveAsTextFile("output2") // 1, 2, 3, 空, 4
	
	// 例题 :
	// 分区数 : 10 / 3 = 3..1 => 4个分区 (1 / 3 > 1.1)
	// 分区号 : (0, 1, 2, 3)
	// > 文件切片 : 按字节
	// 0 => [0, 3]
	// 1 => [3, 6]
	// 2 => [6, 9]
	// 3 => [9, 10]
	// > 数据分区 : 按行读取 + 偏移量
	// data  offset
	// 1\n => 012
	// 2\n => 345
	// 3\n => 678
	// 4   => 9
	// > 最终数据分区 : 文件切片规则 + 按行读取 + 偏移量
	// 0 => [0, 3]  => (012345) => 1\n2\n
	// 1 => [3, 6]  => (678)    => 3\n
	// 2 => [6, 9]  => (9)      => 4
	// 3 => [9, 10] => 空
	val rdd3: RDD[String] = sc.textFile("input/word1.txt", 3)
	rdd3.saveAsTextFile("output3") // 12, 3, 4, 空
	
	sc.stop()
	
	// word1.txt数据(10byte)(\n : 换行)
	// 1\n
	// 2\n
	// 3\n
	// 4

磁盘(多文件)数据分区规则

	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark - Parallelism")
	val sc = new SparkContext(sparkConf)
	
	// 数据分区规则(磁盘/多个文件) :
	
	// 分区数 : 12 / 2 = 6 => 2个分区
	// [0, 6]  => 1\n234
	// [0, 6] => 5\n678
	val rdd1: RDD[String] = sc.textFile("input", 2)
	rdd1.saveAsTextFile("output1") // 1\n234 , 5\n678
	
	// 最终数据分区规则 : 文件划分规则 + 文件切片规则 + 按行读取 + 偏移量
	// Hadoop分区是以文件为单位进行划分的, 读取数据不能跨越文件
	// 分区数 : 12 / 3 = 4 => 4个分区
	// word1.txt => [0, 4] => 1\n234
	//           => [4, 6] => 空
	// word2.txt => [0, 4] => 5\n678
	//           => [4, 6] => 空
	val rdd2: RDD[String] = sc.textFile("input", 3)
	rdd2.saveAsTextFile("output2") // 1\n234 , 5\n678
	
	sc.stop()
	
	// word1.txt数据(6byte)(\n : 换行)
	// 1\n
	// 234
	
	// word2.txt数据(6byte)(\n : 换行)
	// 5\n
	// 678
RDD转换算子

Value类型
map

	// =================== RDD转换算子 ================
	//  问题(初始) → 问题(处理中) → 问题(完成)
	//        operation     operation
	//           算子          算子
	// 所谓的RDD的转换算子, 就是将旧的RDD通过方法的调用转换成新的RDD
	
	// 分类 : Value类型 、双Value类型 、Key-Value类型
	
	// ============== Value类型 - map ================
	// 函数签名 : TODO def map[U: ClassTag](f: T => U): RDD[U]
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// 转换算子 → 不会触发作业的执行, 只是功能的封装
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
	
	// 实现功能 : 旧RDD → map算子 → 新RDD
	// 分区规则 : RDD中有分区列表, 默认分区数量不变, 区内数据转换后分别输出
	// 执行顺序 : 区内数据按照算子的顺序执行, 一条数据执行完所有逻辑, 再操作下一条数据
	//           区间数据执行没有顺序, 且无需等待, 互不相干
	val newRdd: RDD[Int] = rdd.map(_ * 2)
	newRdd.saveAsTextFile("output1")
	
	// collect()方法不会转换RDD, 而是会触发作业的执行, 将该类方法称之为<行动算子(action)>
	// newRdd.collect()
	
	sc.stop()
	// 需求 : 从服务器日志数据apache.log中获取用户请求URL资源路径
	val sparkConf = new SparkConf().setMaster("local[*]").setAppName("function_1")
	val sc = new SparkContext(sparkConf)
	
	val fileRDD: RDD[String] = sc.textFile("input/apache.log")
	val urlRDD: RDD[String] = fileRDD.map(line => {
	  val datas: Array[String] = line.split(" ")
	  datas(6)
	})
	urlRDD.collect().foreach(println)
	
	sc.stop()

mapPartitions

	// ============== Value类型 - mapPartitions ================
	/* 函数签名 :
	TODO def mapPartitions[U: ClassTag](
	  f: Iterator[T] => Iterator[U],
	  preservesPartitioning: Boolean = false): RDD[U]
	 */
	
	// map算子 : 对数据以条为单位进行处理; 全量数据操作, 不能丢失数据
	// mapPartitions算子 : 对数据以区为单位进行处理; 一次性获取分区的所有数据, 可以执行迭代器集合的所有操作
	// 后者类似批处理, 如果一个分区的数据没有完全处理完, 那么当前分区的所有数据都不会释放, 有内存溢出的风险
	// 当内存空间足够大时, 为了提高效率, 推荐使用mapPartitions算子
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
	val dataRDD: RDD[Int] = rdd.mapPartitions(iter => {
	  iter.foreach(println)
	  iter.map(_ * 2)
	  // iter.filter(_ % 2 == 2)
	})
	
	println(dataRDD.collect().mkString(", ")) // 2, 4, 6, 8
	
	sc.stop()
	// 需求 : 获取每个分区的最大值
	
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	val rdd: RDD[Int] = sc.makeRDD(List(4, 7, 2, 9, 4, 0), 3)
	val dataRDD: RDD[Int] = rdd.mapPartitions(iter => {
	  List(iter.max).iterator
	})
	
	println(dataRDD.collect().mkString(", ")) // 7, 9, 4
	
	sc.stop()

mapPartitionsWithIndex

   // ============== Value类型 - mapPartitionsWithIndex ================
    /* 函数签名 :
    TODO def mapPartitionsWithIndex[U: ClassTag](
      f: (Int, Iterator[T]) => Iterator[U],
      preservesPartitioning: Boolean = false): RDD[U]
     */

    // 相较于mapPartitions算子返回datas
    // 该算子返回的是Tuple(index, datas) index => 分区号
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
    val sc = new SparkContext(sparkConf)

    val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
    val dataRDD: RDD[Int] = rdd.mapPartitionsWithIndex((index, datas) => {
      datas.map(index * _)
    })

    println(dataRDD.collect().mkString(", ")) // 0, 0, 3, 4

    sc.stop()
	// 需求 : 获取第二个数据分区的数据
	
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
	val dataRDD: RDD[Int] = rdd.mapPartitionsWithIndex((index, datas) => {
	  if (index == 1) {
	    datas
	  } else {
	    Nil.iterator
	  }
	})
	
	println(dataRDD.collect().mkString(", ")) // 3, 4
	
	sc.stop()

flatMap

	// ============== Value类型 - flatMap ================
	
	// 函数签名 : TODO def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// 将数据扁平化后再进行映射处理, 称之为扁平映射算子
	val rdd: RDD[List[Int]] = sc.makeRDD(List(List(1, 2), List(3, 4)))
	val newRDD: RDD[Int] = rdd.flatMap(list => list)
	println(newRDD.collect().mkString(", ")) // 1, 2, 3, 4
	
	sc.stop()
	// 需求 : 将List(List(1,2), 3, (List(4,5))进行扁平化处理
	
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// 将数据扁平化后再进行映射处理, 称之为扁平映射算子
	val rdd: RDD[Any] = sc.makeRDD(List(List(1, 2), 3, (List(4, 5))))
	val newRDD: RDD[Int] = rdd.flatMap {
	  datas => {
	    datas match {
	      case list: List[Int] => list
	      case data: Int => List(data)
	    }
	  }
	}
	println(newRDD.collect().mkString(", ")) // 1, 2, 3, 4, 5
	
	sc.stop()

glom

	// ============== Value类型 - glom ================
	
	// 函数签名 : TODO def glom(): RDD[Array[T]]
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// 将同一个分区的数据直接转换为相同类型的内存数组进行处理, 分区不变
	// 即 : data : Any => Array[Any]
	val rdd: RDD[Any] = sc.makeRDD(List(1, "2", 3, 4), 2)
	val newRDD: RDD[Array[Any]] = rdd.glom()
	newRDD.collect().foreach(_.foreach(println)) // 1, 2, 3, 4
	
	sc.stop()
    // 需求 : 计算所有分区最大值求和(分区内取最大值,分区间最大值求和)

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

    val rdd: RDD[Int] = sc.makeRDD(List(5, 4, 7, 9, 2, 0), 3) /// 16

    // 将每个分区的数据转换为数组
    val newRDD1: RDD[Array[Int]] = rdd.glom()
    // 将数组中的最大值取出
//    val newRDD2: RDD[Int] = newRDD1.map(arr => arr.toList.max)
    val newRDD2: RDD[Int] = newRDD1.map(arr => arr.max) // 自动转换
    // 将取出的最大值求和
    val array: Array[Int] = newRDD2.collect()

    println(array.sum) // 1, 2, 3, 4

    sc.stop()

groupBy

	// ============== Value类型 - groupBy ================
	
	// 函数签名 : TODO def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// 将数据根据指定的规则进行分组, 分区数默认不变,但是数据会被打乱重新组合,我们将这样的操作称之为shuffle。
	// 极限情况下,数据可能被分在同一个分区中
	// 该算子可能会导致, 下游RDD分区的数据倾斜, 此时可以改变下游RDD的数据分区数
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
	val newRDD: RDD[(Int, Iterable[Int])] = rdd.groupBy(
	  (num: Int) => {
	    num % 2
	  }, 2 // 设置shuffle后的下游分区数
	)
	newRDD.foreach(println)
	
	sc.stop()
    // 需求 : 将List("Hello", "hive", "hbase", "Hadoop")根据单词首写字母进行分组

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

    val rdd: RDD[String] = sc.makeRDD(List("Hello", "hive", "hbase", "Hadoop"), 2)
    val newRDD: RDD[(Char, Iterable[String])] = rdd.groupBy(word => {
//      word.subSequence(0, 1)
//      word.charAt(0)

      // 隐式转换 : String(0) => StringOps
      word(0)
    })

    newRDD.foreach(println)

    sc.stop()
	// 需求 : 从服务器日志数据apache.log中获取每个时间段访问量
	
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	val rdd: RDD[String] = sc.textFile("input/apache.log")
	
	val newRDD1: RDD[String] = rdd.map(
	  log => {
	    val datas: Array[String] = log.split(" ")
	    datas(3)
	  }
	)
	val newRDD2: RDD[(String, Iterable[String])] = newRDD1.groupBy(
	  time => {
	    time.substring(11, 13)
	  }
	)
	
	newRDD2.foreach(println)
	
	sc.stop()
	// 需求 : WordCount
	
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	val rdd: RDD[String] = sc.makeRDD(List("Hello Scala", "Hello Spark", "Spark Scala"))
	
	val newRDD1: RDD[String] = rdd.flatMap(data => data.split(" "))
	val newRDD2: RDD[(String, Iterable[String])] = newRDD1.groupBy(word => word)
	val result: RDD[(String, Int)] = newRDD2.map(kv => (kv._1, kv._2.size))
	
	result.foreach(println)
	
	sc.stop()

filter

	// ============== Value类型 - filter ================
	
	// 函数签名 : TODO def filter(f: T => Boolean): RDD[T]
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// filter : 过滤, 将数据按照指定的规则进行筛选过滤, 符合规则的数据将保留, 不符合规则的数据丢弃
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 6, 4, 5, 3), 3)
	val result: RDD[Int] = rdd.filter(word => word % 2 == 0)
	// 当数据筛选过滤后, 分区不变, 但是分区内的数据可能不均衡, 在生产环境下, 可能会出现数据倾斜
	
	result.foreach(println)
	
	sc.stop()
	// 需求 : 从服务器日志数据apache.log中获取2015年5月17日的请求路径
	
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	val rdd: RDD[String] = sc.textFile("input/apache.log")
	
	val newRDD1: RDD[String] = rdd.map(
	  log => {
	    val datas: Array[String] = log.split(" ")
	    datas(3)
	  }
	)
	
	val result: RDD[String] = newRDD1.filter(time => time.substring(0, 10) == "17/05/2015")
	
	result.foreach(println)
	
	sc.stop()

sample

	// ============== Value类型 - sample ================
	
	/* 函数签名 :
	TODO
	 def sample(
	  withReplacement: Boolean,
	  fraction: Double,
	  seed: Long = Utils.random.nextLong): RDD[T]
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// sample : 根据指定的规则从数据集中抽取数据
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
	
	// 抽取数据不放回(伯努利算法)
	// 伯努利算法 : 又叫0、1分布。例如扔硬币, 要么正面, 要么反面。
	// 具体实现 : 根据种子和随机算法算出一个数和第二个参数设置几率较小, 小于第二个参数要, 大于不要
	// 第一个参数 : 抽取的数据是否放回, false : 不放回
	// 第二个参数 : 每条数据抽取的几率, [0, 1]
	// 第三个参数 : 随机数种子
	val result1: RDD[Int] = rdd.sample(false, 0.5, 1)
	
	// 抽取数据后放回(泊松算法)
	// 第一个参数 : 抽取的数据是否放回, true : 放回
	// 第二个参数 : 每一条数据出现次数的期望(λ)。 [0, ∞), 表示每一条数据被期望抽到的次数
	// 第三个参数 : 随机数种子
	val result2: RDD[Int] = rdd.sample(true, 2, 1)
	
	result1.foreach(println)
	result2.foreach(println)
	
	// 应用 - 取样
	// 比如 : 如果在开发中, 出现了数据倾斜的情况, 那么可以从数据倾斜的分区中抽取一部分数据进行分析
	
	sc.stop()

distinct

	// ============== Value类型 - distinct ================
	
	/* 函数签名 :
	TODO
	 def distinct()(implicit ord: Ordering[T] = null): RDD[T]
	 def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// distinct : 将数据集中的数据进行去重
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 1, 2, 4), 3)
	
	val result1: RDD[Int] = rdd.distinct()
	
	// distinct的第二个参数 : 去重后的分区数量
	val result2: RDD[Int] = rdd.distinct(2)
	
	result1.saveAsTextFile("output1")
	result2.saveAsTextFile("output2")
	
	sc.stop()

coalesce

	// ============== Value类型 - coalesce ================
	
	/* 函数签名 :
	TODO
	 def coalesce(numPartitions: Int, shuffle: Boolean = false,
	       partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
	      (implicit ord: Ordering[T] = null)
	 : RDD[T]
	
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// coalesce : 缩减分区, 用于大数据集过滤后(或分区不合理时), 减少分区的个数, 降低任务调度成本
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 1, 2, 4), 3)
	
	// 减小分区
	// 第一个参数表示缩减分区后的分区数量
	// coalesce默认无法扩大分区, 在缩减分区时数据不会打乱重组, 即coalesce默认没有shuffle过程
	val result1: RDD[Int] = rdd.coalesce(2)
	
	// 扩大分区 : 常用repartition代替
	// 第二个参数表示是否启用shuffle。不开启shuffle, 无法扩大分区
	val result2: RDD[Int] = rdd.coalesce(4, true) // 默认为false
	
	result1.foreach(println)
	result2.foreach(println)
	
	sc.stop()

repartition

	// ============== Value类型 - repartition ================
	
	/* 函数签名 :
	TODO
	 def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// repartition : 改变分区数, 底层执行的coalesce操作
	// 默认开启shuffle过程, 且无法更改。因此缩减分区时不推荐使用
	val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 1, 2, 4), 3)
	
	val result: RDD[Int] = rdd.repartition(4)
	
	result.foreach(println)
	
	sc.stop()

sortBy

	// ============== Value类型 - sortBy ================
	
	/* 函数签名 :
	TODO
	 def sortBy[K](
	  f: (T) => K,
	  ascending: Boolean = true,
	  numPartitions: Int = this.partitions.length)
	  (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// sortBy : 按照规则对数据进行排序, 默认正序, 不改变分区数
	// 可以通过传递第二个参数改变排序的方式
	// 可以设定第三个参数改变排序后的分区数
	val rdd: RDD[Int] = sc.makeRDD(List(4, 5, 3, 1, 2), 3)
	
	// 默认升序
	val result1: RDD[Int] = rdd.sortBy(word => word)
	// 降序, 排序后分区设为2
	val result2: RDD[Int] = rdd.sortBy(word => word, false ,2)
	
	result1.foreach(println)
	result2.foreach(println)
	
	sc.stop()

pipe
函数签名 : def pipe(command: String): RDD[String]
管道,针对每个分区,都调用一次shell脚本,返回输出的RDD。
注意:shell脚本需要放在计算节点可以访问到的位置

  1. 编写一个脚本,并增加执行权限
	[root@linux1 data]# vim pipe.sh
	#!/bin/sh
	echo "Start"
	while read LINE; do
	   echo ">>>"${LINE}
	done
	
	[root@linux1 data]# chmod 777 pipe.sh
  1. 命令行工具中创建一个只有一个分区的RDD
	scala> val rdd = sc.makeRDD(List("hi","Hello","how","are","you"), 1)
  1. 将脚本作用该RDD并打印
	scala> rdd.pipe("/opt/module/spark/pipe.sh").collect()
	res18: Array[String] = Array(Start, >>>hi, >>>Hello, >>>how, >>>are, >>>you)

双Value类型

	// ============== 双Value类型 ================
	
	/* 函数签名 :
	TODO
	 def intersection(other: RDD[T]): RDD[T]
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// 当两个RDD的数据类型不一致时
	//     对于并集/交集/差集 : 编译不通过(泛型限制)
	//     对于拉链 : 正常拉链
	val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 3)
	val rdd2: RDD[Int] = sc.makeRDD(List(3, 4, 5, 6), 4)
	
	// intersection : 对源RDD和参数RDD求 <交集>   对应Scala中的intersect
	// 保留最大分区数, 数据被打乱重组(shuffle)
	val newRDD1: RDD[Int] = rdd1.intersection(rdd2)
	newRDD1.saveAsTextFile("output1")
	
	// union : 对源RDD和参数RDD求 <并集>
	// 数据合并, 分区也合并(numSlices1 + numSlices2)
	val newRDD2: RDD[Int] = rdd1.union(rdd2)
	newRDD2.saveAsTextFile("output2")
	
	// subtract : 对源RDD和参数RDD求 <差集>   对应Scala中的diff
	// 保留rdd1的分区数, 数据被打乱重组(shuffle)
	val newRDD3: RDD[Int] = rdd1.subtract(rdd2) // 1, 2
	newRDD3.saveAsTextFile("output3")
	
	// zip : 将两个RDD中的数据, 以键值对形式进行合并。其中Key为rdd1中的元素, Value为rdd2中的元素
	// 两个RDD的分区数一致, 但总数据量不一致 时
	//     对部分数据拉链 并 报异常 : Can only zip RDDs with same number of elements in each partition
	// 两个RDD的分区数不一致, 总数据量一致 时
	//     报异常 : Can't zip RDDs with unequal numbers of partitions: List(4, 3)
	val newRDD4: RDD[(Int, Int)] = rdd1.zip(rdd2)
	newRDD4.saveAsTextFile("output4")
	
	
	sc.stop()

Key-Value类型
partitionBy

	object RDD29_operator18_partitionBy {
	  def main(args: Array[String]): Unit = {
	    // =================== RDD转换算子 ================
	
	    // ============== Key - Value类型 - partitionBy ================
	    // Spark中很多方法是基于Key进行操作, 所以数据的理想格式为键值对或对偶元组
	
	    // 当RDD中数据类型为K-V类型时, Spark会自动补充很多算子(功能扩展) ==> 隐式转换
	    // partitionBy方法来自于PairRDDFunction类
	    // RDD的伴生对象中提供了隐式转换可以将PairRDDFunction中的功能扩展到RDD[K, V]类中
	
	    /* 函数签名 :
	    TODO
	     def partitionBy(partitioner: Partitioner): RDD[(K, V)]
	     */
	    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	    val sc = new SparkContext(sparkConf)
	
	    val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("d", 4)))
	
	    // partitionBy : 将数据按照指定分区规则进行重分区。
	    // 参数 : 分区器对象。HashPartitioner 或 RangePartitioner, Spark默认的分区器为HashPartitioner
	    // HashPartitioner的分区规则 : 将当前数据的key进行取余操作
	    val result1: RDD[(String, Int)] = rdd.partitionBy(new HashPartitioner(2))
	    result1.saveAsTextFile("output1")
	
	    // RangePartitioner的分区规则 : 按照key的大小匹配分区的设定范围。前提是key可比较大小
	    // sortBy使用了RangePartitioner
	
	    // 如果重分区的分区器和当前RDD的分区数一样, 那么不进行任何处理
	
	    // ============ 自定义分区器 ==================
	    val result2: RDD[(String, Int)] = rdd.partitionBy(MyPartitioner(3))
	
	    result2.mapPartitionsWithIndex(
	      (index, datas) => {
	        datas.map(
	          data => (index, data)
	        )
	      }
	    ).foreach(println)
	
	    sc.stop()
	
	  }
	
	  // 自定义分区器
	  // 继承Partitioner类并重写方法
	  case class MyPartitioner(num: Int) extends Partitioner {
	    // 获取分区的数量
	    override def numPartitions: Int = num
	
	    // 根据数据的key来决定数据在哪个分区中进行处理
	    // 方法的返回值为分区号(index)
	    override def getPartition(key: Any): Int = {
	      key match {
	        case "a" => 0
	        case "b" => 1
	        case _ => 2
	      }
	    }
	  }
	
	}

reduceByKey

	// ============== Key - Value类型 - reduceByKey ================
	
	/* 函数签名 :
	TODO
	 def reduceByKey(func: (V, V) => V): RDD[(K, V)]
	  def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("a", 3), ("b", 4)))
	
	// reduceByKey : 将数据按照相同的key对value进行聚合
	// 第一个参数表示 : 相同key的value的聚合方式
	// 第二个参数表示 : 聚合后的分区数
	val result: RDD[(String, Int)] = rdd.reduceByKey(_ + _, 2)
	
	result.foreach(println)
	
	sc.stop()

groupByKey

	// ============== Key - Value类型 - groupByKey ================
	
	/* 函数签名 :
	TODO
	  def groupByKey(): RDD[(K, Iterable[V])]
	  def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]
	  def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("a", 3), ("b", 4)))
	
	// groupByKey : 将分区的数据直接转换为相同类型的内存数组进行后续处理
	// 返回的数据类型为元组
	//     元组的第一个元素表示用于分组的key
	//     元组的第二个元素表示分组后, 相同key的value的集合
	val result: RDD[(String, Iterable[Int])] = rdd.groupByKey()
	
	result.foreach(println)
	
	// reduceByKey 与 groupByKey 的区别
	// 两个算子在实现相同的业务功能时, reduceByKey存在预聚合功能, 性能较高, 推荐使用
	// 当数据的处理不限于聚合操作时, 可采用groupByKey算子
	
	sc.stop()

aggregateByKey

    // ============== Key - Value类型 - aggregateByKey ================

    /* 函数签名 :
    TODO
     def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,
      combOp: (U, U) => U): RDD[(K, U)]
     */
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
    val sc = new SparkContext(sparkConf)

    // reduceByKey : 分区内和分区间计算规则相同
    // aggregateByKey : 分区间和分区间计算规则不同
    val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("c", 3), ("b", 4), ("c", 5), ("c", 6)), 2)

    // aggregateByKey : 将数据根据不同的规则进行区内计算和区间计算
    // 第一个参数列表 zeroValue : 计算的初始值, 用于在分区内计算时, 当作初始值
    // 第二个参数列表 seqOp : 分区内计算规则, 相同key的value的计算
    //              combOp : 分区间计算规则, 相同key的value的计算
    val result: RDD[(String, Int)] = rdd.aggregateByKey(0)(
      (x, y) => math.max(x, y), // 区内相同key的value取最大值
      (x, y) => x + y // 区间相同key的value求和
    )

    // 如果分区内计算规则和分区间的计算规则相同且都是求和, 那么可以计算WordCount
//    val result1: RDD[(String, Int)] = rdd.aggregateByKey(0)(
//      (x, y) => x + y,
//      (x, y) => x + y
//    )
    // 简化后
//    val result1: RDD[(String, Int)] = rdd.aggregateByKey(0)(_ + _, _ + _)
    // 或使用foldByKey()
    val result1: RDD[(String, Int)] = rdd.foldByKey(0)(_ + _)

    result.foreach(println)
    result1.foreach(println)

    // 对比Scala
    // List().reduce(_ + _)
    // List().fold(0)(_ + _)
    // Spark
    // rdd.reduceByKey(_ + _)
    // rdd.foldByKey(0)(_ + _)

    // reduceByKey 与 groupByKey 的区别
    // 两个算子在实现相同的业务功能时, reduceByKey存在预聚合功能, 性能较高, 推荐使用
    // 当数据的处理不限于聚合操作时, 可采用groupByKey算子

    sc.stop()

combineByKey

	// ============== Key - Value类型 - combineByKey ================
	
	/* 函数签名 :
	TODO
	 def combineByKey[C](
	  createCombiner: V => C,
	  mergeValue: (C, V) => C,
	  mergeCombiners: (C, C) => C): RDD[(K, C)]
	 */
	val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	val sc = new SparkContext(sparkConf)
	
	// combineByKey : 最通用的对key-value型rdd进行聚集操作的聚集函数(aggregation function)。
	// 类似于aggregate(),combineByKey()允许用户返回值的类型与输入不一致
	// 第一个参数表示 : 将计算的第一个key的value值转换结构
	// 第二个参数表示 : 分区内的计算规则
	// 第三个参数表示 : 分区间的计算规则
	
	// 需求 : 将数据List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))求每个key的value的平均值
	// 0 => [("a", 88), ("b", 95), ("a", 91)]
	// 1 => [("b", 93), ("a", 95), ("b", 98)]
	// 88 => (88, 1) + 91 => (179, 2) => (274, 3)
	val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)), 2)
	
	val result: RDD[(String, (Int, Int))] = rdd.combineByKey(
	  v => (v, 1),
	  (x: (Int, Int), y) => (x._1 + y, x._2 + 1), // 区内计算 : 数量叠加 ; 次数+1
	  (x: (Int, Int), y: (Int, Int)) => (x._1 + y._1, x._2 + y._2) // 区间计算 : 数量相加 ; 次数相加
	)
	result.map{
	  case (k, (total, cnt)) => (k, total / cnt)
	}.collect().foreach(println)
	
	sc.stop()

sortByKey

	  def main(args: Array[String]): Unit = {
	    // =================== RDD转换算子 ================
	
	    // ============== Key - Value类型 - sortByKey ================
	
	    /* 函数签名 :
	    TODO
	     def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length)
	      : RDD[(K, V)]
	     */
	    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
	    val sc = new SparkContext(sparkConf)
	
	    // sortByKey : 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的value
	    // 第一个参数 : true 升序 ; false 降序
	    // 第二个参数 : 排序后的分区数
	
	    // 需求 : 设置key为自定义User
	    val rdd: RDD[(User, Int)] = sc.makeRDD(
	      List(
	        (new User(), 3),
	        (new User(), 2),
	        (new User(), 4)
	      )
	    )
	
	    val result: RDD[(User, Int)] = rdd.sortByKey(true, 2)
	    result.collect().foreach(println)
	
	    sc.stop()
	  }
  // 继承Ordered, 混入Serializable
  class User extends Ordered[User] with Serializable {
    override def compare(that: User): Int = {
      1
    }
  }

join

  def main(args: Array[String]): Unit = {
    // =================== RDD转换算子 ================

    // ============== Key - Value类型 - join ================

    /* 函数签名 :
    TODO
     def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
     */
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
    val sc = new SparkContext(sparkConf)

    // join : 在类型为(K,V)(K,W)的RDD上调用,返回一个相同key对应的所有元素连接在一起的(K,(V,W))的RDD
    //        由于join过程存在shuffle过程和笛卡尔积现象, 故性能较低
    val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
    val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))

    val result: RDD[(Int, (String, Int))] = rdd1.join(rdd2)

    result.collect().foreach(println)

    sc.stop()
  }

leftOuterJoin

  def main(args: Array[String]): Unit = {
    // =================== RDD转换算子 ================

    // ============== Key - Value类型 - leftOuterJoin ================

    /* 函数签名 :
    TODO
     def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
     */
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
    val sc = new SparkContext(sparkConf)

    // leftOuterJoin : 类似于SQL语句的左外连接
    val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
    val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))

    val result: RDD[(Int, (String, Option[Int]))] = rdd1.leftOuterJoin(rdd2)

    result.collect().foreach(println)

    sc.stop()
  }

cogroup

  def main(args: Array[String]): Unit = {
    // =================== RDD转换算子 ================

    // ============== Key - Value类型 - cogroup ================

    /* 函数签名 :
    TODO
     def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]
     */
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
    val sc = new SparkContext(sparkConf)

    // cogroup : 在类型为(K,V)(K,W)的RDD上调用,返回一个(K,(Iterable<V>,Iterable<W>))类型的RDD
    val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
    val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))

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

    result.collect().foreach(println)

    sc.stop()
  }

RDD行动算子(Action)

  def main(args: Array[String]): Unit = {
    // =================== RDD行动算子(Action) ================
    // 转换算子不会触发作业的执行, 只是功能的扩展和包装

    // 所谓的行动算子, 不会再产生新的RDD, 而是触发作业的执行
    //      执行后, 会获取到作业的执行结果

    // Spark的行动算子执行时, 会产生Job对象, 然后提交这个Job对象

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

    // ============== reduce ================
    /* 函数签名 :
    TODO
     def reduce(f: (T, T) => T): T
     */
    // reduce  : 简化规约。
    //     聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据
    // 聚集 : 由多变少
    val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    val result1: Int = rdd1.reduce(_ + _)
    println(result1) // 10

    // ============== collect ================
    /* 函数签名 :
    TODO
     def collect(): Array[T]
     */
    // collect : 采集数据。
    //     在驱动程序中,以数组Array的形式返回数据集的所有元素
    val result2: Array[Int] = rdd1.collect()
    println(result2.mkString(", ")) // 1, 2, 3, 4

    // ============== count ================
    /* 函数签名 :
    TODO
     def count(): Long
     */
    // count : 返回RDD中元素的个数
    val result3: Long = rdd1.count()
    println(result3) // 4

    // ============== first ================
    /* 函数签名 :
    TODO
     def first(): T
     */
    // first : 返回RDD中的第一个元素
    val result4: Int = rdd1.first()
    println(result4) // 1

    // ============== take ================
    /* 函数签名 :
    TODO
     def take(num: Int): Array[T]
     */
    // take : 返回一个由RDD的前n个元素组成的数组
    val result5: Array[Int] = rdd1.take(2)
    println(result5.mkString(", ")) // 1, 2

    // ============== takeOrdered ================
    /* 函数签名 :
    TODO
     def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]
     */
    // takeOrdered : 返回该RDD排序后的前n个元素组成的数组
    val rdd2: RDD[Int] = sc.makeRDD(List(3, 2, 4, 1))
    val result6: Array[Int] = rdd2.takeOrdered(3)
    println(result6.mkString(", ")) // 1, 2, 3

    // ============== aggregate ================
    /* 函数签名 :
    TODO
     def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U
     */
    // aggregate : 分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合
    // aggregateByKey : 初始值只参与分区内的计算
    // aggregate : 初始值参与分区内计算和分区间计算
    val rdd3: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
    val result7: Int = rdd3.aggregate(0)(_ + _, _ + _)
    val result8: Int = rdd3.aggregate(10)(_ + _, _ + _)
    println(result7) // 10
    println(result8) // 40

    // ============== fold ================
    /* 函数签名 :
    TODO
     def fold(zeroValue: T)(op: (T, T) => T): T
     */
    // fold : 折叠操作,aggregate的简化版操作
    val result9: Int = rdd3.fold(0)(_ + _)
    val result10: Int = rdd3.fold(10)(_ + _)
    println(result9) // 10
    println(result10) // 40

    // ============== countByKey ================
    /* 函数签名 :
    TODO
     def countByKey(): Map[K, Long]
     */
    // countByKey : 统计相同key的value的个数
    val rdd4: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 1), ("a", 2)))
    val result11: collection.Map[String, Long] = rdd4.countByKey()
    println(result11.mkString(", ")) // a -> 2, b -> 1

    // ============== countByValue ================
    /* 函数签名 :
    TODO
     def countByValue()(implicit ord: Ordering[T] = null): Map[T, Long]
    */
    // countByValue : 统计RDD中元素的个数
    val rdd5: RDD[String] = sc.makeRDD(List("a", "b", "a"))
    val result12: collection.Map[(String, Int), Long] = rdd4.countByValue()
    val result13: collection.Map[String, Long] = rdd5.countByValue()
    println(result12.mkString(", ")) // (a,1) -> 1, (b,1) -> 1, (a,2) -> 1
    println(result13.mkString(", ")) // a -> 2, b -> 1

    // ============== save相关 ================
    /* 函数签名 :
    TODO
      def saveAsTextFile(path: String): Unit
      def saveAsObjectFile(path: String): Unit
      def saveAsSequenceFile(
        path: String,
        codec: Option[Class[_ <: CompressionCodec]] = None): Unit
     */
    // saveAsTextFile : 保存为Text文件
    // saveAsObjectFile : 序列化成对象保存到文件
    // saveAsSequenceFile : 保存成Sequencefile文件
    rdd4.saveAsTextFile("output1")
    rdd4.saveAsObjectFile("output2")
    rdd4.saveAsSequenceFile("output3")

    // ============== foreach ================
    /* 函数签名 :
    TODO
      def foreach(f: T => Unit): Unit = withScope {
        val cleanF = sc.clean(f)
        sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
      }
     */
    // foreach : 分布式遍历RDD中的每一个元素, 调用指定函数
    // RDD中的方法称之为算子,foreach非普通的方法, 是一种分布式的算子, 结果区内有序, 区间无序
    // 算子的逻辑代码时在分布式计算节点Executor执行的
    // 方法的逻辑代码时在Driver端执行的
    rdd3.collect().foreach(println) // 1, 2, 3, 4 // 此时为方法 : 结果有序
    rdd3.foreach(println) // 3, 4, 1, 2 // 此时为算子 : 结果无序

    sc.stop()
  }

RDD序列化

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

    // ======= 序列化方法和属性 ========
    // 从计算的角度, 算子以外的代码都是在Driver端执行, 算子里面的代码都是在Executor端执行
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Serializable")
    val sc = new SparkContext(sparkConf)

    val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello spark", "hive", "hello scala"))
    val search = new Search("hello")
//    search.getMatch1(rdd).collect().foreach(println)
    search.getMatch2(rdd).collect().foreach(println)

    sc.stop()
  }

  class Search(query: String) extends Serializable {
    def isMatch(s: String): Boolean = {
      s.contains(query)
    }

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

    // 属性序列化案例
    def getMatch2(rdd: RDD[String]): RDD[String] = {
      rdd.filter(x => x.contains(query))
//      val q = query
//      rdd.filter(x => x.contains(q)) // 可以不序列化
    }
  }
  def main(args: Array[String]): Unit = {
    // ========== Kryo序列化框架 ===========
    // Java的序列化的序列化能够序列化任何的类。但是比较重(字节多), 序列化后, 对象的提交也比较大。
    // Spark处于对性能的考虑, Spar2.0开始支持另外一种Kryo序列化机制。
    // Kryo速度是Serializable的10倍。当RDD在Shuffle数据时,
    // 简单数据类型、数组和字符串类型已经在Spark内部使用Kryo来序列化。

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

    val sc = new SparkContext(sparkConf)

    val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello scala", "scala", "spark"), 2)
    val searcher = new Searcher("hello")
    val result: RDD[String] = searcher.getMatch1(rdd)
    result.collect().foreach(println)

    sc.stop()
  }

  case class Searcher(query: String) {

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

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

    // 属性序列化
    def getMatch2(rdd: RDD[String]): RDD[String] = {
      rdd.filter(x => x.contains(query))
      //      val q = query
      //      rdd.filter(x => x.contains(q)) // 可以不序列化
    }
  }

RDD依赖关系

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

    // ======= RDD依赖关系 ======
    // 所谓的依赖关系, 就是RDD之间的关系
    // RDD 窄依赖 : 表示每一个父RDD的Partition最多被子RDD的一个Partition使用 (多对一)
    //        NarrowDependency
    // RDD 宽依赖 : 表示同一个父RDD的Partition被多个子RDD的Partition依赖, 会引起Shuffle (多对多)
    //        Dependency

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

    val rdd: RDD[String] = sc.makeRDD(List("Hello Scala", "Hello Spark", "Spark Scala"), 2)
    println("------- 血缘关系 --------")
    println(rdd.toDebugString)
    println("------- 依赖关系 --------")
    println(rdd.dependencies)

    val newRDD1: RDD[String] = rdd.flatMap(_.split(" "))
    println("------- 血缘关系 --------")
    println(newRDD1.toDebugString)
    println("------- 依赖关系 --------")
    println(newRDD1.dependencies)

    val newRDD2: RDD[(String, Iterable[String])] = newRDD1.groupBy(word => word)
    println("------- 血缘关系 --------")
    println(newRDD2.toDebugString)
    println("------- 依赖关系 --------")
    println(newRDD2.dependencies)

    val result: RDD[(String, Int)] = newRDD2.map(kv => (kv._1, kv._2.size))
    println("------- 血缘关系 --------")
    println(result.toDebugString)
    println("------- 依赖关系 --------")
    println(result.dependencies)

    println("------- 结果 --------")
    result.collect().foreach(println)

    sc.stop()
  }

RDD阶段划分
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。例如,DAG记录了RDD的转换过程和任务的阶段。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
阶段的个数取决于数据落盘/shuffle的次数。阶段数 = shuffle依赖数 + 1
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 Cache缓存

  def main(args: Array[String]): Unit = {
    // ======= RDD持久化 ========
    // 由于RDD中是不保存数据的, 如果多个RDD需要共享其中的一个RDD的数据, 那么必须从头执行, 效率很低,
    //    所以如果将一些重复性比较高, 比较耗时的操作的结果缓存起来, 这样就可以大大的提高效率。

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

    // > 默认的缓存是存储在Executor端的内存中, Cache底层其实是调用的persist方法
    // > persist方法在持久化数据时会采用不同的存储级别对数据进行持久化操作
    // > cache缓存的默认操作就是将数据缓存到内存MEMORY_ONLY
    // > cache存储的数据在内存中, 如果内存不够用, executor可以将内存的数据进行整理并丢弃部分数据
    //    如果由于executor端整理内存导致缓存的数据丢失, 那么数据操作依然要从头执行
    //    如果cache后的数据从头执行数据操作的话, 那么必须要遵循血缘关系, 所以cache操作不能删除血缘关系
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Cache")
    val sc = new SparkContext(sparkConf)
    val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    rdd.cache()

    // cache操作在行动算子执行后, 会在血缘关系中增加和缓存相关的依赖
    // cache操作不会切断血缘, 一旦发生错误, 可以重新执行
    val cacheRDD = rdd.cache()
    println(cacheRDD.toDebugString)
    println(cacheRDD.collect().mkString(", "))
    println(cacheRDD.toDebugString)

    sc.stop()
  }

RDD CheckPoint

  def main(args: Array[String]): Unit = {
    // ======= RDD CheckPoint =======
    // 所谓的检查点其实就是通过将RDD中间结果写入磁盘
    //由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,
    //    如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。
    //对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。

    // 检查点操作会切断血缘关系, 一旦数据丢失不会从头读取数据
    // 检查点可以将数据保存到分布式存储系统中, 数据相对来说比较安全, 不易丢失

    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Cache")
    val sc = new SparkContext(sparkConf)
    // 设置检查点路径
    sc.setCheckpointDir("./checkpoint1")
    // 创建RDD
    val rdd: RDD[String] = sc.textFile("input/word1.txt")
    // 业务逻辑
    val newRDD1: RDD[String] = rdd.flatMap(_.split(" "))
    val newRDD2: RDD[(String, Long)] = newRDD1.map((_, System.currentTimeMillis()))
    // 增加缓存
    newRDD2.cache()
    // 数据检查点, 针对newRDD2做检查点计算
    newRDD2.checkpoint()
    // 触发执行逻辑
    newRDD2.collect().foreach(println)

    sc.stop()
  }

缓存和检查点区别
1)Cache缓存只是将数据保存起来,不切断血缘依赖。Checkpoint检查点切断血缘依赖。
2)Cache缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint的数据通常存储在HDFS等容错、高可用的文件系统,可靠性高。
3)建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存中读取数据即可,否则需要再从头计算一次RDD。
RDD文件读取与保存

  def main(args: Array[String]): Unit = {
    // ======= RDD文件读取与保存 =======
    // Spark的数据读取及数据保存可以从两个维度来区分 : 文件格式 & 文件系统
    // 文件格式 : text文件、csv文件、sequence文件、Object文件
    // 文件系统 : 本地文件系统、HDFS、HBASE、数据库

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

    // text文件
    // 读取
    val rdd1: RDD[String] = sc.textFile("input/word1.txt")
    // 保存
    rdd1.saveAsTextFile("output1")

    // sequence文件
    // SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)//    在SparkContext中,可以调用sequenceFile[keyClass, valueClass](path)
    val rdd2: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3)))
    // 保存数据为sequenceFile
    rdd2.saveAsSequenceFile("output2")
    // 读取sequenceFile
    sc.sequenceFile[String, Int]("output2")

    // object对象文件
    // 对象文件是将对象序列化后保存的文件,采用Java的序列化机制。
    //    可以通过objectFile[T: ClassTag](path)函数接收一个路径,读取对象文件,
    //    返回对应的RDD,也可以通过调用saveAsObjectFile()实现对对象文件的输出。
    //    因为是序列化所以要指定类型
    // 保存数据
    rdd1.saveAsObjectFile("output3")
    // 读取数据
    sc.objectFile[String]("output3")

    sc.stop()
  }

累加器

  def main(args: Array[String]): Unit = {
    // ======= 累加器 Accumulator =======
    // 累加器用来把Executor端变量信息聚合到Driver端。在Driver程序中定义的变量,
    //    在Executor端的每个Task都会得到这个变量的一份新的副本,每个task更新这些副本的值后,
    //    传回Driver端进行merge

    // ====== 系统累加器 =======

    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Cache")
    val sc = new SparkContext(sparkConf)
    val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    // 声明累加器
    val sum: LongAccumulator = sc.longAccumulator("sum")
    rdd.foreach(
      num => {
        // 使用累加器
        sum.add(num)
      }
    )
    // 获取累加器的值
    println("sum = " + sum.value)

    // ======= 自定义累加器 ========

    val rdd2: RDD[String] = sc.makeRDD(List("hello scala", "hello spark"))
    // 1. 创建累加器
    val acc = new MyAccumulator()
    // 2. 注册累加器
    sc.register(acc)
    // 3. 使用累加器
    rdd2.flatMap(_.split(" ")).foreach(acc.add)
    // 4. 获取累加器的返回值
    println(acc.value)

    sc.stop()
  }

  // 自定义累加器
  // 继承AccumulatorV2
  // 定义泛型 [In, Out]
  //    In : 输入值的类型
  //    Out : 返回值的类型
  // 重写方法
  class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Int]] {

    // 存储WordCount的集合
    var wordCountMap: mutable.Map[String, Int] = mutable.Map[String, Int]()

    // 判断累加器是否为初始状态
    override def isZero: Boolean = {wordCountMap.isEmpty}

    // 赋值累加器
    override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {new MyAccumulator}

    // 重置累加器
    override def reset(): Unit = {wordCountMap.clear()}

    // 逻辑
    override def add(word: String): Unit = {
      // wordCountMap(word) = wordCountMap.getOrElse(word, 0) + 1
      wordCountMap.update(word, wordCountMap.getOrElse(word, 0) + 1)
    }

    // 合并累加器
    override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
      val map1 = wordCountMap
      val map2 = other.value

      // 两个Map的合并
      wordCountMap = map1.foldLeft(map2)(
        ( innerMap, kv ) => {
          innerMap(kv._1) = innerMap.getOrElse(kv._1, 0) + kv._2
          innerMap
        }
      )
    }

    // 返回累加器的值(Out)
    override def value: mutable.Map[String, Int] = wordCountMap
  }

广播变量

  def main(args: Array[String]): Unit = {
    // ======== 广播变量 - broadcast =======
    // 广播变量 : 分布式共享只读变量

    // 当Executor中有多个Task, 且拥有相同的数据, 那么会造成Executor中数据的冗余, 性能降低
    //    此时可以采用广播变量的方式, 将共有的数据保存到Executor的缓存区中
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("BroadCast")
    val sc = new SparkContext(sparkConf)

    val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("d", 4)), 4)
    val list = List(("a", 4), ("b", 5), ("c", 6), ("d", 7))

    // 声明广播变量
    val bcList: Broadcast[List[(String, Int)]] = sc.broadcast(list)

    rdd.map{
      case (key, num) => {
        var num2 = 0
        // 使用广播变量
        for((k, v) <- bcList.value){
          if(k == key) {
            num2 = v
          }
        }
        (key, (num, num2))
      }
    }.collect().foreach(println)

    sc.stop()
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值