RDD
编程
RDD
是什么
弹性分布式数据集RDD
是Spark
中不可变的分布式对象集合,每个RDD
被分为多个分区,分区运行在集群不同节点上。我们可以通过Java、Scala、Python
语言操作RDD
,进行数据的处理。
RDD
操作类型
- 转化操作
(transformation)
转化操作指将一个RDD
转换成另一个RDD
,就像我们将List
转换成Map
一样。
- 行动操作
(action)
行动操作指将RDD
计算出一个结果,就像我们给List<Integer>
求和一样。
RDD
特性
- 谱系图
RDD
的谱系图会记录RDD
的转化操作的记录,记录不同RDD
间的依赖关系,这样在RDD
部分数据丢失后,可以通过谱系图重新计算出丢失的RDD
数据,保证了分区情况下数据的完整性。
下图所示:原RDD
经过过滤生成两个新的RDD
,两个新的RDD
并集生成新的RDD
,如果badRDD
丢失,可以通过重新取errorsRDD
与warningRDD
的并集生成badRDD
。
graph TD
A[sourceRDD] -->|filter| B[errorsRDD]
A -->|filter| C[warningRDD]
B -->|union| D[badRDD]
C -->|union| D
- 宽依赖与窄依赖
1、窄依赖:一个父RDD
的分区对应于一个子RDD
的分区或者多个父RDD
的分区对应于一个子RDD
的分区。
2、宽依赖:存在一个父RDD
的一个分区对应一个子RDD
的多个分区。
窄依赖是不存在数据混洗(shuffle)
的场景的,宽依赖会产生数据混洗(shuffle)
- 惰性求值
对RDD
的转化操作都是惰性的,只有在行动操作被调用时Spark
才开始计算。这样的优势是Spark
可以将一些操作合并到一起减少计算步骤。
RDD
常用操作
- 代码
demo
def method(): Unit = {
//初始化SparkContext
val conf = new SparkConf().setAppName("demo").setMaster("local")
val sc = new SparkContext(conf)
//使用map()对rdd中数字求平方 1,4,9,16
val list = sc.parallelize(List(1,2,3,4))
list.persist()
val result = list.map(x => x*x)
println(result.collect().mkString(","))
//使用flatMap()将rdd元素切分为多个元素 Hello,World,Scala
val txt = sc.parallelize(List("Hello World","Scala"))
val flatTxt = txt.flatMap(x => x.split(" "))
println(flatTxt.collect().mkString(","))
//使用filter()对rdd进行过滤 3,4
println(list.filter(x => x>2).collect().mkString(","))
//使用distinct()函数进行元素去重 B,A,C
val total = sc.parallelize(List("A","B","C","A"))
println(total.distinct().collect().mkString(","))
//使用sample() 对rdd进行取样 返回指定部分的元素 结果是不确定的
println(list.sample(false,0.4).collect().mkString(","))
//使用union()将rdd进行合并
val a = sc.parallelize(List("A","B"))
val b = sc.parallelize(List("B","C"))
a.persist()
b.persist()
println(a.union(b).collect().mkString(",")) // A,B,B,C
//使用intersection()求rdd的并集 B
println(a.intersection(b).collect().mkString(","))
//使用subtract() 返回只存在于第一个rdd不存在与第二个rdd的结果 A
println(a.subtract(b).collect().mkString(","))
//使用reduce()对rdd计算结果 10
println(list.reduce((x,y) => x+y))
//使用fold()对rdd进行有初始值的计算 每个分区调用时赋一次初始值 10
println(list.fold(0)((x,y) => x+y))
/*
aggregate()求rdd的平均值
第一个括号中是赋予初始值
第二个括号是具体的逻辑 value表示每次拿到的值 acc表示(0,0)
第三个表达式代表 将每个数字修改格式后的结果 (1,1),(2,1),(3,1),(3,1) 然后每个都相加
最终结果为 avg : (9,4)
*/
val nums = sc.parallelize(List(1,2,3,3))
nums.persist()
val avg = nums.aggregate((0,0))((acc, value) => (acc._1 + value, acc._2+1),
((acc1,acc2) => (acc1._1+acc2._1, acc2._2+acc2._2)))
println(avg._1/avg._2.toDouble)
//使用count()返回元素的个数 4
println(nums.count())
//使用countByValue()查看每个元素出现的次数 Map(1 -> 1, 3 -> 2, 2 -> 1)
println(nums.countByValue())
//使用take()从rdd中返回指定个数的元素 1,2
println(nums.take(2).mkString(","))
//使用top()从rdd中返回最前面的几个元素 3,3
println(nums.top(2).mkString(","))
/*
使用takeOrdered()按照自定义的顺序返回结果
class MyOrder extends Ordering[Int] {
override def compare(a: Int, b: Int): Int = {
return -(a-b);
}
}
// 3,3
nums.takeOrdered(2)(new MyOrder).mkString(",")
*/
//使用foreach()遍历元素
nums.foreach(println)
nums.unpersist()
a.unpersist()
b.unpersist()
list.unpersist()
//pair RDD
val init = sc.parallelize(List("1 2","3 4","3 6"))
val kv = init.map(x => (x.split(" ")(0).toInt, x.split(" ")(1).toInt))
kv.persist()
//(1,2),(3,10) 合并具有相同key的值
println(kv.reduceByKey((x, y) => x+y).collect().mkString(","))
//(1,CompactBuffer(2)),(3,CompactBuffer(4, 6)) 相同key的值分组
println(kv.groupByKey().collect().mkString(","))
//(1,3),(3,5),(3,7) 对每个键值对的value进行操作
println(kv.mapValues(x => x+1).collect().mkString(","))
//(1,2),(1,3),(1,4),(1,5),(3,4),(3,5) 对每个键值对的value值比较生成新的键值对
println(kv.flatMapValues(x => (x to 5)).collect().mkString(","))
//1,3,3 获取全部的key
println(kv.keys.collect().mkString(","))
//2,4,6 获取全部的value
println(kv.values.collect().mkString(","))
//(1,2),(3,4),(3,6) 按照key值排序
println(kv.sortByKey().collect().mkString(","))
val initOther = sc.parallelize(List("3 9"))
val other = initOther.map(x => (x.split(" ")(0).toInt, x.split(" ")(1).toInt))
other.persist()
//(1,2) 删除与other中相同key值的元素
println(kv.subtractByKey(other).collect().mkString(","))
//内连接 (3,(4,9)),(3,(6,9))
println(kv.join(other).collect().mkString(","))
//右外连接 (3,(Some(4),9)),(3,(Some(6),9))
println(kv.rightOuterJoin(other).collect().mkString(","))
//左外连接 (1,(2,None)),(3,(4,Some(9))),(3,(6,Some(9)))
println(kv.leftOuterJoin(other).collect().mkString(","))
//先将每个RDD中的相同key进行合并成list 然后再将两个RDD合并 没有的key值存空list (1,(CompactBuffer(2),CompactBuffer())),(3,(CompactBuffer(4, 6),CompactBuffer(9)))
println(kv.cogroup(other).collect().mkString(","))
//对每个key对应元素分别计数
println(kv.countByKey()) //Map(1 -> 1, 3 -> 2)
//将rdd的键值对转换为map
println(kv.collectAsMap()) //Map(1 -> 2, 3 -> 6)
//获取给定key值对应的所有值
println(kv.lookup(3).mkString(",")) //4,6
kv.unpersist()
other.unpersist()
}
combineByKey
//计算 每种新闻类型平均阅读数 每条数据 key:新闻类型 value:阅读数
scala> news.collect()
res41: Array[(String, Int)] = Array((toutiao,10), (zhihu,9), (tengxun,8), (xinlang,9), (toutiao,4), (xinlang,12), (zhihu,6), (tengxun,14))
scala> news.mapValues(x => (x,1)).reduceByKey((x,y) => (x._1 + y._1,x._2+y._2)).map{case(name, (read,times)) => (name,read/times)}.collect()
res42: Array[(String, Int)] = Array((zhihu,7), (tengxun,11), (toutiao,7), (xinlang,10))
//如上的操做可以通过 combineKey的api代替实现
news.combineByKey((v) => (v,1) , //新元素 createCombine
(acc: (Int, Int), v) => (acc._1+v, acc._2+1) ,//存在的元素mergeCombine
(acc1: (Int, Int), acc2: (Int, Int)) =>
(acc1._1+acc2._1, acc1._2+acc2._2)) //多个分片合并
.map{case(key, value) => (key, value._1/value._2.toFloat)}.collect()//计算平均数
res8: Array[(String, Float)] = Array((zhihu,7.5), (tengxun,11.0), (toutiao,7.0), (xinlang,10.5))
RDD
持久化
- 当我们多次使用同一个
RDD
,可以将RDD
进行持久化,默认情况下缓存在JVM
堆内存中
级别 | 使用空间 |
| 是否在内存 | 是否在磁盘 |
| 高 | 低 | 是 | 否 |
| 低 | 高 | 是 | 否 |
| 高 | 中 | 部分 | 部分 |
| 低 | 高 | 部分 | 部分 |
| 低 | 高 | 否 | 是 |
-
MEMORY_AND_DISK
:内存数据放不下、写到磁盘。 -
MEMORY_AND_DISK_SER
:内存数据放不下,写到磁盘,内存数据序列化。
RDD
数据分区
- 降低节点通讯成本
在分布式程序中、不同分区节点间的通讯代价是巨大的。当我们多次对同一个RDD
进行数据处理时,需要对RDD
进行预分区处理,保证处理RDD
数据时,尽量避免跨节点数据混洗。对于处理两个pair RDD
的场景时,根据key
值进行分区,会使得网络通讯大大减少。
- 举例:
A.join(B)
与A.partitionBy(new Partitioner()).join(B)
默认情况下、连接操作会将两个数据集所有键的哈希值求出来,将哈希值相同的记录通过网络传到同一台机器上,然后计算,最后进行结果汇总。这会对两个RDD
都产生数据混洗的结果。效率很低。
如果先将A
进行数据分区,则只会对B
进行哈希求值,然后把key
值对应的数据传送到A
预先分区好的位置。大大的减少了网络通讯代价,提高了效率。
- 自定义分区算法与预处理
RDD
分区
//自定义的分区算法
class MyPartitioner() extends Partitioner {
//获取分区数方法
override def numPartitions: Int = 2
//根据key值获取当前数据分区位置
override def getPartition(key: Any): Int = {
val location = key.toString.toInt % 2
if(location >= 0) {
return location
} else {
return -location
}
}
//判断当前分区算法是否是自定义的分区算法
override def equals(obj: Any): Boolean = {
obj match {
case mp : MyPartitioner =>
return true
case _ =>
return false
}
}
}
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("SparkDemo")
val sc = new SparkContext(conf)
//创建带有自定义分区的RDD
val rdd = sc.parallelize(List("1 world","2 spark","3 sharding","4 watermelon","5 pineapple")).map(x => (x.split(" ")(0), x.split(" ")(1)))
.partitionBy(new MyPartitioner()).persist()
println("partitioner: "+rdd.partitioner + " size: "+rdd.partitioner.size)
rdd.unpersist()
sc.stop()
}