文章目录
Spark核心编程
Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于处理不同的应用场景。三大数据结构分别是:
- RDD : 弹性分布式数据集
- 累加器:分布式共享只写变量
- 广播变量:分布式共享只读变量
接下来我们一起看看这三大数据结构是如何在数据处理中使用的。
1. RDD
RDD与IO
一、IO
二、RDD
- RDD的数据处理方式类似于IO流,也有装饰者设计模式
- RDD的数据只有在调用collect方法时,才会真正执行业务逻辑操作。之前的封装全部都是功能的扩展
- RDD是不保存数据的,但是IO可以临时保存一部分数据
什么是 RDD?
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
- 弹性
- 存储的弹性:内存与磁盘的自动切换;
- 容错的弹性:数据丢失可以自动恢复;
- 计算的弹性:计算出错重试机制;
- 分片的弹性:可根据需要重新分片。
- 分布式:数据存储在大数据集群不同节点上
- 数据集:RDD 封装了计算逻辑,并不保存数据
- 数据抽象:RDD 是一个抽象类,需要子类具体实现
- 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD 里面封装计算逻辑
- 可分区、并行计算
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 的创建方式可以分为四种:
- 从集合(内存)中创建 RDD
- 从外部存储(文件)创建RDD
- 从其他 RDD 创建
- 直接创建 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()
}
}
细节:
-
如果两个RDD 数据类型不一致怎么办?
- 交集,并集和差集要求两个数据源数据类型保持一致
- 拉链操作两个数据源的类型可以不一致
-
对拉链来说:如果两个RDD 数据分区不一致怎么办?
-
对拉链来说:如果两个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()
}
}
- 思考一个问题:如果重分区的分区器和当前RDD 的分区器一样怎么办?
- 思考一个问题:Spark 还有其他分区器吗?
- 思考一个问题:如果想按照自己的方法进行数据分区怎么办?
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 序列化框架
参考地址: https://github.com/EsotericSoftware/kryo
Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。Spark 出于性能的考虑,Spark2.0 开始支持另外一种Kryo 序列化机制。Kryo 速度是 Serializable 的 10 倍。当 RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型已经在 Spark 内部使用 Kryo 来序列化。
注意:即使使用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()
}
}
三、缓存和检查点区别
- Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint 检查点切断血缘依赖。
- Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint 的数据通常存储在HDFS 等容错、高可用的文件系统,可靠性高。
- 建议对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 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
- 文件格式分为:text 文件、csv 文件、sequence 文件以及Object 文件;
SequenceFile 文件是Hadoop 用来存储二进制形式的key-value 对而设计的一种平面文件(Flat File)。在 SparkContext 中,可以调用sequenceFile[keyClass, valueClass](path)。
对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。可以通过objectFile[T: ClassTag](path)函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。
- 文件系统分为:本地文件系统、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()
}
}