Spark-Core
一、RDD概述
1.RDD(Resilient Distributed Dataset)
-
弹性分布式数据集,是Spark中的最基本数据抽象
-
在源码中是一个抽象类,代表一个弹性的,不可变、可分区、里面元素可以并行计算的集合。
- 弹性
- 存储的弹性:内存与磁盘的自动切换
- 容错的弹性:数据丢失可以自动恢复
- 计算的弹性:计算出错重试机制
- 分片的弹性:可根据需要重写分片
- 分布式
- 数据存储在大数据集群中的不同节点上
- 数据集,不存储数据
- RDD封装了计算逻辑,并不保存数据
- 数据抽象
- RDD是一个抽象类,需要子类具体实现。
- 不可变
- RDD封装了计算逻辑,不可以改变,想要改变只能生成新的RDD,在新的RDD中封装计算逻辑
- 可分区,并行计算
2.RDD的五大特性
-
一组分区(Partition),即是数据集的基本组成单位,标记数据来源于哪个分区
-
一个计算每个分区的函数
-
RDD之间的依赖关系
-
一个Partitioner,即RDD的分区器,控制分区数据的流向(KV结构)
-
一个列表,存储存取每个Partition的优先位置,移动数据不如移动计算,除非资源不够
二、创建RDD的方式
-
RDD创建方式分为三种:sc为SparkContext,即Spark连接
-
从集合中创建RDD
-
sc.parallelize(集合)
-
sc.makeRDD(集合)
makeRDD底层调用的就是parallelize
-
-
从外部存储中创建RDD
- 从外部存储中创建RDD包括:本地文件系统,Hadoop支持数据集,如HDFS,Hbase等
- sc.textFile(“文件路径”)
-
从其他RDD中转换新的RDD
- 调用RDD转换算子,运算后产生新的RDD
-
三、RDD的分区规则
1.RDD从集合中创建
- 默认:为cpu核心数的分区数量
- 使用parallelize或makeRDD创建RDD的时候也可以指定分区的数量
分区源码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E2dlULNU-1628181644666)(C:\Users\Jzs\AppData\Roaming\Typora\typora-user-images\image-20210727201546272.png)]
2.RDD从外部存储中创建
- 默认:取值为cpu核心数和2的最小值,一般为2
四、RDD的Transformation转换算子
1)value类型算子:
- 对一个RDD生效的算子
1.map():映射
-
功能说明:
- 参数为一个函数f,作用于RDD中的每一个元素。
当一个RDD执行 map(f:T=>U) 【T为每个元素的类型,U为计算后返回值的类型】
计算的时候,会遍历RDD中的每一个数据项,依次应用函数f,产生一个新的RDD。即返回的新RDD的每一个元素都是依次应用函数f得来的。
- 计算后的RDD不会改变原来的RDD分区
- 一个分区内的计算是有序的,不同分区间计算是无序的
-
代码说明:
//创建一个1,2,3,4且分区为2 的rdd:RDD val rdd: RDD[Int] = sc.makeRDD(1 to 4, 2) //将rdd经过map算子转换为每个元素*2 的新RDD val mapRdd: RDD[Int] = rdd.map(elem => elem *2)
2.mapPartitions():以分区为单位执行Map
-
功能说明:
-
参数(f:Iterator[T] => Iterator[U] )
【T为每个元素的类型,U为计算后每个元素的类型】
函数f将每个分区的元素放入一个迭代器中进行处理,返回处理后的迭代器
-
map()算子一次处理一个数据,mapPartitions一次处理一个分区的数据(批处理)
-
计算后不改变原来的分区数
-
-
代码说明:
//创建一个RDD,分区数为2 val rdd: RDD[Int] = sc.makeRDD(1 to 4, 2) //将一个分区的迭代器中每个元素map转换为2倍后返回新RDD val rdd1 = rdd.mapPartitions(iter=>iter.map(_*2))
3.map()和mapPartitions()的区别
- map()一次处理一条数据
- mapPartitions()一次处理一个分区的数据
- 因为mapPartitions()一次处理一个分区的数据,因此需要将整个分区数据加载到内存中,所以需要占用更多的内存资源。
4.mapPartitionsWithIndex():带分区号
-
**参数:**函数f:(Int,Iterator[T]) => Iterptor[U] 【Int表示分区编号】
-
功能说明:
和mapPartitions相同,但增加了分区编号
-
代码说明
//创建一个RDD val rdd: RDD[Int] = sc.makeRDD(1 to 4, 2) //使每个元素和所在分区号形成一个元组,组成一个新的RDD val indexRdd = rdd.mapPartitionsWithIndex( (index,iter)=>iter.map((index,_)) )
5.flatMap():扁平化
-
**参数:**函数(f:list => list),将RDD中List的多个子List扁平化为一个大List
-
功能说明:
将RDD中的每一个元素经过函数f将数据拆分到一个大集合中返回
-
代码说明:
//创建一个RDD,一个集合中包含多个子集合 val listRDD=sc.makeRDD(List(List(1,2),List(3,4),List(5,6),List(7)), 2) //通过flatMap()算子将所有子集合中数据取出放入到一个大的集合中,并收集打印 listRDD.flatMap(list=>list).collect.foreach(println)
-
flatMap()中所有元素必须都是集合
6.golm():分区转换数据
-
无参数
-
功能说明:
将RDD中的每个分区变成一个数组,返回一个新的RDD。新RDD中元素与原来的类型一致
-
代码说明:
//创建一个RDD,分区数为2 val rdd = sc.makeRDD(1 to 4, 2) //将每个分区的元素转换为数组,并通过map()求出每个风区中的最大值 val maxRdd: RDD[Int] = rdd.glom().map(_.max) //将新RDD收集并求出所有风区最大值之和 println(maxRdd.collect().sum)
7.groupBy():分组
-
参数:函数(f:T => U ) 如:elem => elem 【通过相同的元素聚合】
-
返回值:Tuple2(elem,CompactBuffer(elem,elem,… ))
-
功能说明:
按照传入函数f的返回值进行分组,将计算结果相同的元素放入一个迭代器中。
-
代码说明:
//创建一个RDD,分区为2 val rdd = sc.makeRDD(1 to 4, 2) //将每个%2结果相同的元素放入一个迭代器中 rdd.groupBy(_ % 2).collect().foreach(println) //创建一个RDD val rdd1: RDD[String] = sc.makeRDD(List("hello","hive","hadoop","spark","scala")) //按照首字母第一个单词相同分组 rdd1.groupBy(str=>str.charAt(0)).collect().foreach(println)
//打印结果: (0,CompactBuffer(2, 4)) (1,CompactBuffer(1, 3)) (s,CompactBuffer(spark, scala)) (h,CompactBuffer(hello, hive, hadoop))
8.filter():过滤
-
**参数:**函数(f:T => Boolean) 如(_%2 ==1),即保留结果为true的元素
-
功能说明:
将RDD中的每一个元素经过函数f后返回一个Boolean类型的结果。结果为true的元素保留,false丢弃。
-
代码说明:
//创建一个RDD,分区数为2 val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4), 2) //将每个元素%2,值等于0的,即返回值为true的加入到新的RDD,返回值false的丢弃 val filterRdd: RDD[Int] = rdd.filter(_ % 2 == 0)
9.sample():采样
-
参数:
-
withReplacement: Boolean, 【参数1:Boolean类型。true表示取出后放回抽样,所以能被重复抽到, fase表示取出后不放回,所以最多被抽1次】
-
fraction: Double, 【参数1为false时,取值 [ 0,1 ] 表示每个元素被取出的概率
参数1位true时,取值 [ 1-无穷 ] 表示每个元素可能被抽取的次数的概率最高 】
-
seed: Long = Utils.random.nextLong): RDD[T] 【Seed:随机数种子,默认和当前时间戳有关,若指定随 机数种子的值,则采样后值将固定,重复执行结果不变】
-
-
功能说明:
从数据中采样
-
代码说明:
//3.1 创建一个RDD val dataRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4,5,6)) // 抽取数据不放回(伯努利算法) // 伯努利算法:又叫0、1分布。例如扔硬币,要么正面,要么反面。 // 具体实现:根据种子和随机算法算出一个数和第二个参数设置几率比较,小于第二个参数要,大于不要 // 第一个参数:抽取的数据是否放回,false:不放回 // 第二个参数:抽取的几率,范围在[0,1]之间,0:全不取;1:全取; // 第三个参数:随机数种子 val sampleRDD: RDD[Int] = dataRDD.sample(false, 0.5) sampleRDD.collect().foreach(println) println("----------------------") // 抽取数据放回(泊松算法) // 第一个参数:抽取的数据是否放回,true:放回;false:不放回 // 第二个参数:重复数据的几率,范围大于等于0.表示每一个元素被期望抽取到的次数 // 第三个参数:随机数种子 val sampleRDD1: RDD[Int] = dataRDD.sample(true, 2) sampleRDD1.collect().foreach(println)
10.distinct():去重
-
参数:
- 默认为空参,去重后的分区数和去重前一致
- 指定参数(numPartitions:Int),可以改变去重后的分区数量
-
功能说明:
对内部的元素进行去重,去重后放入新的RDD中
-
与HashSet的去重不同点:
-
HashSet通过将元素放入一个HashSet实现自动去重,但需要将所有元素全部放入一个HashSet,需要消耗大量内存,可能出现OOM(内存溢出)
-
distinct()去重的内部源码:map(x => (x, null)).reduceByKey((x, ) => x, numPartitions).map(._1)
通过将元素map转换,再reduceByKey聚合最后map保留元素本身的方式去重
这种方式使用了Spark算子的多分区并行操作,解决了内存溢出的问题
-
-
代码说明:
// 创建一个RDD val distinctRdd: RDD[Int] = sc.makeRDD(List(1,2,1,5,2,9,6,1)) // 打印去重后生成的新RDD distinctRdd.distinct().collect().foreach(println) // 对RDD采用多个Task去重,提高并发度 distinctRdd.distinct(2).collect().foreach(println)
-
distinc()去重会执行shuffle过程
11.coalesce():分区合并
-
参数:
- numPartitioons:Int 【合并后的分区数,可以大于原来的分区数,也可以小于原来的分区数】
- shuffle:Boolean = false 【默认为false,不走shuffle流程,true则走shuff了流程】
-
功能说明:
默认值为false,不走shuffle流程,适合用于减小分区数的操作。
如果增加分区数,需要将shuffle设置为true。不走shuffle且扩大分区没有意义,元素仍在原来的分区之中。
-
代码说明:
// 创建一个RDD,分区数为2 val rdd: RDD[Int] = sc.makeRDD(Array(1,