文章目录
环境的配置
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("test")
val sc = new SparkContext(sparkConf)
其中“local[*]”中的 星号 代表你的机器上处理器的核数
RDD的创建
通常来说有两种创建方法 “内存、文件”
//内存
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4),2)
//文件
val line: RDD[String] = sc.textFile("datas/apache.log",2)
//后面的2,代表分区的数量,如果省略则按照处理器核数分配
//我们可以用saveAsTextFile将处理的数据保存多个分区的文件
rdd.saveAsTextFile("output")
1、rdd计算 一个分区 内的数据是 一个一个 的执行的,上一数据全部逻辑执行完毕再到下一个
2、有多个分区的话,计算是无序的
value类型
1. map算子和mapPartitions算子
区别:map他是一个一个数据处理,mapPartitions是批处理,一次性从分区中拿取数据进行处理
上图可以看到,我们划分了两个分区,也打印了两次“>>>>>>>”
需要注意的是!
mapPartitions是将整个分区的数据进行计算,而数据会存在内存中,如果你的内存小,数据大的情况,有可能会造成内存溢出
一个小demo
“获取每个分区中的最大值”
因为mapPartitions需要传入一个迭代器,返回一个迭代器,所以我们iter.max拿到的结果并不是迭代器只是一个数,通过List中的iterator方法返回迭代器
2.mapPartitionsWithIndex算子
可以操作分区
小demo
“获取到1号分区的数据,如果不是1号分区就为空”
我想看看每个分区里面有什么数据
3.flatMap算子
压扁,拆分!分成一个一个数据
我想要一个一个的单词
上图的这样数据,我需要“压”两次才可以得到每一个单词
1、得到的是“hello scala”“good byb”
2、得到的是每一个单词了
如果存在是不同的数据结构,可以使用模式匹配
4.glom函数
将同一个分区的数据转换为同类型的内存数组进行处理
一个小demo
5.groupBy算子
按照指定的规则进行分组,数据会被打乱,也就是Shuffle
例如:
本来数据“1,2,3,4”,
第0分区是1,2
第1分区是3,4
我用groupBy(_%2)(也就是通过取2的余数进行分组)后分区变成了
第0分区是1,3
第1 分区是2,4
6.filter算子
符合规则的数据保留,不符合的过滤掉(
1模2后等于1不等于0,符合规则,保留!
2模2后等于0,不符合规则,过滤!
3模2后等于1不等于0,符合规则,保留!
4模2后等于0,不符合规则,过滤!
)
但是!
这个算子由于是过滤操作,所以可能出现数据倾斜
例如:
我分区“1”和分区“2”的数据都是1000条
经过filter操作后
分区“1”中的数据还剩下999条,过滤了1条
分区“2”中的数据还剩下110条,过滤了好多条
一个999,一个110,这个是不是造成了数据的一个不平衡
这种不平衡我们就叫做数据倾斜
7.sample算子
根据指定的规则从数据集中抽取数据
作用?
可以抽取分区中的数据,看是否数据倾斜,例如我抽了很多次,某一个数据出现了多次,我是不是有理由怀疑这个数据有问题
8.distinct算子
去重
9.coalesce算子
缩减分区,合并分区
coalesce算子默认情况下不会将数据打乱重新组合,不会shuffle
这就会出现数据倾斜
这里将1,2,3,4,5,6分成3个分区后,进行合并分区操作,最终变成两个分区。
分区0是1,2
分区1是3,4,5,6
这是因为没有进行shuffle操作,单纯的合并分区,会有数据倾斜的情况
如果想要避免合并分区后,没有数据倾斜的情况,可以使用第二个参数,true
设置为true后,数据会进行shuffle操作,分区会变为
分区0是1,4,5
分区1是2,3,6
扩大分区怎么弄?
我们可以理解,如果不将数据打乱,怎么扩大分区呢?所以理所当然的要使用第二个参数true
repatition算子,其实底层就是coalesce操作,而且采用的是shuffle操作,无论缩减分区还是扩大分区,都是shuffle
10.sortBy算子
根据给定的规则进行排序
根据List中元组的第一个数据的Int类型进行排序
第二个参数false就变成降序了,默认升序
排序后的分区数量还是一样的,但是中间会进行shuffle操作,这里不难理解,因为本来一个没有顺序的数据,变成了有顺序的,那是不是得进行shuffle操作?
双value类型(交集,并集,差异,拉链)
交集 intersection
并集 union
差集 subtract
拉链 zip
zip需要注意的是,无论是分区数量,还是分区中的数据数量要保持一致。
Key-Value类型
1.partitionBy算子
按照指定的partition重新进行分区
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1,"aaa"),(2,"bbb"),(3,"ccc")),3)
val rdd2: RDD[(Int, String)] = rdd.partitionBy(new HashPartitioner(2))
2.reduceByKey算子
相同的key的数据,进行value数据的聚合操作
根据相同的key 也就是“a”放一组,“b”放一组,然后各自的value进行聚合(此处是累加)。
“a”中的数据为1,2,3,reduceByKey后就是,1 + 2 =3,然后在用1+2=3的结果再加下一个数3+3=6
存在shuffle操作
reduceByKey支持分区内进行预处理聚合功能,可以减少shuffle落盘的数据量,提升shuffle性能
因为reduceByKey是两两聚合,只要有两个相同的Key就可以进行预聚合了!
reduceByKey分区内和分区间计算规则是相同的
3.groupBykey算子
相同的key进行分组
groupByKey和之前的groupBy的区别?
首先,groupByKey是用Key进行分组,而groupBy是不确定的
其次,返回的类型不一样
groupByKey会被打乱,shuffle
这里我抛出了一个问题,那就是我想要分组后得到累加的值呢?
我是不是得map?
但是啊!我要map的时候我是不是得等待,等待分组,什么意思?
分区0(a,1)(a,1)(a,1)(b,1)
分区1(b,1)(b,1)(b,1)(a,1)
我得变成
( a,1)(a,1)(a,1)(a,1)
(b,1)(b,1)(b,1)(b,1)
这样我才可以进行map操作变成(a,4)(b,4)
但是我怎么知道我什么时候完成分组呢,这就需要落盘了也就是磁盘
shuffle操作必须落盘,不能在内存中数据等待,会导致内存溢出,shuffle性能非常低
reduceBykey和groupByKey都存在Shuffle
因为reduceByKey有预聚合功能,所以落盘数据较少,而groupByKey并不会减少数据量,因此reduceByKey的性能高
reduceByKey包含了分组和聚合,groupBykey只能分组
4.aggregateByKey算子
根据不同的规则进行分区内和分区间的计算
// TODO : 取出每个分区内相同 key 的最大值然后分区间相加
// aggregateByKey 算子是函数柯里化,存在两个参数列表
// 1. 第一个参数列表中的参数表示初始值
// 2. 第二个参数列表中含有两个参数
// 2.1 第一个参数表示分区内的计算规则
// 2.2 第二个参数表示分区间的计算规则
val rdd =
sc.makeRDD(List(
("a",1),("a",2),("c",3),
("b",4),("c",5),("c",6)
),2)
// 0:("a",1),("a",2),("c",3) => (a,10)(c,10)
// => (a,10)(b,10)(c,20)
// 1:("b",4),("c",5),("c",6) => (b,10)(c,10)
val resultRDD =
rdd.aggregateByKey(10)(
(x, y) => math.max(x,y),
(x, y) => x + y
)
resultRDD.collect().foreach(println)
来一个小demo
计算相同Key的平均值
数据是这样的
(“a”, 1), (“a”, 2),(“b”, 3)
(“b”, 4), (“b”, 5), (“a”, 6)
因为我们要计算相同key的平均值,数值和次数都要记录,最后一除就能得到了
1.我们设置了初始值,所以会先把初始值传进来
(“a”,(0,0)) …
2.分区内的计算规则
上图中 t 是一个元组,(0,0),v是相同key的一个值(‘a’,1)中的1
元组中的第一个数我们用来记录累加值,所以可以用t._1+v来表示
元组中的第二个数我们用来记录次数,所以用t._2+1来表示
3.分区间的计算规则
分区内的计算得到的结果是
分区0 (“a”,(3,2)),(“b”,(3,1))
分区1 (“a”,(6,1)),(“b”,(9,2))
我们要把相同的Key里面的累加值和次数都要进行累加
得到 (“a”,(9,3)),(“b”,(12,3))
4.将相同的key的值相除
mapValue可以取出value的值
9/3=3
12/3=4
最后的结果就是 (“a”,3),(“b”,4)
5.combineByKey算子
功能和上面的aggregateByKey相同,只是没有初始值的概念,将第一个数据进行格式的一个转换就能得到初始值
将(“a”,1)变成了(“a”,(1,1))
因为没有初始值,所以后面的t要加上类型
6.flodByKey算子
分区内和分区间相同的计算规则
(其实就是aggregateByKey的第二个参数列表写了两个相同的参数)
同样的,第一个参数列表写入的是初始值,第二个参数列表传的是计算规格
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 = dataRDD1.foldByKey(0)(_+_)
上面说到的reduceByKey,flodByKey,CombineByKey,aggregateByKey有什么区别呢?
reduceByKey:第一个数据不进行处理,分区内和分区间的数据进行 同样 的计算
flodBykey:传入一个 初始值 ,分区内和分区间的数据进行 同样 的计算
aggregateBykey:传入一个 初始值 ,分区内和分区间的数据进行 不一样 的计算
combineByKey:将第一个数据进行格式的转换,分区内和分区间的数据进行 不一样 的计算
上原码
上面这个就是combineByKeyWithClassTag里面的
7.join,leftOuterJon算子
相同的Key进行join
返回的是一个tuple
8.cogroup
其实就是connect+group
rdd1.cogroup(rdd2)
行动算子
1.reduce算子
2.collect算子
将不同分区的数据按照分区顺序采集到Driver端的内存中,形成数组
3.count算子
4.first算子
5.take算子
6.takeOrdered算子
数据排序后,取n个数据
7.aggregate算子
用法和aggregateByKey类似
aggregateByKey:初始值只会参与分区内计算
aggregate:初始值会产于分区内和分区间计算
8.flod算子
分区内和分区间计算规则一样的时候使用
9.countByKey,contByValue
统计次数
save算子
// 保存成 Text 文件
rdd.saveAsTextFile("output")
// 序列化成对象保存到文件
rdd.saveAsObjectFile("output1")
// 保存成 Sequencefile 文件,要求数据格式必须是K-V类型
rdd.map((_,1)).saveAsSequenceFile("output2")
foreach算子
分布式遍历RDD中的每一个元素
他和collect算子有什么区别呢?
collect是把数据采集回来,在Driver进行打印
而foreach算子是在executor端进行打印,并不能确定先打印的顺序
八种不同的WordCount方式
wordCount练习
都去敲一下!!!
血缘
依赖(窄依赖,宽依赖)
窄依赖:一对一,父的RDD的分区被一个子RDD分区依赖(独生子女)
宽依赖:一个父的RDD的分区被多个子RDD分区依赖(多生)(Shuffle依赖)
持久化操作cache(),persist(),checkpoint()
为什么要持久化?
先看这段代码
同样是用的上一个RDD进行的操作,但是底层却不是的,而是全部再执行一次,因为spark并不保存数据,当上一个RDD(也就是mapRDD)被reduceByKey使用后,它就会消失,所以groupByKey使用mapRDD并不是直接拿过来用,而是重新进行的。
事实上
但是当我们持久化后,就可以了(mapRDD.cache)
cache默认持久化操作,只能保存在 内存 中,如果想要保存到磁盘文件,需要更改存储级别
默认情况下 cache()=persist() 这两个方法是相同的
我们可以更改为
mapRDD.persist(StorageLevel.DISK_ONLY)
需要注意的!!!
持久化操作必须在行动算子执行时完成
checkpoint() 需要落盘,传入指定的保存点的路径
所以checkpoint()和persist()的落盘操作的不同点就是,persist()的落盘是保存为一个临时文件,当程序执行完成后,会删除文件的。而checkpoint()执行完成后文件并不会删除。
cache(),persist(),checkpoint()这三者有什么不同?
1.cache(),将数据临时存储在内存中进行数据的重用,数据会不安全,当内存溢出时会造成数据的丢失。在血缘关系中添加新的依赖,一旦出现问题可以重头读取数据。
2.persist(),将数据临时存储在磁盘文件中进行数据重用,涉及到磁盘IO,性能较低,但是数据安全
3.checkpoint(),将数据长久的存储在文件中,涉及到磁盘IO,性能较低,数据安全。一般情况下回独立执行作业,为了提高效率,需要和cache一起使用。执行过程中,会切断血缘关系,建立新的血缘关系,等同于改变数据源。
mapRDD.cache()
mapRdd.checkpoint()
自定义分区
只有RDD Key-Value类型的才有分区器
累加器
累加器就是把Executor端的变量信息聚合到Driver端。
val rdd = sc.makeRDD(List(1,2,3,4,5))
// 声明累加器,longAccumulator就是累加器的声明了
var sum = sc.longAccumulator("sum");
rdd.foreach(
num => {
// 使用累加器
sum.add(num)
}
)
// 获取累加器的值
println("sum = " + sum.value)
如果不定义累加器,就是这种情况
结果打印出了sum的值还是0,因为Executor并没有把sum累加后的值传回给Driver端。
因此需要使用累加器。
在 Driver 程序中定义的变量,在Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后,传回 Driver 端进行 merge。
他会出现 少加 或者 多加 的情况
一般的话,累加器会放置在行动算子中进行操作。
自定义累加器
import org.apache.spark.rdd.RDD
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable
object test2 {
def main(args: Array[String]): Unit = {
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("test2")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.makeRDD(
List("Hello", "Hello", "Hello", "Hello", "Spark", "Spark", "Hive", "Hive"), 2)
//创建累加器
val wcAcc = new MyAccumulator
//注册累加器
sc.register(wcAcc,"wordCount")
rdd.foreach(
word => {
//累加,调用wcACC对象中的add方法
wcAcc.add(word)
}
)
println(wcAcc.value)
}
class MyAccumulator extends AccumulatorV2[String,mutable.Map[String,Long]]{
// 定义输出数据集合,一个可变的Map
var wcMap: mutable.Map[String, Long] = mutable.Map[String, Long]()
// 是否为初始化状态,如果集合数据为空,即为初始化状态
override def isZero: Boolean = wcMap.isEmpty
// 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = new MyAccumulator()
// 重置累加器
override def reset(): Unit = wcMap.clear()
// 增加数据(分区内的计算)
override def add(word: String): Unit = {
//传入word 也就是每个单词,看看如果map中存在word就返回word对应的值,不存在就返回0
//当第一个单词“Hello”传进来时候,map中是没有的,所以返回0,然后加1,newCnt=1
//更新wcMap(Hello,1)
//当第而个单词“Hello”传进来时候,map中是有的,所以返回它的值为 1,然后加1,newCnt=2
//更新wcMap(Hello,2)
val newCnt: Long = wcMap.getOrElse(word, 0L) + 1
wcMap.update(word,newCnt)
}
// 合并累加器(分区间的计算)
override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {
val map1: mutable.Map[String, Long] = this.wcMap
val map2: mutable.Map[String, Long] = other.value
map2.foreach{
case (word,count) =>{
val newCount: Long = map1.getOrElse(word, 0L) + count
map1.update(word,newCount)
}
}
}
//累加器的值
override def value: mutable.Map[String, Long] = {
wcMap
}
}
}
广播变量
val conf = new SparkConf().setAppName("WordCount").setMaster("local")
val sc = new SparkContext(conf)
val pairRdd = sc.parallelize(List((1,1), (5,10), (5,9), (2,4), (3,5), (3,6),(4,7), (4,8),(2,3), (1,2)),4)
//将字段收集到Driver端,并广播发送到Executor
val allIpAndProvince: Array[(Int, Int)] = pairRdd.reduceByKey(_+_).collect()
//广播发送 返回引用
val broadCast: Broadcast[Array[(Int, Int)]] = sc.broadcast(allIpAndProvince)
val value24: RDD[(Int, Int)] = pairRdd.map(x => {
//使用广播变量 进行优化
val value: Array[(Int, Int)] = broadCast.value
val map: Map[Int, Int] = value.toMap
(x._1, x._2 + map.get(x._1).get)
})