常用RDD算子讲解(Scala语言)

一、目标
  1. 理解 RDD 的算子分类, 以及其特性

  2. 理解常见算子的使用

二、分类

RDD 中的算子从功能上分为两大类

  1. Transformation(转换) 它会在一个已经存在的 RDD 上创建一个新的 RDD, 将旧的 RDD 的数据转换为另外一种形式后放入新的 RDD

  2. Action(动作) 执行各个分区的计算任务, 将的到的结果返回到 Driver 中

RDD 中可以存放各种类型的数据, 那么对于不同类型的数据, RDD 又可以分为三类

  • 针对基础类型(例如 String)处理的普通算子

  • 针对 Key-Value 数据处理的 byKey 算子

  • 针对数字类型数据处理的计算算子

三、特点
  • Spark 中所有的 Transformations 是 Lazy(惰性) 的, 它们不会立即执行获得结果. 相反, 它们只会记录在数据集上要应用的操作. 只有当需要返回结果给 Driver 时, 才会执行这些操作, 通过 DAGScheduler 和 TaskScheduler 分发到集群中运行, 这个特性叫做 惰性求值

  • 默认情况下, 每一个 Action 运行的时候, 其所关联的所有 Transformation RDD 都会重新计算, 但是也可以使用 presist 方法将 RDD 持久化到磁盘或者内存中. 这个时候为了下次可以更快的访问, 会把数据保存到集群上.

四、常见的Transformations 算子

Transformation function

解释

map(T ⇒ U)

// 创建一个并行化的数据集,数据来源于 Seq(1, 2, 3)
sc.parallelize(Seq(1, 2, 3))  
// 对数据集中的每个元素进行映射操作,将每个元素乘以 10
.map( num => num * 10 )  
// 收集处理后的结果
.collect()  

57c2f77284bfa8f99ade091fdd7e9f83

c59d44296918b864a975ebbeb60d4c04

作用

  • 把 RDD 中的数据 一对一 的转为另一种形式

签名

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

参数

  • f → Map 算子是 原RDD → 新RDD 的过程, 传入函数的参数是原 RDD 数据, 返回值是经过函数转换的新 RDD 的数据

注意点

  • Map 是一对一, 如果函数是 String → Array[String] 则新的 RDD 中每条数据就是一个数组

flatMap(T ⇒ List[U])

// 创建一个并行化的数据集,数据来源于包含三个字符串的 Seq
sc.parallelize(Seq("Hello lily", "Hello lucy", "Hello tim"))  
// 对数据集中的每个字符串按照空格分割成单词,并将这些单词展平为一个新的序列
.flatMap( line => line.split(" ") )  
// 收集处理后的结果
.collect()  

ec39594f30ca4d59e2ef5cdc60387866

f6c4feba14bb71372aa0cb678067c6a8

作用

  • FlatMap 算子和 Map 算子类似, 但是 FlatMap 是一对多

调用

def flatMap[U: ClassTag](f: T ⇒ List[U]): RDD[U]

参数

  • f → 参数是原 RDD 数据, 返回值是经过函数转换的新 RDD 的数据, 需要注意的是返回值是一个集合, 集合中的数据会被展平后再放入新的 RDD

注意点

  • flatMap 其实是两个操作, 是 map + flatten, 也就是先转换, 后把转换而来的 List 展开

  • Spark 中并没有直接展平 RDD 中数组的算子, 可以使用 flatMap 做这件事

filter(T ⇒ Boolean)

// 创建一个并行化的数据集,数据来源于包含 1、2、3 的 Seq
sc.parallelize(Seq(1, 2, 3))  
// 表示只保留值大于等于 3 的元素
.filter( value => value >= 3 )  
// 收集筛选后的结果
.collect() 

25a7aef5e2b8a39145d503f4652cc945

05cdb79abd41a7b5baa41a4c62870d73

作用

  • Filter 算子的主要作用是过滤掉不需要的内容

mapPartitions(List[T] ⇒ List[U])

RDD[T] ⇒ RDD[U] 和 map 类似, 但是针对整个分区的数据转换

mapPartitionsWithIndex

和 mapPartitions 类似, 只是在函数中增加了分区的 Index

mapValues

// 创建一个并行化的数据集,数据来源于包含键值对的 Seq
sc.parallelize(Seq(("a", 1), ("b", 2), ("c", 3)))  
// 对数据集中的每个键值对,仅对值进行映射操作,将值乘以 10
.mapValues( value => value * 10 )  
// 收集处理后的结果
.collect()  

7a8b280a054fdab8e8d14549f67b85f9

5551847febe453b134f3a4009df01bec

作用

  • MapValues 只能作用于 Key-Value 型数据, 和 Map 类似, 也是使用函数按照转换数据, 不同点是 MapValues 只转换 Key-Value 中的 Value

sample(withReplacement, fraction, seed)

// 创建一个并行化的数据集,数据来源于包含 1 到 10 的 Seq
sc.parallelize(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))  
// 对数据集进行抽样操作
.sample(withReplacement = true, 0.6, 2)  
// 收集抽样后的结果
.collect()  


// 在 sample 方法中
// withReplacement = true 表示抽样是有放回的,即同一个元素可能被多次选中
// 0.6 表示抽样比例
// 2 是随机数生成器的种子,用于保证每次抽样结果的可重复性(如果需要)

03139edb0211652195dccea955f3a9b3

ccd1ae121f6f6852158c044441437f04

作用

  • Sample 算子可以从一个数据集中抽样出来一部分, 常用作于减小数据集以保证运行速度, 并且尽可能少规律的损失

参数

  • Sample 接受第一个参数为 withReplacement, 意为是否取样以后是否还放回原数据集供下次使用, 简单的说, 如果这个参数的值为 true, 则抽样出来的数据集中可能会有重复

  • Sample 接受第二个参数为 fraction, 意为抽样的比例

  • Sample 接受第三个参数为 seed, 随机数种子, 用于 Sample 内部随机生成下标, 一般不指定, 使用默认值

union(other)

val rdd1 = sc.parallelize(Seq(1, 2, 3))
val rdd2 = sc.parallelize(Seq(4, 5, 6))
rdd1.union(rdd2)
  .collect()

5f31c2c44aa66db3027fea4624a3c4eb

2a8b7d10930251ae32d6d276ab7f41f8

intersection(other)

val rdd1 = sc.parallelize(Seq(1, 2, 3, 4, 5))
val rdd2 = sc.parallelize(Seq(4, 5, 6, 7, 8))
rdd1.intersection(rdd2)
  .collect()

a4475b1193be01efc305ef3c39f4b1e8

76a9873eae8de8a9ed5223921da7c245

作用

  • Intersection 算子是一个集合操作, 用于求得 左侧集合 和 右侧集合 的交集, 换句话说, 就是左侧集合和右侧集合都有的元素, 并生成一个新的 RDD

subtract(other, numPartitions)

(RDD[T], RDD[T]) ⇒ RDD[T] 差集, 可以设置分区数

distinct(numPartitions)

sc.parallelize(Seq(1, 1, 2, 2, 3))
  .distinct()
  .collect()

a8cd033d9ce502337ba746d05ca94ae1

2bfefe5f5cab497d5aded3b7537a58ba

作用

  • Distinct 算子用于去重

注意点

  • Distinct 是一个需要 Shuffled 的操作

  • 本质上 Distinct 就是一个 reductByKey, 把重复的合并为一个

reduceByKey((V, V) ⇒ V, numPartition)

sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
  .reduceByKey( (curr, agg) => curr + agg )
  .collect()

a9b444d144d6996c83b33f6a48806a1a

07678e1b4d6ba1dfaf2f5df89489def4

作用

  • 首先按照 Key 分组生成一个 Tuple, 然后针对每个组执行 reduce 算子

调用

def reduceByKey(func: (V, V) ⇒ V): RDD[(K, V)]

参数

  • func → 执行数据处理的函数, 传入两个参数, 一个是当前值, 一个是局部汇总, 这个函数需要有一个输出, 输出就是这个 Key 的汇总结果

注意点

  • ReduceByKey 只能作用于 Key-Value 型数据, Key-Value 型数据在当前语境中特指 Tuple2

  • ReduceByKey 是一个需要 Shuffled 的操作

  • 和其它的 Shuffled 相比, ReduceByKey是高效的, 因为类似 MapReduce 的, 在 Map 端有一个 Cominer, 这样 I/O 的数据便会减少

groupByKey()

sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
  .groupByKey()
  .collect()

466c1ad2b738c4f0d27f2557ecedaf5b

27de81df110abb6709bf1c5ffad184ab

作用

  • GroupByKey 算子的主要作用是按照 Key 分组, 和 ReduceByKey 有点类似, 但是 GroupByKey 并不求聚合, 只是列举 Key 对应的所有 Value

注意点

  • GroupByKey 是一个 Shuffled

  • GroupByKey 和 ReduceByKey 不同, 因为需要列举 Key 对应的所有数据, 所以无法在 Map 端做 Combine, 所以 GroupByKey 的性能并没有 ReduceByKey 好

combineByKey()

val rdd = sc.parallelize(Seq(
  ("zhangsan", 99.0),
  ("zhangsan", 96.0),
  ("lisi", 97.0),
  ("lisi", 98.0),
  ("zhangsan", 97.0))
)

val combineRdd = rdd.combineByKey(
  score => (score, 1),
  (scoreCount: (Double, Int),newScore) => (scoreCount._1 + newScore, scoreCount._2 + 1),
  (scoreCount1: (Double, Int), scoreCount2: (Double, Int)) =>
    (scoreCount1._1 + scoreCount2._1, scoreCount1._2 + scoreCount2._2)
)

val meanRdd = combineRdd.map(score => (score._1, score._2._1 / score._2._2))

meanRdd.collect()

741d814a50e4c01686f394df079458bf

作用

  • 对数据集按照 Key 进行聚合

调用

  • combineByKey(createCombiner, mergeValue, mergeCombiners, [partitioner], [mapSideCombiner], [serializer])

参数

  • createCombiner 将 Value 进行初步转换

  • mergeValue 在每个分区把上一步转换的结果聚合

  • mergeCombiners 在所有分区上把每个分区的聚合结果聚合

  • partitioner 可选, 分区函数

  • mapSideCombiner 可选, 是否在 Map 端 Combine

  • serializer 序列化器

注意点

  • combineByKey 的要点就是三个函数的意义要理解

  • groupByKeyreduceByKey 的底层都是 combineByKey

aggregateByKey()

val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.aggregateByKey(0.8)(
  seqOp = (zero, price) => price * zero,
  combOp = (curr, agg) => curr + agg
).collect()
println(result)

ee33b17dbc78705dbbd76d76ab4a9072

作用

  • 聚合所有 Key 相同的 Value, 换句话说, 按照 Key 聚合 Value

调用

  • rdd.aggregateByKey(zeroValue)(seqOp, combOp)

参数

  • zeroValue 初始值

  • seqOp 转换每一个值的函数

  • comboOp 将转换过的值聚合的函数

注意点 * 为什么需要两个函数? aggregateByKey 运行将一个 RDD[(K, V)] 聚合为 RDD[(K, U)], 如果要做到这件事的话, 就需要先对数据做一次转换, 将每条数据从 V 转为 UseqOp 就是干这件事的 ** 当 seqOp 的事情结束以后, comboOp 把其结果聚合

  • 和 reduceByKey 的区别::

    • aggregateByKey 最终聚合结果的类型和传入的初始值类型保持一致

    • reduceByKey 在集合中选取第一个值作为初始值, 并且聚合过的数据类型不能改变

foldByKey(zeroValue)((V, V) ⇒ V)

sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1)))
  .foldByKey(zeroValue = 10)( (curr, agg) => curr + agg )
  .collect()

c00063a109a0f9e0b1c2b385c5e1cc47

a406ff8395bb092e719007661b34d385

作用

  • 和 ReduceByKey 是一样的, 都是按照 Key 做分组去求聚合, 但是 FoldByKey 的不同点在于可以指定初始值

调用

foldByKey(zeroValue)(func)

参数

  • zeroValue 初始值

  • func seqOp 和 combOp 相同, 都是这个参数

注意点

  • FoldByKey 是 AggregateByKey 的简化版本, seqOp 和 combOp 是同一个函数

  • FoldByKey 指定的初始值作用于每一个 Value

join(other, numPartitions)

val rdd1 = sc.parallelize(Seq(("a", 1), ("a", 2), ("b", 1)))
val rdd2 = sc.parallelize(Seq(("a", 10), ("a", 11), ("a", 12)))

rdd1.join(rdd2).collect()

bb3eda1410d3b0f6e1bff6d5e6a45879

作用

  • 将两个 RDD 按照相同的 Key 进行连接

调用

join(other, [partitioner or numPartitions])

参数

  • other 其它 RDD

  • partitioner or numPartitions 可选, 可以通过传递分区函数或者分区数量来改变分区

注意点

  • Join 有点类似于 SQL 中的内连接, 只会再结果中包含能够连接到的 Key

  • Join 的结果是一个笛卡尔积形式, 例如 "a", 1), ("a", 2 和 "a", 10), ("a", 11 的 Join 结果集是 "a", 1, 10), ("a", 1, 11), ("a", 2, 10), ("a", 2, 11

cogroup(other, numPartitions)

val rdd1 = sc.parallelize(Seq(("a", 1), ("a", 2), ("a", 5), ("b", 2), ("b", 6), ("c", 3), ("d", 2)))
val rdd2 = sc.parallelize(Seq(("a", 10), ("b", 1), ("d", 3)))
val rdd3 = sc.parallelize(Seq(("b", 10), ("a", 1)))

val result1 = rdd1.cogroup(rdd2).collect()
val result2 = rdd1.cogroup(rdd2, rdd3).collect()

/*
执行结果:
Array(
  (d,(CompactBuffer(2),CompactBuffer(3))),
  (a,(CompactBuffer(1, 2, 5),CompactBuffer(10))),
  (b,(CompactBuffer(2, 6),CompactBuffer(1))),
  (c,(CompactBuffer(3),CompactBuffer()))
)
 */
println(result1)

/*
执行结果:
Array(
  (d,(CompactBuffer(2),CompactBuffer(3),CompactBuffer())),
  (a,(CompactBuffer(1, 2, 5),CompactBuffer(10),CompactBuffer(1))),
  (b,(CompactBuffer(2, 6),CompactBuffer(1),Co...
 */
println(result2)

42262ffe7f3ff35013fbe534d78e3518

作用

  • 多个 RDD 协同分组, 将多个 RDD 中 Key 相同的 Value 分组

调用

  • cogroup(rdd1, rdd2, rdd3, [partitioner or numPartitions])

参数

  • rdd…​ 最多可以传三个 RDD 进去, 加上调用者, 可以为四个 RDD 协同分组

  • partitioner or numPartitions 可选, 可以通过传递分区函数或者分区数来改变分区

注意点

  • 对 RDD1, RDD2, RDD3 进行 cogroup, 结果中就一定会有三个 List, 如果没有 Value 则是空 List, 这一点类似于 SQL 的全连接, 返回所有结果, 即使没有关联上

  • CoGroup 是一个需要 Shuffled 的操作

cartesian(other)

(RDD[T], RDD[U]) ⇒ RDD[(T, U)] 生成两个 RDD 的笛卡尔积

sortBy(ascending, numPartitions)

val rdd1 = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
val sortByResult = rdd1.sortBy( item => item._2 ).collect()
val sortByKeyResult = rdd1.sortByKey().collect()

println(sortByResult)
println(sortByKeyResult)

作用

  • 排序相关相关的算子有两个, 一个是 sortBy, 另外一个是 sortByKey

调用

sortBy(func, ascending, numPartitions)

参数

  • func 通过这个函数返回要排序的字段

  • ascending 是否升序

  • numPartitions 分区数

注意点

  • 普通的 RDD 没有 sortByKey, 只有 Key-Value 的 RDD 才有

  • sortBy 可以指定按照哪个字段来排序, sortByKey 直接按照 Key 来排序

partitionBy(partitioner)

使用用传入的 partitioner 重新分区, 如果和当前分区函数相同, 则忽略操作

coalesce(numPartitions)

减少分区数

val rdd = sc.parallelize(Seq(("a", 3), ("b", 2), ("c", 1)))
val oldNum = rdd.partitions.length

val coalesceRdd = rdd.coalesce(4, shuffle = true)
val coalesceNum = coalesceRdd.partitions.length

val repartitionRdd = rdd.repartition(4)
val repartitionNum = repartitionRdd.partitions.length

print(oldNum, coalesceNum, repartitionNum)

作用

  • 一般涉及到分区操作的算子常见的有两个, repartitioin 和 coalesce, 两个算子都可以调大或者调小分区数量

调用

  • repartitioin(numPartitions)

  • coalesce(numPartitions, shuffle)

参数

  • numPartitions 新的分区数

  • shuffle 是否 shuffle, 如果新的分区数量比原分区数大, 必须 Shuffled, 否则重分区无效

注意点

  • repartition 和 coalesce 的不同就在于 coalesce 可以控制是否 Shuffle

  • repartition 是一个 Shuffled 操作

repartition(numPartitions)

重新分区

repartitionAndSortWithinPartitions

重新分区的同时升序排序, 在 partitioner 中排序, 比先重分区再排序要效率高, 建议使用在需要分区后再排序的场景使用

五、常用Action算子
Action function解释

reduce( (T, T) ⇒ U )

val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.reduce((curr, agg) => ("总价", curr._2 + agg._2))
println(result)

作用

  • 对整个结果集规约, 最终生成一条数据, 是整个数据集的汇总

调用

  • reduce( (currValue[T], agg[T]) ⇒ T )

注意点

  • reduce 和 reduceByKey 是完全不同的, reduce 是一个 action, 并不是 Shuffled 操作

  • 本质上 reduce 就是现在每个 partition 上求值, 最终把每个 partition 的结果再汇总

collect()

以数组的形式返回数据集中所有元素

count()

返回元素个数

first()

返回第一个元素

take( N )

返回前 N 个元素

takeSample(withReplacement, fract)

类似于 sample, 区别在这是一个Action, 直接返回结果

fold(zeroValue)( (T, T) ⇒ U )

指定初始值和计算函数, 折叠聚合整个数据集

saveAsTextFile(path)

将结果存入 path 对应的文件中

saveAsSequenceFile(path)

将结果存入 path 对应的 Sequence 文件中

countByKey()

val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
val result = rdd.countByKey()
println(result)

作用

  • 求得整个数据集中 Key 以及对应 Key 出现的次数

注意点

  • 返回结果为 Map(key → count)

  • 常在解决数据倾斜问题时使用, 查看倾斜的 Key

foreach( T ⇒ …​ )

遍历每一个元素

应用

```scala
val rdd = sc.parallelize(Seq(("手机", 10.0), ("手机", 15.0), ("电脑", 20.0)))
// 结果: Array((手机,10.0), (手机,15.0), (电脑,20.0))
println(rdd.collect())
// 结果: Array((手机,10.0), (手机,15.0))
println(rdd.take(2))
// 结果: (手机,10.0)
println(rdd.first())
```

总结

RDD 的算子大部分都会生成一些专用的 RDD

  • mapflatMapfilter 等算子会生成 MapPartitionsRDD

  • coalescerepartition 等算子会生成 CoalescedRDD

常见的 RDD 有两种类型

  • 转换型的 RDD, Transformation

  • 动作型的 RDD, Action

常见的 Transformation 类型的 RDD

  • map

  • flatMap

  • filter

  • groupBy

  • reduceByKey

常见的 Action 类型的 RDD

  • collect

  • countByKey

  • reduce

六、RDD 对不同类型数据的支持

目标

  1. 理解 RDD 对 Key-Value 类型的数据是有专门支持的

  2. 理解 RDD 对数字类型也有专门的支持

一般情况下 RDD 要处理的数据有三类

  • 字符串

  • 键值对

  • 数字型

RDD 的算子设计对这三类不同的数据分别都有支持

  • 对于以字符串为代表的基本数据类型是比较基础的一些的操作, 诸如 map, flatMap, filter 等基础的算子

  • 对于键值对类型的数据, 有额外的支持, 诸如 reduceByKey, groupByKey 等 byKey 的算子

  • 同样对于数字型的数据也有额外的支持, 诸如 max, min 等

RDD 对键值对数据的额外支持

键值型数据本质上就是一个二元元组, 键值对类型的 RDD 表示为 RDD[(K, V)]

RDD 对键值对的额外支持是通过隐式支持来完成的, 一个 RDD[(K, V)], 可以被隐式转换为一个 PairRDDFunctions 对象, 从而调用其中的方法.

3b365c28403495cb8d07a2ee5d0a6376

既然对键值对的支持是通过 PairRDDFunctions 提供的, 那么从 PairRDDFunctions 中就可以看到这些支持有什么

类别算子

聚合操作

reduceByKey

foldByKey

combineByKey

分组操作

cogroup

groupByKey

连接操作

join

leftOuterJoin

rightOuterJoin

排序操作

sortBy

sortByKey

Action

countByKey

take

collect

RDD 对数字型数据的额外支持

对于数字型数据的额外支持基本上都是 Action 操作, 而不是转换操作

算子含义

count

个数

mean

均值

sum

求和

max

最大值

min

最小值

variance

方差

sampleVariance

从采样中计算方差

stdev

标准差

sampleStdev

采样的标准差

val rdd = sc.parallelize(Seq(1, 2, 3))
// 结果: 3
println(rdd.max())
七、阶段练习和总结

导读

  1. 通过本节, 希望大家能够理解 RDD 的一般使用步骤

// 1. 创建 SparkContext
// 创建一个 SparkConf 对象,并设置主节点为本地模式,使用 6 个核心,应用名称为 "stage_practice1"
val conf = new SparkConf().setMaster("local[6]").setAppName("stage_practice1")  
// 使用创建的 SparkConf 对象创建 SparkContext
val sc = new SparkContext(conf)  

// 2. 创建 RDD
// 从指定的文件路径读取数据创建一个文本类型的 RDD
val rdd1 = sc.textFile("dataset/BeijingPM20100101_20151231_noheader.csv")  

// 3. 处理 RDD
// 对 rdd1 中的每个元素进行处理,将其按逗号分割,然后构建键值对
val rdd2 = rdd1.map { item =>
  val fields = item.split(",")
  ((fields(1), fields(2)), fields(6))
}
// 过滤出值不为 "NA" 的键值对
val rdd3 = rdd2.filter { item =>!item._2.equalsIgnoreCase("NA") }
// 将值转换为整数类型
val rdd4 = rdd3.map { item => (item._1, item._2.toInt) }
// 按照键进行规约,将相同键的值相加
val rdd5 = rdd4.reduceByKey { (curr, agg) => curr + agg }
// 按照键降序排序
val rdd6 = rdd5.sortByKey(ascending = false)  

// 4. 行动, 得到结果
// 打印出排序后的第一个元素
println(rdd6.first())  

通过上述代码可以看到, 其实 RDD 的整体使用步骤如下

20190518105630

  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值