Spark(二)Spark Core
一.RDD
1.概念
RDD:弹性分布式数据集,是Spark中最基本的数据抽象,用来表示分布式集合,支持分布式操作,代表一个不可变、只读的,被分区的数据集
弹性
-
存储的弹性:内存与磁盘的自动切换
-
容错的弹性:数据丢失可以自动恢复
-
计算的弹性:计算出错重试机制
-
分片的弹性:可根据需要重新分片
分布式:数据存储在大数据集群不同节点上
数据集:RDD封装了计算逻辑,并不保存数据,读取数据后会经过一系列的计算逻辑直到将结果保存或显示
数据抽象:RDD是一个抽象类,需要子类具体实现
不可变:RDD封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD里面封装计算逻辑
可分区、并行计算
RDD五大属性
1)A list of partitions
RDD 数据结构中存在分区列表,用于执行任务时并行计算,是实现分布式计算的重要属性
2)A function for computing each split
Spark 在计算时,是使用分区函数对每一个分区进行计算
3)A list of dependencies on other RDDs
RDD 是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个 RDD 建立依赖关系
4)Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
当数据为KV 类型数据时,可以通过设定分区器自定义数据的分区
5)Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
一组最优的数据块的位置,这里涉及到数据的本地性和数据位置最优spark后期在进行任务调度的时候,会优先考虑存有数据的worker节点来进行任务的计算。大大减少数据的网络传输,提升性能
2.RDD转换算子
2.1 RDD两类算子
Transformation:返回一个新的RDD,它不会立即执行,需要Action函数触发
Action:返回值不是RDD,它会立即执行

2.2 RDD创建
RDD中的数据可以来源于2个地方:本地集合或外部数据源
- sc.parallelize(本地集合,分区数)
- sc.makeRDD(本地集合,分区数) 底层是parallelize
- sc.textFile(本地/HDFS/文件夹,分区数) 以行为单位读取数据
- sc.wholeTextFiles(本地/HDFS/文件夹,分区数) 以文件为单位,专门读取小文件,结果是元组,第一个元素是文件路径,第二个元素是文件内容
2.3 RDD分区
- rdd.getNumPartitions 获取分区数,底层是partitions.length
- rdd.partitions.length 获取分区数
object RddCreate {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val list = List[String]("liu", "guan", "zhang")
val list2 = ListBuffer[Int]() // 可变List
list2.append(1,2,3,10,18)
val rdd: RDD[String] = sc.parallelize(list, 2)
val rdd2: RDD[Int] = sc.makeRDD(list2, 3) // 底层是parallelize
val rdd3: RDD[String] = sc.textFile("hdfs://192.168.10.102:8020/input/spark")
val rdd4: RDD[(String, String)] = sc.wholeTextFiles("F:\\input\\small",1)
val partitions: Int = rdd.getNumPartitions
println(partitions)
val partitions1: Int = rdd2.getNumPartitions
println(partitions1)
val partitions2: Int = rdd3.getNumPartitions
println(partitions2)
val partitions3: Int = rdd4.getNumPartitions
println(partitions3)
}
}

2.4 RDD Value类型算子
map是对RDD中每个分区的数据逐条进行自定义转换
flatMap是对数据先映射后扁平化,返回TraversableOnce
mapPartitions则是对RDD中的每个分区的迭代器进行操作,数据已经在迭代器里,返回一个迭代器
map 算子因为类似于串行操作,所以性能比较低,而是 mapPartitions 算子类似于批处理,所以性能较高。但是mapPartitions 算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用。使用map操作
mapPartitionsWithIndex是对每个分区的迭代器进行操作,并且具有分区号
综合示例
object RddPartition {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
val list: ListBuffer[Map[Int, Tuple2[String,Int]]] =
new ListBuffer[Map[Int, Tuple2[String,Int]]]()
val lines: RDD[String] = sc.textFile("F:\\input\\wordcount",2)
// flatMap扁平化映射,每一行以_分割后组成一个集合,最后将所有集合再整合为一个集合
lines.flatMap(_.split(" "))
// mapPartitions以分区为单位进行操作
.mapPartitions(iter =>{
// map映射
iter.map((_,1))
})
.reduceByKey(_ + _)
.mapPartitionsWithIndex((index, iter) => {
iter.foreach(value => {
val data = Map[Int, Tuple2[String, Int]](index -> value)
list += data
})
list.iterator
}).foreach(map => println(map))
}
}

glom将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变
groupBy根据一个函数的规则进行分组
filter将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃
sample根据指定的规则从数据集中抽取数据
distinct去重
repartition可以增加或减少分区,输入几分区数就变为几。注意:原来的RDD不变,因为产生了新的RDD
coalesce(Int,shuffle) 默认减少分区,如果把shuffle参数指定为true,数据将会被打乱重新分区,并且具有了扩大分区的功能,否则原来分区的数据不会被分开,且只能缩小分区。注意:原来的RDD不变,因为产生了新的RDD
sortBy 每个分区内单独排序
object RddValue {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val list: ListBuffer[Int] = new ListBuffer[Int]() // 可变List
list.append(1,2,3,4,5,6,7,8,8,8,6)
val rdd: RDD[Int] = sc.parallelize(list,3)
// glom转换成一个类型相同的数组
val value: RDD[Array[Int]] = rdd.glom()
// gruopBy(function)
val value1: RDD[(Boolean, Iterable[Int])] = rdd.groupBy(num => {
num < 5 // 根据num小于5分组
})
value1.foreach(data => {
println(data._2)
})
// filter根据条件过滤 RDD中小于4的元素
rdd.filter(num => {
num < 4
}).foreach(println)
// sample根据指定的规则从数据集中抽取数据
// false代表每个数据被抽到不放回,0.3代表每个数据被抽取到的概率
// true代表每个数据被抽到放回,第二个参数代表期望被抽到的次数
rdd.sample(false,0.3)
// distinct去重
rdd.distinct().foreach(println)
// sortBy 每个分区内单独排序,第二个参数控制升降序
rdd.sortBy(num =>{
num
},false).saveAsTextFile("./sort")
}
}
2.5 RDD 双Value类型算子
就是要求有两个RDD集合参与运算
object RddValues {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val list: ListBuffer[Int] = new ListBuffer[Int]() // 可变List
list.append(1,2,3,4,5,6,7,8,8,8,6)
val list1: List[Int] = List[Int](6, 8, 5, 10, 40, 60, 100,8,200,123,56)
val rdd: RDD[Int] = sc.parallelize(list)
val rdd1: RDD[Int] = sc.parallelize(list1)
// RDD交集,数据类型要求相同
val intersection: RDD[Int] = rdd.intersection(rdd1)
// 并集,数据类型要求相同
val union: RDD[Int] = rdd.union(rdd1)
// 差集,不同角度结果不同,数据类型要求相同
val subtract: RDD[Int] = rdd.subtract(rdd1)
// 拉链配对,要求个数相等,分区数量相等,数据类型可以不一致
val zip: RDD[(Int, Int)] = rdd.zip(rdd1)
zip.foreach(println)
}
}
2.6 RDD K-V类型算子
partitionBy将数据按照指定Partitioner 重新进行分区。Spark 默认的分区器是HashPartitioner
reduceByKey可以将数据按照相同的Key对Value进行聚合
groupByKey将数据源的数据根据key对value进行分组
面试:groupByKey和reduceByKey区别
groupByKey和reduceByKey是以Key为单位的,但是相同的Key可能在不同的分区,于是就有落盘操作,先在磁盘上将所有相同的Key聚集到一个分区,再读入内存
groupByKey不会在每个分区预聚合,只有等待sum时才聚合,集群节点之间传输的开销较大。groupByKey 只能分组,不能聚合

reduceByKey会在每个分区预先聚合,在落盘,读取文件效率高,集群节点之间传输的开销较小。reduceByKey其实包含分组和聚合的功能

aggregateByKey将数据根据不同的规则进行分区内计算和分区间计算,初始值只会参与分区内计算
(zeroValue)((zeroValue,key的value值) => {key的value值聚合进zeroValue})((t1,t2) => 分区间聚合)
foldByKey当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为foldByKey
combineByKey
createCombiner: V => C,相同key的第一条数据处理
mergeValue: (C, V) => C 分区内处理函数
mergeCombiners: (C, C) => C // 分区间处理函数
object ByKey {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val rdd = sc.makeRDD(List(("a",5),("a",4),("c",7),("b",10),("b",2),("c",11)),2)
// reduceByKey
rdd.reduceByKey((a,b) =>a+b).foreach(println)
// foldByKey 求各个键对应的最大Value值
rdd.foldByKey(0)((a,b) => Math.max(a,b)).foreach(println)
// aggregateByKey
rdd.aggregateByKey((0,0))(
(t,v) => { // t代表(0,0) v代表5
(t._1 + v,t._2 + 1)
},
(t1,t2) =>{
(t1._1 + t2._1,t2._2 + t2._2)
}
).map(data =>{
(data._1, data._2._1 /data._2._2)
}).foreach(println)
// combineByKey
// createCombiner: V => C,相同key的第一条数据处理
// mergeValue: (C, V) => C 分区内处理函数
// mergeCombiners: (C, C) => C // 分区间处理函数
rdd.combineByKey(
v => (v,1),
(t:(Int,Int),v) => { // t代表(5,1) v代表4
(t._1 + v,t._2 + 1)
},
(t1:(Int,Int),t2:(Int,Int)) =>{
(t1._1 + t2._1,t2._2 + t2._2)
}
).map(data =>{
(data._1, data._2._1 /data._2._2)
}).foreach(println)
}
}
2.7 RDD关联算子
类似于SQL的连接操作
object RddJoin {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
val empRDD: RDD[(Int, String)] = sc.parallelize(List((101, "jack"), (102, "rose"), (103, "young")))
val deptRDD: RDD[(Int, String)] = sc.parallelize(List((101, "销售部"), (102, "财务部")))
val result: RDD[(Int, (String, String))] = empRDD.join(deptRDD) // 根据key匹配,类比SQL内连接
result.foreach(data => {
println("编号为"+data._1+"的姓名为"+data._2._1+"其部门为"+data._2._2)
})
val result1: RDD[(Int, (String, Option[String]))] = empRDD.leftOuterJoin(deptRDD) // 类似左连接
result1.foreach(data => {
println("编号为"+data._1+"的姓名为"+data._2._1+"其部门为"+data._2._2)
})
}
}


2.8 RDD排序算子
- sortBy
- sortByKey
- top
案例:统计文本中单词数量最多的前三个
object RddSort {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("F:\\input\\wordcount",3)
val result: RDD[(String, Int)] = lines.flatMap(_.split(" "))
.filter(_.length >= 4)
.map((_, 1))
.reduceByKey(_ + _)
// sortBy,根据数量进行排序,false降序,take取出数量
val array: Array[(String, Int)] = result.sortBy(_._2, false).take(3)
array.foreach(data => println(data._1+"的数量是"+data._2))
// sortByKey 根据Key排序 但是我们目前的K-V是 (word,数量) 我们需要交换一下K-V 让数量成为K
result.map(_.swap).sortByKey(false).take(3)
.foreach(data =>println(data._2+"的数量是"+data._1))
// top 默认降序,适合结果数据量很小的时候
result.top(3)(Ordering.by(_._2)).foreach(data =>println(data._1+"的数量是"+data._2))
}
}
所有结果均为

3.RDD行动算子
redcue整形聚合
collect将不同分区的数据按照顺序采集到Driver,形成数组
count 数据源的数据个数
first 数据源的第一个
take 获取前n个数据
takeOrdered 自动排序!取前n个,只适合单个value
object Action_Demo1 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
val rdd: RDD[Int] = sc.makeRDD(List(8,2,3,4,5,8,7,9,6))
// redcue
val redcue: Int = rdd.reduce(_+_)
// collect将不同分区的数据按照顺序采集到Driver,形成数组
val ints: Array[Int] = rdd.collect()
// count 数据源的数据个数
val count: Long = rdd.count()
// first 数据源的第一个
val fir: Int = rdd.first()
// take 获取前n个数据
val take: Array[Int] = rdd.take(3)
take.foreach(println)
// takeOrdered 自动排序!取前n个,只适合单个value
val order: Array[Int] = rdd.takeOrdered(3)
order.foreach(println)
sc.stop();
}
}
fold(zeroValue: T)(op: (T, T) => T)
对每一个分区,执行一遍操作,初始值也要参与,最后再对每一个分区的结果执行操作,初始值再一次参与
aggregate(zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U)
对每一个分区,执行第一个函数的操作,初始值参与。然后对每一个分区的结果执行第二个函数,初始值再参与一次
countByKey 统计K-V中每个K出现的次数
conutByValue 注意,这个算子是把RDD集合中每个元素看成一个整体!即使是K-V也会把K-V看成一个整体,计算出现的次数!此value非K-V中value的意思!
object Action_Demo2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val value: RDD[String] = sc.textFile("F:\\input\\wordcount", 3)
// 每一个单词都是K-V键值对,(K,1)
val wordAndOne: RDD[(String, Int)] = value.flatMap(_.split(" ")).map((_, 1))
// countByKey 统计K-V中每个K出现的次数
val map: collection.Map[String, Long] = wordAndOne.countByKey()
map.foreach(println)
// conutByValue
val map1: collection.Map[(String, Int), Long] = wordAndOne.countByValue()
map1.foreach(println)
// foldBuKey 每个值都要加上初始值
/**
* 因为设置了三个分区,foldByKey会以每个分区为单位,与初始值进行运算
分区0 zhang 3+2=5 liu 1+2=3
分区1 hao 1+2=3 wang 1+2=3 song 2+2=4
分区2 kun 2+2=4 li 1+2=3 song 1+2=3
再将分区结果根据Key聚合就得到了如下的结果
*/
val result3: RDD[(String, Int)] = wordAndOne.foldByKey(2)(_ + _)
result3.foreach(println)
// aggregateByKey
/**
* 因为设置了三个分区,aggregateByKey会以每个分区为单位,根据第一个函数与初始值进行运算
分区0 zhang 3+2=5 liu 1+2=3
分区1 hao 1+2=3 wang 1+2=3 song 2+2=4
分区2 kun 2+2=4 li 1+2=3 song 1+2=3
根据分区结果,再根据第二个函数根据Key进行运算
因为只有1个zhang,第二个函数不执行,但是song有两个,所以分区1的song会和分区2的song进行运算(第二个函数)也就是 song : 3*4 = 12
*/
val result4: RDD[(String, Int)] = wordAndOne.aggregateByKey(2)(_ + _, _ * _)
result4.foreach(println)
}
}
saveAsTextFile保存到文件
saveAsObjectFile对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制
collect把各分区的数据收集到本地
4.RDD序列化
闭包检测
从计算的角度,算子以外的代码都是在Driver 端执行,算子里面的代码都是在Executor 端执行。那么在 scala 的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给Executor端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测。Scala2.12 版本后闭包编译方式发生了改
object Action_Demo3 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val value: RDD[Int] = sc.parallelize(List(1,2,3,1,4))
val user: User = new User
value.foreach(str =>{
println(str + user.name)
})
}
class User() extends Serializable {
var name = "对象内变量"
}
}
如果对象不序列化,将会报错,因为无法在服务器之间进行传输
5.RDD依赖
5.1 血缘关系
RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建 RDD 的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD 的Lineage 会记录RDD 的元数据信息和转换行为,当该RDD 的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区
通过toDebugString方法查看RDD的血缘关系
object WordCount {
def main(args: Array[String]): Unit = {
// 1.准备SparkContext上下文执行环境
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("F:\\input\\wordcount")
println(lines.toDebugString)
val words: RDD[String] = lines.flatMap(_.split(" "))
println(words.toDebugString)
val wordAndOne: RDD[(String, Int)] = words.map((_,1))
println(wordAndOne.toDebugString)
val result: RDD[(String, Int)] = wordAndOne.reduceByKey(_+_)
println(result.toDebugString)
result.foreach(println)
sc.stop()
}
}

5.2 宽依赖与窄依赖
窄依赖表示每一个父(上游)RDD 的Partition 最多被子(下游)RDD 的一个 Partition 使用,源码称之为OneToOne依赖
正是因为每个RDD操作的数据不变,所以我们最后只需要两个任务就可以

宽依赖表示同一个父(上游)RDD 的 Partition 被多个子(下游)RDD 的 Partition 依赖,会引起 Shuffle,称之为Shuffle依赖
由于宽依赖会导致数据Shuffle,所以对一个RDD的一步处理而言,代表一个阶段,对每个分区而言,都要有对应的任务进行处理,并且要等阶段内的任务全部处理完成,才可以进行下一个阶段
当RDD存在Shuffle依赖时,阶段就会增加一个。ResultStage只有一个,最开始就会创建,代表最后需要执行的阶段
RDD 任务划分
RDD 任务切分中间分为:Application、Job、Stage 和 Task
- Application:初始化一个 SparkContext 即生成一个Application
- Job:一个Action 算子就会生成一个Job
- Stage:Stage 等于宽依赖(ShuffleDependency)的个数加 1
- Task:一个 Stage 阶段中,最后一个RDD 的分区个数就是Task 的个数
6.RDD持久化
6.1 RDD缓存
在Spark中有些RDD可能会被频繁使用到,我们可以对RDD进行持久化/缓存,这样下次再使用这些RDD时就不需要计算了,提高了效率

API
- rdd.cache()
- rdd.persist()
- rdd.unpersist() // 清空缓存
cache底层是persist()

默认使用只使用内存(缓存到JVM的堆内存中)的级别

存储策略如下

开发中常用MEMORY_AND_DISK级别,内存不够放到磁盘
object RddCache {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("F:\\input\\wordcount")
val result: RDD[(String, Int)] = lines.flatMap(_.split(" "))
.filter(_.length >= 4)
.map((_, 1))
.reduceByKey(_ + _)
// result计算过程稍微复杂,可以放到缓存中
result.persist(StorageLevel.MEMORY_AND_DISK)
}
}
6.2 RDD Checkpoint
RDD虽然可以缓存/持久化,但是放在内存中,遇到突然断电的情况,显然是不可靠的,即使放在磁盘上,也不能保证100%可靠
Checkpoint就是一种更加可靠的数据持久化,它将数据放在了高可靠、高容错的HDFS上,实现了RDD的高可靠和高容错
对计算复杂且后续会被频繁使用的RDD进行checkpoint
API
- sc.setCheckpointDir(HDFS目录)
- rdd.checkpoint
缓存/持久化和Checkpoint检查点的区别
1.存储位置
缓存/持久化数据存默认存在内存,一般设置为内存+磁盘(普通磁盘),作业执行完成磁盘文件(临时文件)就被清除了
Checkpoint检查点:一般存储在HDFS
2.功能
缓存/持久化:保证数据后续使用的效率高
Checkpoint检查点:保证数据更加安全/也能一定程度上提高效率,每个action后都是独立作业,比如我把一个map操作的RDD进行了checkpoint,那么我将这个RDD进行一次求和,一次分组,那么这个map操作会被开启两次
3.对于依赖关系:
缓存/持久化:保留了RDD间的依赖关系,是Transformation
Checkpoint检查点:不保留RDD间的依赖关系,是Action
4.开发中如何使用?
对于计算复杂且后续会被频繁使用的RDD先进行缓存/持久化,再进行Checkpoint
sc.setCheckpointDir(hdfs)//实际中写HDFS目录
rdd.persist(StorageLevel.MEMORY_AND_DISK)
rdd.checkpoint()
//频繁操作rdd
result.unpersist()//清空缓存
7.RDD自定义分区器
1.继承Partitioner
2.重写两个方法
示例:将中文名字和英文名字分到不同的分区
object PartitionTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val rdd: RDD[(String, Int)] = sc.parallelize(List(("Jack", 12), ("杰斯", 36), ("卡特琳娜", 54), ("Rose", 33)),2)
// 设置分区器
val partitioner: RDD[(String, Int)] = rdd.partitionBy(new MyPartitioner)
partitioner.saveAsTextFile("./partitioner")
}
class MyPartitioner extends Partitioner{
// 分区的数量 一个英文,一个汉语
override def numPartitions: Int = 2
// 返回分区索引(从0开始)
override def getPartition(key: Any): Int = {
val bool: Boolean = Pattern.matches("[a-zA-Z]+", String.valueOf(key))
if (bool) {
0
} else {
1
}
}
}
}

RDD文件读取与保存
Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统
文件格式分为:text 文件、csv 文件、sequence 文件以及Object 文件
文件系统分为:本地文件系统、HDFS、HBASE 以及数据库
text 文件
// 读取输入文件
val inputRDD: RDD[String] = sc.textFile("input/1.txt")
// 保存数据
inputRDD.saveAsTextFile("output")
SequenceFile文件是Hadoop 用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)
object 对象文件
对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型
二.累加器
1 原理
累加器用来把Executor端变量信息聚合到Driver 端。在Driver程序中定义的变量,在Executor端的每个Task都会得到这个变量的一份新的副本,每个task更新这些副本的值后, 传回Driver端进行merge
2.示例
object SaveTextfile {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val rdd: RDD[Int] = sc.parallelize(List[Int](1, 2, 3, 4, 5, 6))
var sum = 0
rdd.foreach(num =>{
sum += num
})
println(sum)
}
}
上述代码结果为0。因为变量sum在Driver端,而rdd的运算操作是在Executor端进行的,当Executor计算完成后,没有办法将结果传回Driver,所以sum依旧是0
通过上下文创建一个累加器
object SaveTextfile {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val rdd: RDD[Int] = sc.parallelize(List[Int](1, 2, 3, 4, 5, 6))
// 累加器
val sum: LongAccumulator = sc.longAccumulator("sum")
rdd.foreach(num =>{
sum.add(num) // add方法
})
println(sum)
}
}

常见的累加器有三种

3.自定义累加器
实现wordcount
object MyAccumulator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Spark_RDD").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf);
val rdd: RDD[String] = sc.textFile("F:\\input\\wordcount", 3)
// 创建累加器对象
val acc: MyAcc = new MyAcc
// 向Spark注册
sc.register(acc, "wc")
rdd.flatMap(_.split(" ")).foreach(word => {
// 数据的累加
acc.add(word)
})
println(acc.value)
}
// 自定义累加器
// 1.设置输入输出的泛型
class MyAcc extends AccumulatorV2[String, mutable.Map[String, Int]] {
private val wcMap = mutable.Map[String, Int]()
// 初始化
override def isZero: Boolean = {
wcMap.isEmpty
}
// 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new MyAcc()
}
// 重置累加器
override def reset(): Unit = {
wcMap.clear()
}
// 获取累加器需要的数据
override def add(word: String): Unit = {
val newCount: Int = wcMap.getOrElse(word, 0) + 1 // 有word就加1,没有默认为0
wcMap.update(word, newCount)
}
// 不同分区的合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
val mapCur = this.wcMap // 当前的map
val mapOther: mutable.Map[String, Int] = other.value // 另外一个map
mapOther.foreach(kv => {
val newCount = mapCur.getOrElse(kv._1, 0) + kv._2
mapCur.update(kv._1, newCount)
})
}
// 返回的结果
override def value: mutable.Map[String, Int] = {
wcMap
}
}
}
三.广播变量
广播变量用来高效分发较大的对象。向所有工作节点(Executor相当于一个JVM)发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表
object BroadTest {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3))
val list = List[Int](3,2,1)
// 封装广播变量
val bc: Broadcast[List[Int]] = sc.broadcast(list)
// 获取访问变量
val value: List[Int] = bc.value
}
}
17万+



被折叠的 条评论
为什么被折叠?



