Spark学习——Spark-core核心编程之RDD算子

Spark学习笔记——SparkCore核心编程之RDD算子

一、概念

RDD (Resilient Distributed Dataset) 叫做弹性分布式数据集,是 Spark 中最基本的数据 处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行 计算的集合。

二、特点

➢ 弹性

​ 存储的弹性:内存与磁盘的自动切换;

​ 容错的弹性:数据丢失可以自动恢复;

​ 计算的弹性:计算出错重试机制;

​ 分片的弹性:可根据需要重新分片。

➢ 分布式:数据存储在大数据集群不同节点上

➢ 数据集:RDD 封装了计算逻辑, 并不保存数据

➢ 数据抽象: RDD 是一个抽象类, 需要子类具体实现

➢ 不可变:RDD 封装了计算逻辑, 是不可以改变的, 想要改变, 只能产生新的 RDD,在新的 RDD 里面封装计算逻辑

➢ 可分区、并行计算

三、核心属性

RDD 有五大核心属性,分别是

1)分区列表:RDD 数据结构中存在分区列表, 用于执行任务时并行计算, 是实现分布式计算的重要属性。

2)分区计算函数:Spark 在计算时, 是使用分区函数对每一个分区进行计算

3)RDD 之间的依赖关系:RDD 是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个 RDD 建 立依赖关系

4)分区器:当数据为 KV 类型数据时,可以通过设定分区器自定义数据的分区

5)首选位置:计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算

四、基础编程

1.RDD的创建

1.1从集合(内存)中创建

方法:parallelizemakeRDD

val rdd1 = sparkContext.parallelize (
List(1,2,3,4)
)
val rdd2 = sparkContext.makeRDD (
List(1,2,3,4)
)

从底层代码实现来讲,makeRDD 方法其实就是 parallelize 方法。

1.2从外部存储(文件) 创建

RDD还可以从本地的文件系统,所有 Hadoop 支持的数据集,比如 HDFS 、HBase 等等这些外部存储系统的数据集中创建。

读取文件数据时,数据是按照 Hadoop 文件读取的规则进行切片分区

val fileRDD: RDD[String] = sparkContext.textFile ("input/word.txt")
1.3从其他RDD创建

通过一个 RDD 运算完后, 再产生新的 RDD。

1.4直接创建

使用 new 的方式直接构造 RDD,一般由 Spark 框架自身使用。

2.RDD并行度与分区

Spark 可以将一个作业切分多个任务后,发送给 Executor 节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在构建 RDD 时指定。这个数量可以在构建 RDD 时指定。注意, 这里的并行执行的任务数量, 并不是指的切分任务的数量,不要混淆了。

读取内存数据时,数据可以按照并行度的设定进行数据的分区操作; 读取文件数据时,数据是按照 Hadoop 文件读取的规则进行切片分区,而切片规则和数据读取的规则有些差异

val dataRDD: RDD[Int] = sparkContext.makeRDD (List(1,2,3,4), 4)

上面的代码就指定了4个并行度,即分区数为4。

3.RDD转换算子

RDD 根据数据处理方式的不同将算子整体上分为 Value 类型、双 Value 类型和 Key-Value 类型

3.1 Value 类型
3.1.1 map

签名函数

def map[U: ClassTag](f: T => U): RDD[U]

其中f: T => U 就是我们对数据进行的操作。将处理的数据逐条进行映射转换,这里的转换可以是类型的转换, 也可以是值的转换。

val dataRDD1: RDD[Int] = dataRDD.map (
  num => {
   	 num * 2
  }
)

上面的代码就是对每个数据的值乘以2

3.1.2 mapPartitions

签名函数

def mapPartitions[U: ClassTag](
f: Iterator[T] => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U]

可以看到,mapPartitions 算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变, 所以可以增加或减少数据。将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据。

val dataRDD1: RDD[Int] = dataRDD.mapPartitions (
	datas => {
		datas.filter (_==2)
	}
)

上面的代码就是对等于2的数据过滤

注意:

mapPartitions 算子 是以分区为单位进行批处理操作,所以性能较高。但是 mapPartitions 算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。在内存有限的情况下,不推荐使用。

3.1.3 mapPartitionsWithIndex

签名函数

def mapPartitionsWithIndex[U: ClassTag](
f: (Int, Iterator[T]) => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U]

可以看到它与mapPartitions类似,但是传入参数多了一个索引,所以它在mapPartitions的功能的基础上,可以获取当前分区索引。

3.1.4 flatMap

签名函数

def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]

将处理的数据进行扁平化后再进行映射处理, 所以flatMap算子也称之为扁平映射

val value: RDD[List[Int]] = sc.makeRDD(List(
   List(1, 2), 3 ,List(3, 4)
), 1)

val dataRDD: RDD[Int] = value.flatMap(
   data =>{
      data match {
        case list: List[_] => list
        case dat => List(dat)
      }
   }
)

上面的代码就是将 List(1, 2), 3,List(4,5) 拆成1,2,3,4,5

3.1.5 glom

函数签名

def glom(): RDD[Array[T]]

将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变。

// 计算所有分区最大值求和 (分区内取最大值,分区间最大值求和)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
// Int => Array[Int]
val glomrdd: RDD[Array[Int]] = rdd.glom()
val maxrdd: RDD[Int] = glomrdd.map(
  array => {
    array.max
  }
)
println(maxrdd.collect().sum)

输出结果:6

3.1.6 groupBy

函数签名

def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]

将数据根据指定的规则进行分组, 分区默认不变, 但是数据会被打乱重新组合,这就是 shuffle。极限情况下, 数据可能被分在同一个分区中。

一个组的数据在一个分区中,但是并不是说一个分区中只有一个组

val dataRDD = sparkContext.makeRDD(List(1,2,3,4),1)
val dataRDD1 = dataRDD.groupBy(
  _%2
)

上面的代码就是将偶数分到一组

3.1.7 filter

函数签名

def filter(f: T => Boolean): RDD[T]

将数据根据指定的规则进行筛选过滤,符合规则的数据保留, 不符合规则的数据丢弃。

当数据进行筛选过滤后, 分区不变, 但是分区内的数据可能不均衡,生产环境下, 可能会出

现数据倾斜。

val dataRDD = sparkContext.makeRDD(List(
  1,2,3,4
),1)
val dataRDD1 = dataRDD.filter(_%2 == 0)  // 将偶数保留,奇数过滤
3.1.8 sample

签名函数

def sample(
  withReplacement: Boolean,           	  // 抽取完之后是否放回去
  fraction: Double,						// 分子比率
  seed: Long = Utils.random.nextLong	  // 随机数种子,如果不传递就用系统时间作为种子
): RDD[T]

根据指定的规则从数据集中抽取数据,在特殊场合使用,比如可以用来检验数据倾斜。

val dataRDD = sparkContext.makeRDD(List(
  1,2,3,4
),1)
// 不放回的情况:第二个参数:抽取的几率,范围在[0,1]之间,0:全不取; 1:全取;
val dataRDD1 = dataRDD.sample(false, 0.5)
// 第二个参数:重复数据的几率,范围大于等于 0.表示每一个元素被期望抽取到的次数
val dataRDD2 = dataRDD.sample(true, 2)
3.1.9 distinct

函数签名

def distinct()(implicit ord: Ordering[T] = null): RDD[T]
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

用来将数据集中重复的数据去重

Scala集合中的 distinct 方法的底层用到了 HashSet 的数据结构来实现去重

而RDD中的 distinct 方法的底层原理:

​ 先用 map 将每个数据变成( data, null )这样的元组

​ 在用 reduceByKey 将相同的数据聚合

​ 最后在用 map 将( data, null )变成data

3.1.10 coalesce

函数签名

def coalesce(
    numPartitions: Int,				// 重新分区的数量
    shuffle: Boolean = false,		// 是否进行shuffle操作
    partitionCoalescer: Option[PartitionCoalescer] = Option.empty
)(implicit ord: Ordering[T] = null): RDD[T]

可以根据数据量缩减分区,常用于大数据集过滤后, 提高小数据集的执行效率

当 spark 程序中, 存在过多的小任务的时候,可以通过 coalesce 方法,收缩合并分区,减少

分区的个数,减小任务调度成本

val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
// 默认情况下数据不会被打乱重组,可能会导致数据倾斜
val newRDD1: RDD[Int] = rdd.coalesce(2)
// 指定第二个参数,即 shuffle=ture,数据均衡但全部打乱
val newRDD2: RDD[Int] = rdd.coalesce(2, true)

注意,coalesce 是可以用来扩大分区的,此时必须设置 shuffle=ture 否则没有意义。

3.1.11 repartition

函数签名

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

这是 Spark 提供的一个化简操作,用来扩大分区,底层代码调用的就是 coalesce 且写死必须 shuffle,用法与 coalesce 一样。

3.1.12 sortBy

函数签名

def sortBy[K](
  f: (T) => K,
  ascending: Boolean = true,
  numPartitions: Int = this.partitions.length
)(implicit ord: Ordering[K], 
  ctag: ClassTag[K]): RDD[T]

sortBy 方法可以根据指定的规则 f: (T) => K 来对数据进行排序,默认为升序,第二个参数为 false 时为降序。默认情况下,不会改变分区,但是中间存在 shuffle 操作。

val rdd: RDD[(String, Int)] = sc.makeRDD(List(("1", 1), ("11", 2), ("2", 3)), 2)
rdd.sortBy(t => t._1.toInt, false)

上面的代码是按 (“1”, 1) 的字符串里面的数字进行降序排序。

3.2 双Value类型
intersection、union、subtract、zip

函数签名

def intersection(other: RDD[T]): RDD[T]				// 交集
def union(other: RDD[T]): RDD[T]				    // 并集,重复的地方算两次		
def subtract(other: RDD[T]): RDD[T]					// 差集
def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)]	 // 拉链,将相同位置配对

对源 RDD 和参数 RDD 进行计算后返回一个新的 RDD。

交集,并集,差集数据类型必须一致;

拉链不要求数据类型,但两个数据源的分区数量要保持一致,每个分区的数据数量也要保持一致。

val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD3 = dataRDD1.intersection (dataRDD2)	 // 3,4
val dataRDD4 = dataRDD1.union (dataRDD2)		// 1,2,3,4,3,4,5,6
val dataRDD5 = dataRDD1.subtract (dataRDD2)		// 1,2
val dataRDD6 = dataRDD1.zip (dataRDD2)		    // (1,3),(2,4)(3,5),(4,6)
3.3Key-Value类型
3.3.1 partitionBy

函数签名

def partitionBy(partitioner: Partitioner): RDD[(K, V)]

将数据(必须是 K-V 类型)按照指定 Partitioner 对数据重新进行分区。 Spark 默认的分区器是 HashPartitioner 即按照(key % 分区数) 的值来重分区。

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))
3.3.2 reduceByKey

函数签名

def reduceByKey(func: (V, V) => V): RDD[(K, V)]
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]

可以将数据按照相同的 Key 对 Value 进行聚合

val dataRDD1 = sparkContext.makeRDD(List(("a",1),("a",2),("b",3)))
val dataRDD2 = dataRDD1.reduceByKey( _+_ )		// (a,3) (b,3)
val dataRDD3 = dataRDD1.reduceByKey( _+_, 2)	// 一样,但会重新设定分区数为2
3.3.3 groupByKey

函数签名

def groupByKey(): RDD[(K, Iterable[V])]							   
def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]			// numPartitions,这个参数用于指定分区的数量
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]		// 可以使用自定义的分区策略:partitioner对象

将数据源中的数据,相同Key的数据分在一个组中,形成一个对偶元组,元组中的第一个元素就是key,第二个元素就是相同key的value集合

val dataRDD1 = sparkContext.makeRDD(List(("a",1),("a",2),("b",3)))
val dataRDD2 = dataRDD1.groupByKey ()					// ("a",(1,2)), ("b",3)
val dataRDD3 = dataRDD1.groupByKey (2)
val dataRDD4 = dataRDD1.groupByKey (new HashPartitioner(2))

!!!groupByKey 和 reduceByKey 的区别:

1)从功能的角度:groupByKey 只能进行分组操作,无法对数据进行聚合,想要聚合数据需要借助 map;而 reduceByKey 能对数据进行分组,还能对相同key的value值进行聚合操作。所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合,那 么还是只能使用 groupByKey

2)从性能的角度:两者都要打乱数据并重组,都需要进行 shuffle,但是性能上有差异。在Spark中,shuffle 操作必须落盘处理(写入磁盘),不能在内存中进行数据等待,否则会导致内存溢出。既然要落盘,就涉及到磁盘IO,性能会受到影响。但是,reduceByKey 支持分区内预聚合功能(Combine),可以有效减少 Shuffle 时落盘的数据量。所以 reduceByKey 性能比较高

3.3.4 aggregateByKey

函数签名

def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,combOp: (U, U) => U): RDD[(K, U)]

可以看到,aggregateByKey 存在函数柯里化,有两个参数列表

​ 1)第一个参数列表需要传递一个参数:表示初始值,主要用于当碰见第一个key的时候,和value进行分区内计算

​ 2)第二个参数列表需要传递两个参数:第一个表示分区内计算规则,第二个表示分区间计算规则

reduceByKey 在分区内和分区间的计算规则是相同的, aggregateByKey 可以将数据根据不同的规则进行分区内计算和分区间计算

// TODO : 获取相同key的数据的平均值
val rdd = sc.makeRDD(List(
  ("a", 1), ("a", 2), ("b", 3),
  ("b", 5), ("b", 4), ("a", 6)
), 2)

// aggregateByKey的最终返回值类型:key保持不变,value和初始值的类型保持一致
// aggregateByKey的最终返回值由初始值决定
val newRDD: RDD[(String, (Int, Int))] = rdd.aggregateByKey((0, 0))(
  (tuple, v) => { // 分区内计算规则
    // tuple表示传进来的初始值(0,0),v表示每个相同key数据的value值
    // 第一个0用来计算累加的和,第二个0用来计算出现次数
    (tuple._1 + v, tuple._2 + 1)  
  },
  (t1, t2) => { // 分区间计算规则
    // t1、t2是两个分区经过上面计算分别得到的结果
    // 如,对a来说t1是(3,2) t2是(6,1),计算后得到(9,3),最终返回值为("a",(9,3))
    (t1._1 + t2._1, t1._2 + t2._2)
  }
)
val resultRDD: RDD[(String, Int)] = newRDD.mapValues {
  case (num, cnt) => {
    num / cnt
  }
}
resultRDD.collect().foreach(println)  // 输出结果:(a,3) (b,4)
3.3.5 foldByKey

函数签名

def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]

当分区内计算规则和分区间计算规则相同时, aggregateByKey 就可以简化为 foldByKey

val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("a",3))) 
val dataRDD2 = dataRDD1.foldByKey(0)(_+_)				// 输出:(a,4) (b,2)
3.3.6 combineByKey

函数签名

def combineByKey[C](
	createCombiner: V => C,											// 第一个参数:对相同key的第一个数据进行的结构的转换
	mergeValue: (C, V) => C,										// 第二个参数:分区内的计算规则
	mergeCombiners: (C, C) => C): RDD[(K, C)]			// 第三个参数:分区间的计算规则

最通用的对 key-value 型 rdd 进行聚集操作的聚集函数(aggregation function) 。类似于 aggregate() ,combineByKey() 但允许用户返回值的类型与输入不一致。

// TODO : 取出每个分区内相同 key 的最大值然后分区间相加
val rdd = sc.makeRDD(List(
  ("a", 1), ("a", 2), ("b", 3),
  ("b", 5), ("b", 4), ("a", 6)
), 2)
// combineByKey方法可以将计算数据的第一个值变化
rdd.combineByKey(
  v => (v, 1),    // 对相同key的第一个数据进行的结构的转换
  (tuple:(Int,Int), v) => { // 分区内计算规则
    (tuple._1 + v, tuple._2 + 1)
  },
  (t1:(Int,Int), t2:(Int,Int)) => { // 分区间计算规则
    (t1._1 + t2._1, t1._2 + t2._2)
  }
).mapValues {
    case (num, cnt) => {
      num / cnt
   }
 }
  .collect().foreach(println) // 输出结果依然是 (a,3) (b,4)

!!!四个聚合算子的区别:

reduceByKey: 相同 key 的第一个数据不进行任何计算,分区内和分区间计算规则相同

FoldByKey: 相同 key 的第一个数据和初始值进行分区内计算, 分区内和分区间计算规则相同

AggregateByKey:相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则可以不相同

CombineByKey:当计算时,发现数据结构不满足要求时, 可以让第一个数据转换结构。分区内和分区间计算规则可以不相同。

3.3.7 join

函数签名

def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]

在类型为 (K,V) 和 (K,W) 的 RDD 上调用, 返回一个相同 key 对应的所有元素连接在一起的 (K,(V,W)) 的 RDD。如果两个数据源中有的数据的 key 没有匹配上那么它不会出现在结果中。在数据源中如果有多个相同的 key,则会依次匹配,可能会出现笛卡尔乘积,数据量几何性增长,导致性能降低

val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6))) 
rdd.join(rdd1).collect().foreach(println)   // 输出:(1,(a,4)) (2,(b,5)) (3,(c,6))
3.3.8 leftOuterJoin

函数签名

def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]

类似于 SQL 语句的左外连接(右外连接 rightOuterJoin 同理)

val dataRDD1 = sparkContext.makeRDD(
  List(("a", 1), ("b", 2), ("c", 3)
))
val dataRDD2 = sparkContext.makeRDD(
  List(("a", 3), ("b", 5) //,("c",7)
))
val rdd: RDD[(String, (Int, Option[Int]))] = dataRDD1.leftOuterJoin(dataRDD2)
rdd.collect().foreach(println)  // 输出结果:(a,(1,Some(3))) (b,(2,Some(5))) (c,(3,None))
3.3.9 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。可以将 cogroup 理解成 connect + group (分组链接)

val dataRDD1 = sparkContext.makeRDD(
  List(("a", 1), ("b", 2)//, ("c", 3)
  ))
val dataRDD2 = sparkContext.makeRDD(
  List(("a", 3), ("b", 5), ("c", 7), ("c", 9)
  ))
val rdd: RDD[(String, (Iterable[Int], Iterable[Int]))] = dataRDD1.cogroup(dataRDD2)
rdd.collect().foreach(println)
// 输出结果:
// (a,(CompactBuffer(1),CompactBuffer(3)))
// (b,(CompactBuffer(2),CompactBuffer(5)))
// (c,(CompactBuffer(),CompactBuffer(7, 9)))
3.4 案例实操
3.4.1 数据准备

agent.log:时间戳, 省份,城市, 用户,广告, 中间字段使用空格分隔。如下所示:

1516609143867 6 7 64 16
1516609143869 9 4 75 18
1516609143869 1 7 87 12
1516609143869 2 8 92 9
......
3.4.2 案例需求

统计出每一个省份每个广告被点击数量排行的 Top3

3.4.3 需求分析

步骤如下:

1、获取原始数据
2、将原始数据进行结构的转化,方便统计						时间戳,省份,城市,用户,广告 =>((省份,广告),1)
3、将转换结构后的数据,进行分组聚合					 	((省份,广告),1)=>((省份,广告),sum)  
4、将聚合的结果进行结构的转化									((省份,广告),sum)=>(省份,(广告,sum))
5、将转换结构后的数据根据省份进行分组						(省份A,(广告A,sumA)),(省份A,(广告B,sumB))......
6、将分组后的数据进行组内排序(降序),取前三名		
7、采集数据并打印在控制台
3.4.5 代码实现
package operator.transform

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Requirement {
  def main(args: Array[String]): Unit = {

    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Opertaor")
    val sc = new SparkContext(sparkConf)

    // TODO 案例实操:统计出每一个省份每个广告被点击数量排行的 Top3
    // agent.log数据格式:时间戳,省份,城市,用户,广告  中间字段使用空格分隔。

    // 1、获取原始数据
    val dataRDD: RDD[String] = sc.textFile("input/agent.log")

    // 2、将原始数据进行结构的转化,方便统计(缺什么,补什么,多什么,删什么)
    //   时间戳,省份,城市,用户,广告
    //   =>((省份,广告),1)
    dataRDD.map(
      line => {
        val strings: Array[String] = line.split(" ")
        Tuple2(Tuple2(strings(1), strings(4)), 1)
      }
    )

      // 3、将转换结构后的数据,进行分组聚合
      // ((省份,广告),1)=>((省份,广告),sum)
      .reduceByKey(_ + _)

      // 4、将聚合的结果进行结构的转化
      // ((省份,广告),sum)=>(省份,(广告,sum))
      /*
      .map(
        t => {
          Tuple2(t._1._1, Tuple2(t._1._2, t._2))
        }
      )
      */// 这里可以用模式匹配来优化
      .map {
        case ((prv, ad), sum) => {
          (prv, (ad, sum))
        }
      }

      // 5、将转换结构后的数据根据省份进行分组
      // (省份A,(广告A,sumA)),(省份A,(广告B,sumB))......
      .groupByKey()

      // 6、将分组后的数据进行组内排序(降序),取前三名
      /*
      .map(
        t =>{
          t._2.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
        }
      )
      */// 这里可以用mapValues来优化
      .mapValues(
        iter => {
          iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
        }
      )

      // 7、采集数据并打印在控制台
      .collect().foreach(println)


  }
}

输出结果如下:

(4,List((12,25), (2,22), (16,22)))
(8,List((2,27), (20,23), (11,22)))
(6,List((16,23), (24,21), (22,20)))
(0,List((2,29), (24,25), (26,24)))
(2,List((6,24), (21,23), (29,20)))
(7,List((16,26), (26,25), (1,23)))
(5,List((14,26), (21,21), (12,21)))
(9,List((1,31), (28,21), (0,20)))
(3,List((14,28), (28,27), (22,25)))
(1,List((3,25), (6,23), (5,22)))

可以看到,每个省份的广告点击量的前三名都被打印出来了,满足需求。

4. RDD行动算子

所谓行动算子,其实就是触发作业(job)执行的方法,底层代码调用的就是 runJob方法。

4.1 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(_+_)
println(reduceResult)				// 输出:10
4.2 collect

函数签名

def collect(): Array[T]

方法会将不同分区的数据按照分区顺序采集到 Driver 段内存中,形成数组(以数组 Array 的形式返回数据集的所有元素)。

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 收集数据到 Driver
rdd.collect().foreach(println)  // 输出:1 2 3 4
4.3 count

函数签名

def count(): Long

顾名思义,它可以统计并返回 RDD 中元素的个数

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val countResult: Long = rdd.count()
println(countResult)			// 输出:4
4.4 first

函数签名

def first(): T

也是顾名思义,获取 RDD 中的第一个元素

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val firstResult: Int = rdd.first()
println(firstResult)			// 输出:1
4.5 take

函数签名

def take(num: Int): Array[T]

用来获取前 n 个数据,返回一个由 RDD 的前 n 个元素组成的数组

vval rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val takeResult: Array[Int] = rdd.take(3)
println(takeResult.mkString(","))			// 输出:1,2,3
4.6 takeOrdered

函数签名

def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]

将数据排序之后,取前 n 个(想要降序的话,将第二个参数,传 Ordering.Int.reverse )

val rdd: RDD[Int] = sc.makeRDD(List(4231))
// 返回 RDD 中元素的个数
val result: Array[Int] = rdd.takeOrdered(3)(Ordering.Int.reverse)
println(result.mkString(","))					// 输出:4,3,2
4.7 aggregate

函数签名

def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U

aggregate 可以将分区的数据通过初始值和分区内的数据进行聚合, 然后再和初始值进行分区间的数据聚合

val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
// 将该 RDD 所有元素相加得到结果
val result: Int = rdd.aggregate(10)(_ + _, _ + _)    // 输出:40

!!!aggregate 和 aggregateByKey 的区别

aggregate:行动算子,返回值类型就是计算结果,初始值会参与分区内计算,并且参与分区间计算。

aggregateByKey:转换算子,返回值类型是一个 RDD 类型数值,初始值会参与分区内计算,但不会参与分区间计算

4.8 fold

函数签名

def fold(zeroValue: T)(op: (T, T) => T): T

与 foldByKey 是 aggregateByKey 的简化版同理, fold 就是 aggregate 的简化版

val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4),2)
val foldResult: Int = rdd.fold(0)(_+_) 			// 输出:40
4.9 countByKey

函数签名

def countByKey(): Map[K, Long]

统计每种 key 的个数,注意,跟 key 所对应的 value 值无关,返回值是一个 Map 类型

val rdd: RDD[(String, Int)] = sc.makeRDD(List(
  ("a", 1), ("a", 2), ("a", 3)
))
val stringToLong: collection.Map[String, Long] = rdd.countByKey()
println(stringToLong)					// 输出:Map(a -> 3)
4.10 countByValue

函数签名

def countByValue() : Map[T, Long]

与 countByKey 功能类似,用来统计相同的数据值出现的次数,注意,这里的 Value 并不是指键值对中的 Value,而是直接指数据

val rdd: RDD[Int] = sc.makeRDD(List(1, 1, 1, 4), 2)
val intToLong: collection.Map[Int, Long] = rdd.countByValue()
println(intToLong)				// 输出:Map(4 -> 1, 1 -> 3)
4.11 save相关算子

函数签名

def saveAsTextFile(path: String): Unit
def saveAsObjectFile(path: String): Unit
def saveAsSequenceFile(
	path: String,
	codec: Option[Class[_ <: CompressionCodec]] = None): Unit

save 相关算子的功能是,将数据保存到到不同格式的文件中,但要注意的是,saveAsSequenceFile 方法要求数据的格式必须为 K-V 类型

// 保存成 Text 文件
rdd.saveAsTextFile ("output")
// 序列化成对象保存到文件
rdd.saveAsObjectFile ("output1")
// 保存成 Sequencefile 文件
rdd.map ((_,1)).saveAsSequenceFile ("output2")
4.12 foreach

函数签名

def foreach(f: T => Unit): Unit = withScope {
	val cleanF = sc.clean(f)
	sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}

分布式遍历 RDD 中的每一个元素,调用指定函数

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 收集后打印
// 这里的 foreach 其实是Driver端内存集合的循环遍历方法(有序)
rdd.map (num=>num).collect().foreach(println)			// 输出:1 2 3 4
println("****************")
// 分布式打印
// 这里的 foreach 是Executor端内存数据打印(乱序)
rdd.foreach (println)													// 输出:2 1 3 4 

!!!概念补充:为什么 RDD 的方法被称为算子(Operator)!!!

RDD 的方法和 Scala 中集合对象的方法不一样:集合对象的方法都是在同一个节点的内存中完成的;而 RDD 的方法可以以将计算逻辑发到 Executor 端(分布式节点)中执行。为了区分不同的执行效果,所以将 RDD 的方法称为算子。RDD 的方法外部操作都是在 Driver 端执行的,而方法内部的逻辑代码在 Executor 端执行。

5. RDD序列化

5.1 闭包检测

上面我们提到了计算的节点分 Driver 端和 Executor 端,算子外的代码在 Driver 端执行, 算子里面的代码都是在 Executor 端执行。经常会出现算子内调用算子外的数据的现象,这就意味着数据要进行网络传输,所以必须要将数据序列化

而在 scala 的函数式编程中,在算子内调用算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给 Executor 端执行,就会发生错误,所以需要在执行任务计算前, 检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测

5.2 序列化方法和属性

来看案例:

package serializable

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Serializable01 {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("Serial")
    val sc = new SparkContext(conf)

    val rdd: RDD[String] = sc.makeRDD(List("hello world", "hello spark", "hadoop"))

    // 想从数据源中查询符合要求的数据
    val search: Search = new Search("spark")

    search.getMatch1(rdd).collect().foreach(println)


    sc.stop()
  }

  // 自定义查询对象
  class Search(query: String) {

    def isMatch(s: String): Boolean = {
      s.contains(query)
    }

    // 函数序列化案例
    // getMatch1 采用外部函数
    def getMatch1(rdd: RDD[String]): RDD[String] = {
      rdd.filter(isMatch)
    }
  }
}

运行后出现报错:

Task not serializable		// 任务没有序列化
java.io.NotSerializableException: serializable.Serializable01$Search		// Search 这个对象没有序列化

思考:

getMatch1 调用了 isMatch 函数,使用了 query 这个参数,但是 query 是字符串,本身已经序列化,为什么还会报错

尝试加上extends Serializable ,再次运行,成功不报错,输出结果:

hello spark

原因:

这是因为在 Scala 中类的构造器中的参数其实是类的属性,构造器参数需要进行闭包检测,其实就等同于类进行闭包检测,所以要对类进行序列化。

补充:class 前面加上关键字 case 也可以使编译通过,这是因为样例类会自动进行序列化操作

5.3 Kryo 序列化框架

Java 的序列化能够序列化任何的类。但是比较重 (字节多) ,序列化后,对象的提交也 比较大。Spark 出于性能的考虑, Spark2.0 开始支持另外一种 Kryo 序列化机制。Kryo 速度 是 Serializable 的 10 倍。当RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型 已经在 Spark 内部使用 Kryo 来序列化。

6. RDD依赖关系

6.1 RDD血缘关系
val rdd1 = rdd.map(_*2)

如上所示,想要构建 rdd1 就要用到 rdd,则称 rdd1 依赖于 rdd。相邻的两个 rdd 的关系称为依赖关系,新的 rdd 依赖于旧的 rdd。多个连续的依赖关系,称之为血缘关系。

RDD 是不会保存数据的,为了提供容错性,需要将 RDD 之间的关系保存下来,一旦出现错误,可以根据血缘关系找到数据源重新读取数据。

6.2 宽窄依赖

窄依赖表示每一个父(上游)RDD 的 Partition 最多被子 (下游) RDD 的一个 Partition 使用,窄依赖我们形象的比喻为独生子女。

宽依赖表示同一个父 (上游) RDD 的 Partition 被多个子 (下游) RDD 的 Partition 依赖,会引起 Shuffle,宽依赖形象的比喻为多生。

6.3 RDD阶段划分

DAG (Directed Acyclic Graph) 有向无环图是由点和线组成的拓扑图形, 该图形具有方向,不会闭环。例如,DAG 记录了 RDD 的转换过程和任务的阶段。

RDD 中存在宽依赖时阶段数会自动增加一个ResultStage,即 阶段的数量 = 宽依赖数量 + 1

6.4 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的分区数量

7. RDD持久化

7.1 是什么

前面说到,RDD是不会存储数据的,如果一个 RDD 要重复使用,那么就要从头到尾再来执行一次来获取数据。RDD的对象是可以重复使用的,但是数据无法重复使用。将计算得到的数据存到内存或者文件中就叫RDD的持久化。

7.2 RDD Cache缓存

RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存, 默认情况下会把数据以缓存 在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的行动算子时, 该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

// 数据缓存。
rdd.cache ()
// 可以更改存储级别
rdd.persist (StorageLevel.MEMORY_AND_DISK)

Cache缓存默认持久化到内存,如果想要保存到磁盘文件,需要更改存储级别。这里的 MEMORY_AND_DISK 就是表示缓存到内存和磁盘。RDD对象的持久化操作不一定是为了重用,在数据执行比较长,或者数据比较重要的场合也可以采用持久化操作。

Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作(比如: reduceByKey)。这样 做的目的是为了当一个节点 Shuffle 失败了避免重新计算整个输入。但是,在实际使用的时 候,如果想重用数据,仍然建议调用 persist 或 cache

7.3 RDD CheckPoint检查点

CheckPoint 检查点是要落盘的,且不会像缓存一样被删除,所以需要指定文件路径。一般保存路径都是在分布式存储系统中,例如HDFS

// 设置检查点路径
sc.setCheckpointDir ("./checkpoint1")

由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点 之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。同样,对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action(行动算子) 操作才能触发。

7.4 缓存和检查点区别

cache:将数据临时存储在内存中进行数据重用。
会在血缘关系中添加新的依赖。一旦出现问题,可以重头读取数据

persist:将数据临时存储到磁盘文件中进行数据重用,
涉及到磁盘IO,性能较低,但相对安全。
也会在血缘关系中添加新的依赖

checkpoint:将数据长久的保存到磁盘文件中进行数据重用,
涉及到磁盘IO,性能较低,但数据安全
为了保证数据安全,所以一般情况下会独立执行作业。(行动算子执行的时候,checkpoint 会产生一个新的作业
为了提高效率,一般会和 cache 一起使用。(先 cache 再 checkpoint)
执行过程中会切断血缘关系建立新的血缘关系。
checkpoint 等同于改变数据源

8. RDD 分区器

​ Spark 目前支持 Hash 分区和 Range 分区,和用户自定义分区。 Hash 分区为当前的默认 分区。分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 后进入哪个分 区,进而决定了 Reduce 的个数。
注意:只有 Key-Value 类型的 RDD 才有分区器,非 Key-Value 类型的 RDD 分区的值是 None

下面是一段自定义分区器的代码演示:

package partitioner

import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}

object PartitionerTset {
  def main(args: Array[String]): Unit = {

    val conf = new SparkConf().setMaster("local[*]").setAppName("partitioner")
    val sc = new SparkContext(conf)

    val rdd: RDD[(String, String)] = sc.makeRDD(List(
      ("nba", "xxxx"), ("cba", "xxxx"), ("nba", "xxxx"), ("cxk", "xxxx")
    ),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 值来返回数据的分区索引(从零开始)
    override def getPartition(key: Any): Int = {
      key match {
        case "nba"=>0
        case "cba"=>1
        case _=>2
      }

    }
  }
}

9. RDD文件的读取和保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。

文件格式分为: text 文件、csv 文件、 sequence 文件以及 Object 文件;

文件系统分为: 本地文件系统、 HDFS 、HBASE 以及数据库。

9.1 text文件

比较简单,直接代码演示

// 读取输入文件
val inputRDD: RDD[String] = sc.textFile ("input/1.txt")
// 保存数据
inputRDD.saveAsTextFile("output")
9.2 sequence 文件

SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件。

// 保存数据为 SequenceFile
dataRDD.saveAsSequenceFile("output")
// 读取 SequenceFile 文件
sc.sequenceFile[Int,Int]("output").collect().foreach(println)
9.3 Object 文件

对象文件是将对象序列化后保存的文件, 采用 Java 的序列化机制。可以通过 objectFile 函数接收一个路径,读取对象文件, 返回对应的 RDD ,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定数据类型。

// 保存数据
dataRDD.saveAsObjectFile("output")
// 读取数据
sc.objectFile[Int]("output").collect().foreach(println)

/**

  • 自定义分区器
  • 1、继承 Partitioner
  • 2、重写方法
    */
    class Mypartitioner extends Partitioner{
    // 分区数量
    override def numPartitions: Int = 3
// 根据数据的 key 值来返回数据的分区索引(从零开始)
override def getPartition(key: Any): Int = {
  key match {
    case "nba"=>0
    case "cba"=>1
    case _=>2
  }

}

}
}


### 9. RDD文件的读取和保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。

文件格式分为: text 文件、csv 文件、 sequence 文件以及 Object 文件;

文件系统分为: 本地文件系统、 HDFS 、HBASE 以及数据库。

#### 9.1 text文件

比较简单,直接代码演示

```scala
// 读取输入文件
val inputRDD: RDD[String] = sc.textFile ("input/1.txt")
// 保存数据
inputRDD.saveAsTextFile("output")
9.2 sequence 文件

SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件。

// 保存数据为 SequenceFile
dataRDD.saveAsSequenceFile("output")
// 读取 SequenceFile 文件
sc.sequenceFile[Int,Int]("output").collect().foreach(println)
9.3 Object 文件

对象文件是将对象序列化后保存的文件, 采用 Java 的序列化机制。可以通过 objectFile 函数接收一个路径,读取对象文件, 返回对应的 RDD ,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定数据类型。

// 保存数据
dataRDD.saveAsObjectFile("output")
// 读取数据
sc.objectFile[Int]("output").collect().foreach(println)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值