目录
- spark outline
- spark 3大数据结构
- 为什么要使用累加器
- spark 累加器功能
- 自定义累加器案例演示
- spark 累加器执行原理
spark outline
大纲目录
spark 3大数据结构
- RDD:弹性分布式数据集
- 累加器:分布式共享只写变量
- 广播变量:分布式共享只读变量
为什么要使用累加器
首先来看一段代码(统计单词出现的次数)
def main(args: Array[String]): Unit = {
// 1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[1]")
// 2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
// 3.创建RDD
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("a", 3), ("a", 4)),numSlices = 2)
// 3.1 打印单词出现的次数(a,10)
rdd.reduceByKey(_ + _).collect().foreach(println) // 其中有shuffle过程
// 有shuffle过程,那么意味着执行效率低下
// 3.2 如果不用shuffle,怎么处理呢?
var sum = 0
rdd.foreach {
case (word, count) => {
sum = sum + count
println("sum=" + sum) // 打印是在Executor端
}
}
println(("a", sum)) // 打印是在Driver端
sc.stop()
}
输出结果:
(a,10)
sum=1
sum=3
sum=3
sum=7
(a,0)
问题1:
使用reduceByKey会有shuffle,有shuffle则意味着执行效率低下
问题2:
如果不用reduceByKey,怎么处理呢?
通常的思路是:自己去定义一个全局sum变量,每遍历到相同key的单词后,就去做累加(sum+count),但为什么最终结果会是(a,0)?

因为numSlices = 2,所以会由2个Executor来处理数据,然后每个Executor会去复制一份Driver端的sum变量,每个Executor内的sum变量互不干扰,所以就有:第一个分区中的数据为(“a”, 1), (“a”, 2),输出sum=1,sum=3;第二个分区中的数据为(“a”, 3), (“a”, 4),输出sum=3,sum=7;因为Driver端的sum变量是被复制Executor端过去的,Executor端并没有直接操作Driver端的sum变量,所以Driver端的sum变量仍然为0,如果想要Executor端的sum变量影响到Driver端的sum变量,则需要使用累加器
// 3.3 使用累加器实现数据的聚合功能
// Spark自带常用的累加器
// 3.3.1 声明累加器
val sum1: LongAccumulator = sc.longAccumulator("myAcc")
rdd.foreach {
case (word, count) => {
// 3.3.2 使用累加器
sum1.add(count)
}
}
//3.3.3 获取累加器的值
println(sum1.value)

spark 累加器功能
累加器用来对信息进行聚合。比如上述使用 foreach 算子时,该算子内部使用到了 Dirver 端定义的变量,并且集群中运行的每个任务的 Executor 端都会得到变量的一个副本,在 Executor 端更新副本的值不会影响 Dirver 端对应的变量,如果想要影响,则需要使用累加器
自定义累加器案例演示
自定义累加器在1.x版本中就已经提供了,但是使用起来比较麻烦,在2.0版本后累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型累加器的实现方式
需求:统计集合List(“Hello”, “World”, “Hi”, “Spark”)中首字母为“H”单词出现的次数
输出:Map(Hello -> 1, Hi -> 1)
package com.xcu.bigdata.spark.core.pg02_accumulator
import org.apache.spark.rdd.RDD
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable
/**
* @Desc : 累加器的使用 统计特定形式的wordCount
*/
object Spark01_Accumulator {
def main(args: Array[String]): Unit = {
//创建配置文件
val conf: SparkConf = new SparkConf().setAppName("Spark01_Accumulator").setMaster("local[*]")
//创建SparkContext,该对象是提交的入口
val sc = new SparkContext(conf)
//创建RDD
val rdd: RDD[String] = sc.makeRDD(List("Hello", "World", "Hi", "Spark"))
//创建累加器
val myAcc = new MyAccumulator()
//注册累加器
sc.register(myAcc, name = "myAcc")
//使用累加器
rdd.foreach(
word => {
myAcc.add(word)
}
)
//获取累计器的累加结果
println(myAcc.value)
//释放资源
sc.stop()
}
}
//AccumulatorV2[输入数据的类型, 输出数据的类型]
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Int]] {
//定义一个集合,记录单词以及出现次数
var map: mutable.Map[String, Int] = mutable.Map[String, Int]()
//初始化状态
override def isZero: Boolean = map.isEmpty
//拷贝(不同的excutor需要去拷贝一份driver上的累加器,作为副本)
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
val newAcc = new MyAccumulator
newAcc.map = this.map
newAcc
}
//重置集合
override def reset(): Unit = map.clear()
//分区内计算逻辑
override def add(e: String): Unit = {
if (e.startsWith("H")) {
//向可变集合中添加或者更新元素
map(e) = map.getOrElse(e, 0) + 1
}
}
//分区间计算逻辑
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
//当前excutor的map
var map1: mutable.Map[String, Int] = map
//另一个excutor的map
var map2: mutable.Map[String, Int] = other.value
map = map1.foldLeft(map2) {
(map2, kv) => {
map2(kv._1) = map2.getOrElse(kv._1, 0) + kv._2
map2
}
}
}
//获取累加器的值
override def value: mutable.Map[String, Int] = map
}
spark 累加器执行原理
首先序列化 driver 端 accumulator 到 executor ,序列化前调用 reset 重置 value 并使用 isZero 检测是否重置成功。单个 executor 内使用 add 进行累加,最终 driver 端对多个 executor 间的 accumulaotr 使用merge 进行合并得到结果