【Spark算子】Spark RDD编程

本文详细介绍了Spark中的核心概念RDD,包括其特点、创建方式、转换算子和行动算子。RDD是不可变、可分区的,具备血缘关系、依赖和缓存特性,用于分布式数据处理。重点讨论了Transformation算子,如map、filter、flatMap、groupByKey等,并解释了宽依赖和窄依赖的概念,以及shuffle的产生原因。同时,还涵盖了Action算子,如collect、reduce、saveAsTextFile等。最后,讨论了key-value RDD的特殊操作,如reduceByKey和join。

1.1 什么是RDD

RDD是 Spark 的基石,是实现 Spark 数据处理的核心抽象。
RDD 是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合
在这里插入图片描述

RDD(Resilient Distributed Dataset)是 Spark 中的核心概念,它是一个容错、可以并行执行的分布式数据集。

RDD包含5个特征

  1. 一个分区的列表
  2. 一个计算函数compute,对每个分区进行计算
  3. 对其他RDDs的依赖(宽依赖、窄依赖)列表
  4. 对key-value RDDs来说,存在一个分区器(Partitioner)【可选的】
  5. 对每个分区有一个优先位置的列表【可选的】

有一组分区(Partitions),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值;
有一个对分区数据进行计算的函数。Spark中RDD的计算是以分区为单位的,每个RDD都会实现 compute 函数以达到该目的。compute函数会对迭代器进行组合,不需要保存每次计算的结果;
RDD之间存在依赖关系。RDD的每次转换都会生成一个新的RDD,RDD之间形成类似于流水线一样的前后依赖关系(lineage)。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算;
对于 key-value 的RDD而言,可能存在分区器(Partitioner)。Spark 实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有key-value的RDD,才可能有Partitioner, 非key-value的RDD的Parititioner的值是None。Partitioner函数决定了RDD本身的分区数量,也决定了parent RDD Shuffle输出时的分区数量;
一个列表,存储每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照 “移动计算不移动数据” 的理念,Spark在任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。

1.2 RDD的特点

1、分区

RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute 函数得到每个分区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。
在这里插入图片描述

题外思考: 为什么RDD要分区?
简而言之是为了分布式计算,分区数决定了并行度.
每个RDD会被划分成一个或多个分区,这些分区可能是保存在Spark集群中的多个节点上的(但是分区就是最小单位了,Spark保证同一个分区的数据是在同一个机器上执行的,不会跨机器处理), 在执行任务时,会并行的在RDD各个分区上进行计算,然后再把结果进行整合得到最后的结果, 同时分区可以减少网络IO, 尤其对于有shuffle操作时,相同key的value会进入同一个分区,这样如果key相似或在同一范围内的数据尽量在同一分区的话, 可以减少网络直接的传输,大大提升效率

分区的特点
spark的分区有以下特点:
1.在Spark集群中每个工作节点,可能都包含一个或多个分区。
2.Spark中使用的分区数是可配置的,但要注意,分区太少或分区太多都不好。

  • 分区太少,会导致较少的并发、数据倾斜、或不正确的资源利用。
  • 分区太多,导致任务调度花费比实际执行时间更多的时间。
  • 若没有配置分区数,默认的分区数是:所有执行程序节点上的内核总数。

3.Spark保证同一个分区的数据位于同一个机器上,不会跨多台机器保存。
4.Spark为每个分区分配一个任务,每个worker一次可以处理一个任务。

我们知道,任务是在worker节点上执行,而分区也保存在worker节点上,
而无论任务做什么计算都是基于分区数据进行的

这就意味着:每个阶段的基础任务数等于分区数。
也就是说:每个阶段的任务不会大于分区数。
由于分区数决定了并行度,因此这也是进行性能调优时需要考虑的重要的方面。
选择适当的分区属性可以大大提高应用程序的性能

2、只读

RDD是只读的(也就意味着不可变),要想改变RDD中的数据,
只能在现有的RDD基础上创建新的RDD;
一个RDD转换为另一个RDD,通过丰富的操作算子(map、filter、union、join、
reduceByKey… …)实现,不再像MR那样只能写map和reduce了。

在这里插入图片描述

RDD的操作算子包括两类:
transformation。用来对RDD进行转化,延迟执行(Lazy);
action。用来触发RDD的计算;得到相关计算结果或者将RDD保存的文件系统中;

3、依赖

RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的
信息,RDDs之间维护着这种血缘关系(lineage),也称之为依赖。依赖包括两种:

  • 窄依赖。RDDs之间分区是一一对应的(1:1 或 n:1)
  • 宽依赖。子RDD每个分区与父RDD的每个分区都有关,是多对多的关系(即n:m)。有shuffle发生

什么是shuffle?

之前在Mapreduce中了解到, shuffle阶段一般指从Map阶段产生输出到Reduce阶段取得输出结果作为输入的这个过程称作shuffle.
在RDD中,shuffle是把父RDD中的KV对按照Key重新分区,从而得到一个新的RDD。
也就是说原本同属于父RDD同一个分区的数据需要进入到子RDD的不同的分区的过程就是shuffle.

为什么会产生shuffle ?

在分布式计算框架中,数据本地化是一个很重要的考虑,即计算需要被分发到数据所在的位置,从而减少数据的移动,提高运行效率
Map-Reduce的输入数据通常是HDFS中的文件,所以数据本地化要求map任务尽量被调度到保存了输入文件的节点执行。
但是,有一些计算逻辑是无法简单地获取本地数据的,reduce的逻辑都是如此。
对于reduce来说,处理函数的输入是key相同的所有value,但是这些value所在的数据集(即map的输出)位于不同的节点上,
因此需要对map的输出进行重新组织,使得同样的key进入相同的reducer。
shuffle移动了大量的数据,对计算、内存、网络和磁盘都有巨大的消耗,
因此,只有确实需要shuffle的地方才应该进行shuffle,否则尽可能避免shuffle

什么时候shuffle?

去重,聚合,byKey,排序(sortByKey),重分区,集合或者表操作(join,cogroup)

再回看什么是宽依赖?

父RDD的分区被子RDD的多个分区使用,例如 groupByKey、reduceByKey、sortByKey等操作会产生宽依赖,会产生shuffle

在这里插入图片描述

4、缓存

可以控制存储级别(内存、磁盘等)来进行缓存。
如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用
在这里插入图片描述

5、checkpoint

虽然RDD的血缘关系天然地可以实现容错,当RDD的某个分区数据失败或丢失,可以通过血缘关系重建。
是于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。
RDD支持 checkpoint 将数据保存到持久化的存储中, 这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从 checkpoint 处拿到数据。

1.3 Spark编程模型

spark编程模型

RDD表示数据对象

  • 通过对象上的方法调用来对RDD进行转换
  • 最终显示结果 或 将结果输出到外部数据源
  • RDD转换算子称为Transformation是Lazy的(延迟执行)
  • 只有遇到Action算子,才会执行RDD的转换操作

创建一个SparkContext把外部的数据源转换成RDD,然后调用各种算子,把数据做一个转换得到输出结果

要使用Spark,需要编写 Driver 程序(里面有个main方法,会创建SparkContext),它被提交到集群运行

  • Driver中定义了一个或多个 RDD ,并调用 RDD 上的各种算子
  • Worker则执行RDD分区计算任务
    在这里插入图片描述

1.4 RDD的创建

1、SparkContext

SparkContext是编写Spark程序用到的第一个类,是Spark的主要入口点,它负责和整个集群的交互;
在这里插入图片描述

如把Spark集群当作服务端,那么Driver就是客户端,SparkContext 是客户端的核心;

SparkContext是Spark的对外接口,负责向调用者提供 Spark 的各种功能;
SparkContext用于连接Spark集群、创建RDD、累加器、广播变量;

在 spark-shell 中 SparkContext 已经创建好了,可直接使用;
在这里插入图片描述
编写Spark Driver程序第一件事就是:创建SparkContext;

2、从集合创建RDD

从集合中创建RDD,主要用于测试。Spark 提供了以下函数:parallelize、makeRDD、range
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

val rdd1 = sc.parallelize(Array(1,2,3,4,5))
val rdd2 = sc.parallelize(1 to 100)

// 检查 RDD 分区数
rdd2.getNumPartitions
rdd2.partitions.length

// 创建 RDD,并指定分区数
val rdd2 = sc.parallelize(1 to 100)
rdd2.getNumPartitions

val rdd3 = sc.makeRDD(List(1,2,3,4,5))
val rdd4 = sc.makeRDD(1 to 100)
rdd4.getNumPartitions

val rdd5 = sc.range(1, 100, 3)
rdd5.getNumPartitions
val rdd6 = sc.range(1, 100, 2 ,10)
rdd6.getNumPartitions

备注:rdd.collect 方法在生产环境中不要使用,会造成Driver OOM

3、从文件系统创建RDD

用 textFile() 方法来从文件系统中加载数据创建RDD。方法将文件的 URI 作为参数,这个URI可以是:

  • 本地文件系统
    • 使用本地文件系统要注意:该文件是不是在所有的节点存在(在Standalone模式下)
  • 分布式文件系统HDFS的地址
  • Amazon S3的地址
// 从本地文件系统加载数据
val lines = sc.textFile("file:///root/wc.txt")

// 从分布式文件系统加载数据
val lines = sc.textFile("hdfs://linux01:9000/wcinput/wc.txt")
val lines = sc.textFile("/user/root/data/uaction.dat")
val lines = sc.textFile("data/uaction.dat")

小细节: 当从本地文件系统加载数据时,明明文件存在,却报文件不存在的错误, 并且最后还是计算出了结果, 原因是在Standalone模式下,会在所有节点扫描这个文件是否存在,而我们并没有在所有节点有这个文件,此时如果分发到各个节点,那么错误就会消失
在这里插入图片描述

4、从RDD创建RDD

本质是将一个RDD转换为另一个RDD。详细信息参见 Transformation

1. 5 Transformation【重要】

RDD的操作算子分为两类

  • Transformation。用来对RDD进行转化,这个操作时延迟执行的(或者说是Lazy 的);
  • Action。用来触发RDD的计算;得到相关计算结果 或者 将结果保存的外部系统中;
  • Transformation:返回一个新的RDD
  • Action:返回结果int、double、集合(不会返回新的RDD)

每一次 Transformation 操作都会产生新的RDD,供给下一个“转换”使用;
转换得到的RDD是惰性求值的

也就是说,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,
只有遇到 Action 操作时,才会发生真正的计算,开始从血缘关系(lineage)源头开始,进行物理的转换操作;
在这里插入图片描述

常见的 Transformation 算子:
官方文档:http://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations

常见转换算子1

map(func):对数据集中的每个元素都使用func,然后返回一个新的RDD

filter(func):对数据集中的每个元素都使用func,然后返回一个包含使func为true的元素构成的RDD

flatMap(func):与 map 类似,每个输入元素被映射为0或多个输出元素
flatmap就类似于把一堆叠在一起的数据平铺展开

mapPartitions(func):和 map 很像,但是map是将func作用在每个元素上,而mapPartitions是func作用在整个分区上。假设一个RDD有N个元素,M个分区(N>> M), 那么map的函数将被调用N次,而mapPartitions中的函数仅被调用M次,一次处理一个分区中的所有元素

mapPartitionsWithIndex(func):与 mapPartitions 类似,多了分区索引值信息全部都是窄依赖 (父RDD的每个分区都只被子RDD的一个分区使用)

val rdd1 = sc.parallelize(1 to 10)
val rdd2 = rdd1.map(_*2)
val rdd3 = rdd2.filter(_>10)


// 以上都是 Transformation 操作,没有被执行. 如何证明这些操作按预期执行,此时需要引入Action算子
rdd2.collect
rdd3.collect
// collect 是Action算子, 触发Job的执行,将RDD的全部元素从 Executor搜集到 Driver 端。生产环境中禁用

// flatMap 使用案例
val rdd4 = sc.textFile("/wcinput/wc.txt")
rdd4.collect
rdd4.flatMap(_.split("\\s+")).collect

在这里插入图片描述

// RDD 是分区,rdd1有几个区,每个分区有哪些元素
rdd1.getNumPartitions
rdd1.partitions.length
rdd1.mapPartitions{iter =>
  Iterator(s"${iter.toList}")
}.collect

rdd1.mapPartitions{iter =>
  Iterator(s"${iter.toArray.mkString("-")}")
}.collect

rdd1.mapPartitionsWithIndex{(idx, iter) =>
  Iterator(s"$idx:${iter.toArray.mkString("-")}")
}.collect

// 每个元素 * 2
val rdd5 = rdd1.mapPartitions(iter => iter.map(_*2))
rdd5.collect

在这里插入图片描述

map 与 mapPartitions 的区别
map:每次处理一条数据
mapPartitions:每次处理一个分区的数据,分区的数据处理完成后,数据才能释放,资源不足时容易导致OOM

最佳实践:当内存资源充足时,建议使用mapPartitions,以提高处理效率

常见转换算子2

groupBy(func):按照传入函数的返回值进行分组。将key相同的值放入一个迭代器

glom():将每一个分区形成一个数组,形成新的RDD类型 RDD[Array[T]], 里面是一个Array数组,其中sliding函数可以作用于数组

sample(withReplacement, fraction, seed):采样算子。以指定的随机种子(seed)随机抽样出数量为fraction的数据,withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样

distinct([numTasks])):对RDD元素去重后,返回一个新的RDD。可传入numTasks参数改变RDD分区数,这里distinct算子底层用到reduceByKey,所以是有shuffle的
在这里插入图片描述

coalesce(numPartitions):缩减分区数,无shuffle(可以设置第二个参数为true,就等同于下面的repartition算子)
在这里插入图片描述

repartition(numPartitions):增加或减少分区数,有shuffle
可以看到其实就是调用了coalesce方法,只不过把shuffle设为了true
在这里插入图片描述

sortBy(func, [ascending], [numTasks]):使用 func 对数据进行处理,对处理后的结果进行排序

宽依赖的算子(意味着都有shuffle):groupBy、distinct、repartition、sortBy…

比如groupBy,要把相同key的value数据汇聚到一个分区,但是数据可能会在不同的分区里,那就要涉及到一个数据的移动,
这个阶段就是shuffle阶段,以上的宽依赖算子都涉及到shuffle, 例如还有distinct去重,底层是使用到了reduceByKey这个算子,所以也是使用到了shuffle

重分区一般也会shuffle,因为需要在整个集群中,对之前所有的分区的数据进行随机,
均匀的打乱,然后把数据放入下游新的指定数量的分区内

// 将 RDD 中的元素按照3的余数分组
val rdd = sc.parallelize(1 to 10)
val group = rdd.groupBy(_%3)
group.collect

// 将 RDD 中的元素每10个元素分组
val rdd = sc.parallelize(1 to 101)
rdd.glom.collect
rdd.glom.map(_.sliding(10, 10).toArray)
// sliding是Scala中的方法

这里方便测试,创建一个rdd,包含101个元素, 2个分区, 通过glom算子,将每个分区形成一个数组, 也就是一个分区50个元素, 此时我们如果要将RDD中每个10元素分为一组,使用sliding方法, sliding第一个参数的含义是几个元素组成一个Array,这里是10个, 然后第二个参数是步长, 剩余不够的元素组成最后一个Array. 所以如下图所示, 原本两个分区,各50个元素组成两个Array, 然后分组后,每10个元素分成了一组,但是总的分区依旧是2个.
在这里插入图片描述

// 对数据采样。fraction采样的百分比,近似数
// 有放回的采样,使用固定的种子
rdd.sample(true, 0.2, 2).collect

// 无放回的采样,使用固定的种子
rdd.sample(false, 0.2, 2).collect
// 有放回的采样,不设置种子
rdd.sample(false, 0.2).collect

// 数据去重
val random = scala.util.Random
val arr = (1 to 20).map(x => random.nextInt(10))
val rdd = sc.makeRDD(arr)
rdd.distinct.collect

// RDD重分区
val rdd1 = sc.range(1, 10000, numSlices=10)
val rdd2 = rdd1.filter(_%2==0)
rdd2.getNumPartitions

// 减少分区数;都生效了
val rdd3 = rdd2.repartition(5)
rdd3.getNumPartitions
val rdd4 = rdd2.coalesce(5)
rdd4.getNumPartitions

// 增加分区数
val rdd5 = rdd2.repartition(20)
rdd5.getNumPartitions

// 增加分区数,这样使用没有效果
val rdd6 = rdd2.coalesce(20)
rdd6.getNumPartitions

// 增加分区数的正确用法, 第二个参数数shuffle,默认是false,增加分区是需要shuffle的
val rdd6 = rdd2.coalesce(20, true)
rdd6.getNumPartitions

// RDD元素排序
val random = scala.util.Random
val arr = (1 to 20).map(x => random.nextInt(10))
val rdd = sc.makeRDD(arr)
rdd.collect

// 数据全局有序,默认升序
rdd.sortBy(x=>x).collect
// 降序
rdd.sortBy(x=>x,false).collect

coalesce 与 repartition 的区别
在这里插入图片描述
在这里插入图片描述

repartition底层调用了coalesce,shuffle设置为true

小结

  • repartition:增大或减少分区数;有shuffle
  • coalesce:一般用于减少分区数(此时无shuffle)

常见转换算子3

RDD之间的交、并、差算子,分别如下:

  • intersection(otherRDD)
  • union(otherRDD)
  • subtract (otherRDD)

cartesian(otherRDD):笛卡尔积
zip(otherRDD):将两个RDD组合成 key-value 形式的RDD,默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常

宽依赖的算子(shuffle):intersection、subtract

val rdd1 = sc.range(1, 21)
val rdd2 = sc.range(10, 31)
rdd1.intersection(rdd2).sortBy(x=>x).collect

// 元素求并集,不去重
rdd1.union(rdd2).sortBy(x=>x).collect
rdd1.subtract(rdd2).sortBy(x=>x).collect

在这里插入图片描述

// 检查分区数
rdd1.intersection(rdd2).getNumPartitions
rdd1.union(rdd2).getNumPartitions  //分区等于两个RDD的分区和
rdd1.subtract(rdd2).getNumPartitions

在这里插入图片描述

// 笛卡尔积
val rdd1 = sc.range(1, 5)
val rdd2 = sc.range(6, 10)
rdd1.cartesian(rdd2).collect
// 检查分区数
rdd1.cartesian(rdd2).getNumPartitions
// 拉链操作
rdd1.zip(rdd2).collect
rdd1.zip(rdd2).getNumPartitions
// zip操作要求:两个RDD的partition数量以及元素数量都相同,否则会抛出异常
val rdd2 = sc.range(6, 20)
rdd1.zip(rdd2).collect

在这里插入图片描述

备注:

  • union是窄依赖。得到的RDD分区数为:两个RDD分区数之和
  • cartesian是窄依赖
    • 得到RDD的元素个数为:两个RDD元素个数的乘积
    • 得到RDD的分区数为:两个RDD分区数的乘积
    • 使用该操作会导致数据膨胀,慎用

通过DAG无环图区分宽依赖和窄依赖, stage切割规则:从后往前,遇到宽依赖就切割stage
这里intersection 和sortBy 都是宽依赖,都切分了stage
在这里插入图片描述
subtract也是宽依赖
在这里插入图片描述

Union是窄依赖, distinct是宽依赖
在这里插入图片描述

1.6 Action

Action 用来触发RDD的计算,得到相关计算结果;
Action触发会形成Job。
一个Spark程序(Driver程序)包含了多少 Action 算子,那么就有多少Job;

典型的Action算子: collect / count
collect() => sc.runJob() => ... => dagScheduler.runJob() => 触发了Job

在这里插入图片描述
在这里插入图片描述

collect() / collectAsMap()
stats / count / mean / stdev / max / min
reduce(func) / fold(func) / aggregate(func)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

first():Return the first element in this RDD
take(n):Take the first num elements of the RDD
top(n):按照默认(降序)或者指定的排序规则,返回前num个元素。
takeSample(withReplacement, num, [seed]):返回采样的数据
foreach(func) / foreachPartition(func):与map、mapPartitions类似,区别是
foreach 是 Action

saveAsTextFile(path) / saveAsSequenceFile(path) / saveAsObjectFile(path)

// 返回统计信息。仅能作用 RDD[Double] 类型上调用
val rdd1 = sc.range(1, 101)
rdd1.stats

val rdd2 = sc.range(1, 101)

// 不能调用
rdd1.zip(rdd2).stats

// count在各种类型的RDD上,均能调用
rdd1.zip(rdd2).count

// 聚合操作
val rdd = sc.makeRDD(1 to 10, 2)
rdd.reduce(_+_)  //55

rdd.fold(0)(_+_)  //55
rdd.fold(1)(_+_)  //55+3=58
rdd.fold(1)((x, y) => {
println(s"x=$x, y=$y")
x+y
})

其中zeroValue代表初始值
在这里插入图片描述

fold(初始值)(+) 执行过程, 首先按照分区进行局部汇总,然后每次局部汇总都加上初值, 最后一次全局汇总, 所以全部元素相加, 相当于 所有元素的和+ 初始值 * (分区数+1)
在这里插入图片描述

其中fold的局部汇总和全局汇总的算法是一样的, 那么能否局部汇总使用一种算法, 全局汇总使用另一种算法呢?

于是就有了 aggregate
在这里插入图片描述


val rdd = sc.parallelize(1 to 10, 10)

rdd.aggregate(0)(_+_, _+_)  // 这第一个参数代表局部汇总,第二个参数代表全局汇总 55
rdd.aggregate(1)(_+_, _+_)  // 这种方式定义的局部和全局的算法都是累加,所以结果等同于rdd.fold(1)(_+_)
rdd.aggregate(1)(
(a, b) => {
 println(s"a=$a, b=$b")
 a+b
},
(x, y) => {
 println(s"x=$x, y=$y")
 x+y
})

其他的一些算子

// first / take(n) / top(n) :获取RDD中的元素。多用于测试
rdd.first
rdd.take(10)
rdd.top(10)
// 采样并返回结果
rdd.takeSample(false, 5)
 
//遍历rdd每个元素
rdd.foreach()

// 保存文件到指定路径(rdd有多少分区,就保存为多少文件,保存文件时注意小文件问题)
rdd.saveAsTextFile("data/t1")

1.7 Key-Value RDD操作

RDD整体上分为 Value 类型和 Key-Value 类型
前面介绍的是 Value 类型的RDD的操作,实际使用更多的是 key-value 类型的RDD,也称为 PairRDD。
Value 类型RDD的操作基本集中在 RDD.scala 中;
key-value 类型的RDD操作集中在 PairRDDFunctions.scala 中;
在这里插入图片描述

前面介绍的大多数算子对 Pair RDD 都是有效的。Pair RDD还有属于自己的
Transformation、Action 算子;

1.7.1 创建Pair RDD

val arr = (1 to 10).toArray
val arr1 = arr.map(x => (x, x*10, x*100))

//rdd1不是Pair RDD
val rdd1 = sc.makeRDD(arr1)

//rdd2是Pair RDD
val arr2 = arr.map(x => (x, (x*10, x*100)))
val rdd2 = sc.makeRDD(arr2)

1.7.2 Transformation操作

1、类似 map 操作

mapValues / flatMapValues / keys / values,这些操作都可以使用 map 操作实现,是简化操作。

val a = sc.parallelize(List((1,2),(3,4),(5,6)))
// 使用 mapValues 更简洁
val b = a.mapValues(x=>1 to x)
b.collect //(1,Range 1 to 2) (3,Range 1 to 4) (5,Range 1 to 6)

// 可使用map实现同样的操作
val b = a.map(x => (x._1, 1 to x._2))
b.collect
val b = a.map{case (k, v) => (k, 1 to v)}
b.collect

// flatMapValues 将 value 的值压平
val c = a.flatMapValues(x=>1 to x)
c.collect
val c = a.mapValues(x=>1 to x).flatMap{case (k, v) => v.map(x=> (k, x))}
c.collect
val c = a.map(x=>(x._1, 1 to x._2)).flatMap{case (k,v) => v.map(elem => (k, elem))}
c.collect
// 上面三种形式的结果都是一样的,但是最原始的方式还是使用map操作

// 获取键和值
c.keys
c.values
c.map{case (k, v) => k}.collect
c.map{case (k, _) => k}.collect
c.map{case (_, v) => v}.collect

2、聚合操作【重要】

PariRDD(k, v)使用范围广,聚合操作.
groupByKey / reduceByKey / foldByKey / aggregateByKey (Transformation算子)
combineByKey(OLD) / combineByKeyWithClassTag (NEW) => 底层实现
subtractByKey:类似于subtract,删掉 RDD 中键与 other RDD 中的键相同的元素

在这里插入图片描述

小案例:给定一组数据:
("spark", 12), ("hadoop", 26), ("hadoop", 23), ("spark",15), ("scala", 26), 
("spark", 25), ("spark", 23), ("hadoop", 16), ("scala", 24), ("spark",16),
 键值对的key表示图书名称,value表示某天图书销量。计算每个键对应的平均值,
也就是计算每种图书的每天平均销量
val rdd = sc.makeRDD(Array(("spark", 12), ("hadoop", 26),
("hadoop", 23), ("spark", 15), ("scala", 26), ("spark", 25),
("spark", 23), ("hadoop", 16), ("scala", 24), ("spark", 16)))

// groupByKey : 下面三种方式结果都一样
rdd.groupByKey().map(x => (x._1, x._2.sum.toDouble / x._2.size)).foreach(println) //(scala,25.0) (spark,18.2) (hadoop,21.666666666666668)
rdd.groupByKey().map{case (k,v) => (k, v.sum.toDouble / v.size)}.foreach(println)
rdd.groupByKey().mapValues(v => v.sum.toDouble / v.size).foreach(println)
-------------------------------------------------------------------------------------
//reduceByKey
//第一步: 通过mapValues算子,把 (spark,12) => (spark,(12,1))
rdd.mapValues((_,1)).foreach(println) //(spark,(12,1)) (hadoop,(26,1))

//第二步: 我们通过把value的第一个元素相加得到数量, 第二个元素相加得到这个key出现的个数
//reduceByKey 会寻找相同key的数据,当找到两条记录时,参数(x,y)就代表两条记录的value,这里是(x1,x2)(y1,y2) 
//做(x,y) => (x1+y1,x2+y2) 处理,反复执行这个操作,直到每个key只剩下一条记录
rdd.mapValues((_,1))
    .reduceByKey((x,y) => (x._1 + y._1, x._2 + y._2))
    .foreach(println) //(scala,(50,2)) (spark,(91,5)) (hadoop,(65,3))
    
//第三步: 就最后相除一下得到结果
rdd.mapValues((_,1))
    .reduceByKey((x,y) => (x._1 + y._1, x._2 + y._2))
    .mapValues( x => x._1.toDouble / x._2)
    .foreach(println) //(scala,25.0) (spark,18.2) (hadoop,21.666666666666668)
------------------------------------------------------------------------------------
// foldByKey  : 这种方式与上面类似,只不过多了一个初值,这里(0,0)代表,每个分区的元素都要计算这个初值  
rdd.mapValues((_ , 1)).foldByKey((0,0))((x,y) => (x._1+y._1, x._2+y._2)).foreach(println) //(scala,(50,2)) (spark,(91,5)) (hadoop,(65,3))
rdd.mapValues((_ , 1))
    .foldByKey((0,0))((x,y) => (x._1+y._1, x._2+y._2))
    .mapValues(x => x._1.toDouble / x._2)
    .foreach(println)  //(scala,25.0) (spark,18.2) (hadoop,21.666666666666668)
------------------------------------------------------------------------------------
// aggregateByKey
// aggregateByKey => 定义初值 + 分区内的聚合函数(x,y) + 分区间的聚合函数(a,b)
rdd.mapValues((_, 1))
  .aggregateByKey((0,0))(
    (x,y) => {println(s"x=$x, y=$y"); (x._1 + y._1, x._2 + y._2)},
    (a,b) => {println(s"a=$a, b=$b"); (a._1 + b._1, a._2 + b._2)}
  ).mapValues(x => x._1.toDouble / x._2).foreach(println)  

//以下是打印结果
//首先,rdd通过mapValues算子, 把value的值变成了一个元组类型,并把第二个值变成1,例如("spark",12)=> (12,1) 
// 所以此时, x代表初值(0,0), y代表传入的value值(12,1)
// (x._1 + y._1, x._2 + y._2) => (0+12,0+1) 作为下一步的x值, 再与下一个key相同的value值(也就是y值)进行计算
x=(0,0), y=(12,1) ------ ("spark", 12) 
x=(0,0), y=(26,1) ------ ("hadoop", 26)
x=(26,1), y=(23,1)------ ("hadoop", 23)
x=(12,1), y=(15,1)------ ("spark", 15)
x=(0,0), y=(26,1) ------ ("scala", 26)
x=(27,2), y=(25,1)------ ("spark", 25)
x=(52,3), y=(23,1)------ ("spark", 23)
x=(49,2), y=(16,1)------ ("hadoop", 16)
x=(26,1), y=(24,1)------ ("scala", 24)
x=(75,4), y=(16,1)------ ("spark", 16)

----------------------------------------------------------------------------------------
// 换种方式,我们根本不需要先通过mapValues进行转换,直接使用aggregateByKey算子
// 这里x,就是初值(0,0), y代表 (spark,1)中的1,是我们传入的Int值
// 所以这里 x._1 + y
rdd.aggregateByKey((0,0))(
  (x,y) => {println(s"x=$x, y=$y"); (x._1 + y, x._2 + 1)},
  (a,b) => {println(s"a=$a, b=$b"); (a._1 + b._1, a._2 + b._2)}
).mapValues(x => x._1.toDouble / x._2).collect 
//打印出来可以看到过程,首先这是一个分区,所以很清晰的按照rdd内元素的顺序进行计算
//其中分区内的计算规则(x._1 + y, x._2 + 1), 分区间的规则:(a._1 + b._1, a._2 + b._2)
//x=(0,0), y=12  ------ ("spark", 12) 
//x=(0,0), y=26  ------ ("hadoop", 26)
//x=(26,1), y=23 ------ ("hadoop", 23)
//x=(12,1), y=15 ------ ("spark", 15)
//x=(0,0), y=26  ------ ("scala", 26)
//x=(27,2), y=25 ------ ("spark", 25) 
//x=(52,3), y=23 ------ ("spark", 23)
//x=(49,2), y=16 ------ ("hadoop", 16)
//x=(26,1), y=24 ------ ("scala", 24)
//x=(75,4), y=16 ------ ("spark", 16)

梳理一下流程:
第一个元素(spark,12),我们说了x代表初值一个元组(0,0),y代表value值,也就是12,所以按照后面的计算
(x._1 + y, x._2 +1) => (spark,(0+12,0+1))
接着计算第二个元素(hadoop,26), x还是初值, 我们推测,那个初值的作用就是给每个key的第一个value赋初值(0,0)
(x._1 + y, x._2 +1) => (hadoop,(0+26,0+1))
接着计算第三个元素(hadoop,23), 此时key为hadoop的元素,value就不是初值了,(26,1),那么就从这个值开始
(x._1 + y, x._2 +1) => (hadoop,(26+23,1+1)) => (hadoop,(49,2)) => 直到遇到下一个key为hadoop的元素,(49,2)开始累加
...后面以此类推
这是计算完分区内的元素,还要计算分区间的元素  
由于分区内最后计算出来得到的值就是一个元组,比如上面的结果最后一条
x=(75,4),y=16,计算完代表这个分区内,key为spark的元素,已经计算完了,
值为(75+16,4+1) => (91,5) ,这个值作为分区内的最后一个元素,也是下一步分区间计算的初值
假设还有一个分区,里面也有key为spark的值,最后得到一个元组比如(q,w)
那么分区间的计算两个数据类型都是元组,所以可以直接相加,
那么分区间的计算规则(a,b) => (a._1 + b._1, a._2 + b._2)
a代表第一个分区间的结果值,b代表第二个分区间的结果值
最后得到(91+q, 5+w) 
--------------------------------------------------
这里最疑惑的点在于x和y究竟代表什么,一句可能不太恰当的理解
x就是传入的初值,这个值是按照ByKey的方式给每个唯一Key赋的值,作为第一步,
然后y值就是传入参数的value值                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        

理解:

reduceByKey会寻找相同key的数据,当找到这样的两条记录时会对其value(分别记为x,y)做 (x,y)=> XXX 的处理,即只保留求和之后的数据作为value。反复执行这个操作直至每个key只留下一条记录。

对于aggregateByKey的源码进行解析理解一下
在这里插入图片描述

我们对比上面的案例
rdd.aggregateByKey((0,0))(
  (x,y) => {println(s"x=$x, y=$y"); (x._1 + y, x._2 + 1)},
  (a,b) => {println(s"a=$a, b=$b"); (a._1 + b._1, a._2 + b._2)}
).mapValues(x => x._1.toDouble / x._2).collect 

其中初值zeroValue的初值是U,
分区内的函数 (U,V) => U 
其中(U,V) 就代表(x,y) ,然后x = U,最后得到一个U类型的数据
分区间的函数 (U,U) 代表(a,b) 最终得到一个 RDD[(K,U)]
// 分区内的合并与分区间的合并,可以采用不同的方式;这种方式是低效的!
rdd.aggregateByKey(scala.collection.mutable.ArrayBuffer[Int]
())(
 (x, y) => {x.append(y); x},
 (a, b) => {a++b}
).mapValues(v => v.sum.toDouble/v.size).collect

// combineByKey(理解就行)
rdd.combineByKey(
(x: Int) => {println(s"x=$x"); (x,1)},
(x: (Int, Int), y: Int) => {println(s"x=$x, y=$y");(x._1+y,x._2+1)},
(a: (Int, Int), b: (Int, Int)) => {println(s"a=$a, b=$b");
(a._1+b._1, a._2+b._2)}
).mapValues(x=>x._1.toDouble/x._2).collect

// subtractByKey
val rdd1 = sc.makeRDD(Array(("spark", 12), ("hadoop", 26),("hadoop", 23), ("spark", 15)))
val rdd2 = sc.makeRDD(Array(("spark", 100), ("hadoop", 300)))
rdd1.subtractByKey(rdd2).collect()

// subtractByKey : 类似于subtrac, 删掉 RDD 中键与 other RDD 中的键相同的元素
val rdd = sc.makeRDD(Array(("a",1), ("b",2), ("c",3), ("a",5),("d",5)))
val other = sc.makeRDD(Array(("a",10), ("b",20), ("c",30)))
rdd.subtractByKey(other).collect()

结论:效率相等用最熟悉的方法;groupByKey在一般情况下效率低,尽量少用
初学:最重要的是实现;如果使用了groupByKey,寻找替换的算子实现;

在这里插入图片描述

groupByKey Shuffle过程中传输的数据量大,效率低
在这里插入图片描述

3、排序操作

sortByKey:sortByKey函数作用于PairRDD,对Key进行排序。
在org.apache.spark.rdd.OrderedRDDFunctions 中实现:
在这里插入图片描述
在这里插入图片描述

val a = sc.parallelize(List("wyp", "iteblog", "com","397090770", "test"))
val b = sc.parallelize (1 to a.count.toInt)
val c = a.zip(b)  //进行拉链操作
c.sortByKey().collect
c.sortByKey(false).collect

4、join操作

cogroup / join / leftOuterJoin / rightOuterJoin / fullOuterJoin

cogroup:对两个RDD中的KV元素,每个RDD中相同key中的元素分别聚合成一个集合。
与reduceByKey不同的是针对两个RDD中相同的key的元素进行合并

val rdd1 = sc.makeRDD(Array((1,"Spark"), (2,"Hadoop"),(3,"Kylin"), (4,"Flink")))
val rdd2 = sc.makeRDD(Array((3,"李四"), (4,"王五"), (5,"赵六"),(6,"冯七")))
val rdd3 = rdd1.cogroup(rdd2)
rdd3.collect.foreach(println)

// 相同key的元素,进行了合并
//(4,(CompactBuffer(Flink),CompactBuffer(王五)))
//(1,(CompactBuffer(Spark),CompactBuffer()))
//(6,(CompactBuffer(),CompactBuffer(冯七)))
//(3,(CompactBuffer(Kylin),CompactBuffer(李四)))
//(5,(CompactBuffer(),CompactBuffer(赵六)))
//(2,(CompactBuffer(Hadoop),CompactBuffer()))
----------------------------------------------

rdd3.filter{case (_, (v1, v2)) => v1.nonEmpty & v2.nonEmpty}.collect

// 仿照源码实现join操作
rdd3.flatMapValues( pair =>for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, w))

val rdd1 = sc.makeRDD(Array(("1","Spark"),("2","Hadoop"),("3","Scala"),("4","Java")))
val rdd2 = sc.makeRDD(Array(("3","20K"),("4","18K"),("5","25K"),("6","10K")))

rdd1.join(rdd2).collect
rdd1.leftOuterJoin(rdd2).collect
rdd1.rightOuterJoin(rdd2).collect
rdd1.fullOuterJoin(rdd2).collect

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值