0. RDD 的算子

目标

  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 持久化到磁盘或者内存中. 这个时候为了下次可以更快的访问, 会把数据保存到集群上.

1. Transformations 算子

Transformation function

解释

map(T ⇒ U)

sc.parallelize(Seq(1, 2, 3))
  .map( num => num * 10 )
  .collect()

spark系列6:常用RDD介绍与演示_spark 算子

spark系列6:常用RDD介绍与演示_大数据_02

作用

  • 把 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])

sc.parallelize(Seq("Hello lily", "Hello lucy", "Hello tim"))
  .flatMap( line => line.split(" ") )
  .collect()

spark系列6:常用RDD介绍与演示_数据_03

spark系列6:常用RDD介绍与演示_spark_04

作用

  • 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)

sc.parallelize(Seq(1, 2, 3))
  .filter( value => value >= 3 )
  .collect()

spark系列6:常用RDD介绍与演示_rdd_05

spark系列6:常用RDD介绍与演示_大数据_06

作用

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

  • scala> sc.parallelize(Seq("Hello lily", "Hello lucy", "Hello tim")).flatMap(_.split(" ")).filter(x=>x.length >4).collect
    res8: Array[String] = Array(Hello, Hello, Hello)  

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

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

mapPartitionsWithIndex

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

mapValues

sc.parallelize(Seq(("a", 1), ("b", 2), ("c", 3)))
  .mapValues( value => value * 10 )
  .collect()

spark系列6:常用RDD介绍与演示_大数据_07

spark系列6:常用RDD介绍与演示_rdd_08

作用

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

sample(withReplacement, fraction, seed)

sc.parallelize(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
  .sample(withReplacement = true, 0.6, 2)
  .collect()

spark系列6:常用RDD介绍与演示_spark_09

spark系列6:常用RDD介绍与演示_大数据_10

作用

  • 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()

spark系列6:常用RDD介绍与演示_数据_11

spark系列6:常用RDD介绍与演示_数据_12

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()

spark系列6:常用RDD介绍与演示_spark 算子_13

spark系列6:常用RDD介绍与演示_数据_14

作用

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

subtract(other, numPartitions)

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

distinct(numPartitions)

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

spark系列6:常用RDD介绍与演示_spark 算子_15

spark系列6:常用RDD介绍与演示_spark 算子_16

作用

  • 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()

spark系列6:常用RDD介绍与演示_spark_17

spark系列6:常用RDD介绍与演示_spark_18

作用

  • 首先按照 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()

spark系列6:常用RDD介绍与演示_spark 算子_19

spark系列6:常用RDD介绍与演示_大数据_20

 

scala> sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1))).groupByKey().collect
res33: Array[(String, Iterable[Int])] = Array((a,CompactBuffer(1, 1)), (b,CompactBuffer(1)))

scala> sc.parallelize(Seq(("a", 1), ("a", 1), ("b", 1))).groupByKey().mapValues(_.reduce(_+_)).collect
res34: Array[(String, Int)] = Array((a,2), (b,1))

作用

  • 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()

spark系列6:常用RDD介绍与演示_spark_21

 

 

作用

  • 对数据集按照 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)

spark系列6:常用RDD介绍与演示_数据_22

作用

  • 聚合所有 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()

spark系列6:常用RDD介绍与演示_rdd_23

spark系列6:常用RDD介绍与演示_大数据_24

作用

  • 和 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()

spark系列6:常用RDD介绍与演示_大数据_25

作用

  • 将两个 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)

spark系列6:常用RDD介绍与演示_大数据_26

作用

  • 多个 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 中排序, 比先重分区再排序要效率高, 建议使用在需要分区后再排序的场景使用

2. 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)

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

作用

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

调用

  • 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