Spark RDD算子学习笔记
文章学习笔记内容来源:拉勾教育大数据开发高薪训练营。
记录一下Spark RDD算子学习笔记,这里记录一下RDD算子的使用,重点还是学习Spark SQL。Spark SQL比RDD开发更简单,Spark的优化器对SQL语句做了大量的优化,一般情况下实现同样的功能,Spark SQL更容易也更高效。使用的spark2.4.5,scala。
Spark SQL学习笔记[https://blog.csdn.net/qq_38914431/article/details/109741135]
什么是RDD
RDD是spark的基石,是实现spark数据处理核心抽象。RDD是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合,具有如下特征。
- 一个分区的列表
- 一个计算函数compute,对每个分区进行计算
- 对其他RDDs的依赖(宽依赖、窄依赖)列表
- 对key-value RDDs来说,存在一个分区器(Parititoner)[可选的]
- 对每个分区有一个优先位置的列表[可选的]
RDD逻辑上是分区的,每个分区的数据是抽象存在的,在计算的时候会通过一个compute函数得到每个分区的数据。RDD是只读,一旦RDD被创建,数据则不能修改,只能通过一个RDD转换为另外一个RDD。在RDD的转换操作算子有两类
- transformation。用来对RDD进行转化,延迟执行(Lazy)
- action。用来触发RDD的计算;得到相关计算结果或者将RDD保存的文件系统中
RDDs通过操作算子进行转换,转换得到新的RDD包含了从其他RDDs,衍生所必需的信息,RDDs之间维护着这种血缘关系(lineage),也称为依赖,有两种依赖。
- 窄依赖。RDDs之间分区是一一对应的(1:1 或 n:1)
-宽依赖。子RDD每个分区与父RDD的每个分区都有关,是多对多的关系(即n:m)。有shuffle发生
对于多次使用的同一个RDD可以进行将RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。
RDD的血缘关系可以容错,对于计算迭代比较长的,RDDs之间的血缘关系会越来越长,出错后,需要经过非常长的血缘关系重新计算,对性能有影响。RDD支持 checkpoint 将数据保存到持久化的存储中,这样就可以切断之前的血缘关
系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从 checkpoint 处拿到数据。
RDD创建方式
parallelize、makeRDD、range:主要用于测试
val rdd1 = sc.parallelize(Array(1,2,3,4,5))
// 1到100
val rdd2 = sc.parallelize(1 to 100)
// 1到6,2个分区
val rdd2 = sc.parallelize(1 to 6, 2)
val rdd3 = sc.makeRDD(List(1,2,3,4,5))
// 1到100
val rdd4 = sc.makeRDD(1 to 100)
// 1到99,间隔为3
val rdd5 = sc.range(1, 100, 3)
// 1到99,间隔为2, 10个分区
val rdd6 = sc.range(1, 100, 2 ,10)
从文件系统加载数据:主要用于生产
val lines = sc.textFile("file:///root/data/wc.txt")
val lines =sc.textFile("hdfs://linux121:9000/user/root/data/uaction.dat")
getNumPartitions获取RDD分区数,spark rdd完整demo
import org.apache.spark.SparkContext
import org.apache.spark.SparkConf
object TestSpark {
def main(args: Array[String]): Unit = {
// 1.创建sparkcontext
val conf = new SparkConf().setAppName("WordCount")
// 打包时去掉
conf.setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val rdd5 = sc.range(1, 100, 3)
println(rdd5.getNumPartitions)
println(rdd5.collect().toBuffer)
val rdd6 = sc.range(1, 100, 2 ,10)
println(rdd6.collect().toBuffer)
println(rdd6.getNumPartitions)
// 关闭
sc.stop();
}
}
RDD算子
Transformation:用来对RDD进行转换,这个操作时延执行的(是Lazy 的)
Action:用来触发RDD的计算;得到相关计算结果 或者 将结果保存的外部系统中
Transformation:返回一个新的RDD
Action:返回结果int、double、集合(不会返回新的RDD)
宽依赖算子
常见宽依赖的算子(shuffle):**ByKey类型的、groupBy、distinct、repartition、sortBy、intersection、subtract 、cartesian、reduceByKey、countByKey
value类型
map(func)
对数据集中的每个元素都使用func,然后返回一个新的RDD
sc.makeRDD(1 to 10).map(_*2).collect
//结果 Array(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
filter(func)
对数据集中的每个元素都使用func,然后返回一个包含使func为true的元素构成的RDD
sc.makeRDD(1 to 10).filter(_%2==0).collect
// 结果Array(2, 4, 6, 8, 10)
flatMap(func)
与 map 类似,每个输入元素被映射为0或多个输出元素
sc.parallelize(1 to 6).flatMap(1 to _).collect
// 结果Array(1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 1, 2, 3,4, 5, 6)
mapPartitions(func)
和map很像,但是map是将func作用在每个元素上,而mapPartitions是func作用在整个分区上。假设一个RDD有N个元素,M个分区(N>> M),那么map的函数将被调用N次,而mapPartitions中的函数仅被调用M次,一次处理一个分区中的所有元素
map和mapPartition的区别
map:每次处理一条数据
mapRartition:每次处理一个分区的数据,这个分区的数据处理完之后,原 RDD 中分区的数据才能释放,可能导致OOM。
当内存空间较大的时候建议使用 mapPartition() ,以提高处理效率
// 创建2个分区
val rdd = sc.parallelize(1 to 6, 2)
rdd.mapPartitions(items => items.filter(_>=3)).collect
// 结果 Array[Int] = Array(3, 4, 5, 6)
mapPartitionsWithIndex(func)
与 mapPartitions 类似,多了分区的索引值的信息
// 创建2个分区
val rdd = sc.parallelize(1 to 6, 2)
rdd.mapPartitionsWithIndex((index,items) => Iterator(index + ":" +items.toList)).collect
//结果 Array(0:List(1, 2, 3), 1:List(4, 5, 6))
groupBy(func)
按照传入函数的返回值进行分组。将key相同的值放入一个迭代器
val rdd = sc.parallelize(1 to 4)
val group = rdd.groupBy(_%2).collect
// 结果Array((0,CompactBuffer(2, 4)),(1,CompactBuffer(1, 3)))
glom()
将每一个分区形成一个数组,形成新的RDD类型 RDD[Array[T]]
val rdd = sc.parallelize(1 to 16,4)
rdd.glom().collect()
// 结果Array[Array[Int]] = Array(Array(1, 2, 3, 4), Array(5, 6, 7, 8), Array(9,10, 11, 12), Array(13, 14, 15, 16))
sample(withReplacement, fraction, seed)
采样算子。以指定的随机种子(seed)随机抽样出数量为fraction的数据,withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样
有放回时,fraction可以大于1,代表元素被抽到的次数
无放回时,fraction代表元素被抽到的概率(0-1)
val rdd = sc.parallelize(1 to 10)
//放回抽样 Array(1, 2, 2, 7, 7, 8, 9)
rdd.sample(true,0.4,2).collect()
//不放回抽样结果 Array(1, 9)
rdd.sample(false,0.2,3).collect()
distinct([numTasks]))
对RDD元素去重后,返回一个新的RDD。可传入numTasks参数改变RDD分区数
val rdd = sc.parallelize(List(1,2,3,4,4,2,1))
rdd.distinct.collect
// 结果Array(4, 2, 1, 3)
coalesce(numPartitions)
缩减分区数,无shuffle
// 创建分区数为4
val rdd = sc.parallelize(1 to 16,4)
// 进行减少分区
val coalesceRDD = rdd.coalesce(3)
// 分区数 3
coalesceRDD.partitions.size
repartition(numPartitions)
增加或减少分区数,有shuffle
// 创建分区数为4
val rdd = sc.parallelize(1 to 16,4)
// 进行减少分区
val coalesceRDD = rdd.coalesce(3)
// 分区数 3
coalesceRDD.partitions.size
coalesce和repartition的区别
repartition:增大或减少分区数;有shuffle
coalesce:一般用于减少分区数(此时无shuffle)
sortBy(func, [ascending], [numTasks])
使用 func 对数据进行处理,对处理后的结果进行排序,默认为正序
val rdd = sc.parallelize(List(4,2,1,3))
rdd.sortBy(x => x).collect()
// 结果 Array(1, 2, 3, 4)
双Value类型交互
intersection(otherRDD)、union(otherRDD)、subtract (otherRDD)
RDD之间的交、并、差算子
union得到的RDD分区数为:两个RDD分区数之和
val rdd1 = sc.parallelize(Array(1,1,2,3,4,5,4,5))
val rdd2 = sc.parallelize(Array(1,2,1,4))
// 交集 Array(1, 2, 4)
rdd1.intersection(rdd2).collect
// 并集 Array(1, 1, 2, 3, 4, 5, 4, 5, 1, 2, 1, 4)
rdd1.union(rdd2).collect
// 差集Array(3, 5, 5)
rdd1.subtract(rdd2).collect()
cartesian(otherRDD)
笛卡尔积
cartesian得到的RDD分区数为:两个RDD分区数之积。慎用
val rdd1 = sc.parallelize(1 to 3)
val rdd2 = sc.parallelize(2 to 5)
rdd1.cartesian(rdd2).collect()
// 结果Array[(Int, Int)] = Array((1,2), (1,3), (1,4), (1,5), (2,2), (2,3),(2,4), (2,5), (3,2), (3,3), (3,4), (3,5))
zip(otherRDD)
将两个RDD组合成 key-value 形式的RDD,默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常。
val rdd1 = sc.parallelize(Array(1,2,3),3)
val rdd2 = sc.parallelize(Array("a","b","c"),3)
rdd1.zip(rdd2).collect
// 结果Array((1,a), (2,b), (3,c))
Key-Value 类型
类似 map 操作
mapValues
针对于(K,V)形式的类型只对V进行操作
val rdd = sc.parallelize(Array((1,"a"),(1,"d"),(2,"b"),(3,"c")))
//对value添加字符1
rdd3.mapValues(_+"1").collect()
// 结果Array((1,a1), (1,d1), (2,b1), (3,c1))
flatMapValues
flatMapValues 将 value 的值压平
val rdd = sc.parallelize(List((1,2),(3,4),(5,6)))
rdd.flatMapValues(x=>1 to x).collect
// 结果 Array((1,1), (1,2), (3,1), (3,2), (3,3), (3,4), (5,1), (5,2), (5,3), (5,4), (5,5), (5,6))
keys、 values
获取所有的Key 、values
val rdd = sc.parallelize(List((1,2),(3,4),(5,6)))
// Array(1,3,5)
rdd.keys.collect()
// Array(2,4,6)
rdd.values.collect()
聚合操作
groupByKey
groupByKey是对每个key进行操作,根据Key分组,但只生成一个sequence。
val words = Array("one", "two", "two", "three", "three", "three")
val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))
// 将相同key对应值聚合到一个sequence中
wordPairsRDD.groupByKey().collect()
//结果 Array[(String, Iterable[Int])] = Array((two,CompactBuffer(1, 1)),(one,CompactBuffer(1)), (three,CompactBuffer(1, 1, 1)))
reduceByKey
在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起
val rdd = sc.parallelize(List(("a",1),("b",5),("a",5),("b",2)))
// 计算相同key对应值的相加结果
rdd.reduceByKey((x,y) => x+y).collect()
// 结果Array((a,6), (b,7))
aggregateByKey
aggregateByKey => 定义初值 + 分区内的聚合函数 + 分区间的聚合函数
参数: (zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U)=> U)参数描述:
zeroValue: 给每一个分区的每一个key一个初始值。
seqOp: 分区内合并,函数用于在每一个分区中用初始值逐步迭代value
combOp:分区间合并,函数用于合并每个分区中的结果
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)))
// 根据Key求平均值
rdd.mapValues((_, 1)).aggregateByKey((0,0))(
(x, y) => (x._1 + y._1, x._2 + y._2),
(a, b) => (a._1 + b._1, a._2 + b._2)
).mapValues(x=>x._1.toDouble / x._2).collect()
// 结果 Array((spark,18.2), (hadoop,21.666666666666668), (scala,25.0))
foldByKey
(zeroValue: V)(func: (V, V) => V): RDD[(K, V)] aggregateByKey的简化操作、
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)))
// 根据Key求平均值
rdd.mapValues((_, 1)).foldByKey((0, 0))((x, y) => {
(x._1+y._1, x._2+y._2)
}).mapValues(x=>x._1.toDouble/x._2).collect
// 结果 Array((spark,18.2), (hadoop,21.666666666666668), (scala,25.0))
排序操作
sortByKey
sortByKey函数作用于PairRDD,对Key进行排序
val rdd = sc.parallelize(Array((3,"aa"),(6,"cc"),(2,"bb"),(1,"dd")))
//按照key的正序 Array((1,dd), (2,bb), (3,aa), (6,cc))
rdd.sortByKey(true).collect()
// 按照key的倒序 Array((6,cc), (3,aa), (2,bb), (1,dd))
rdd.sortByKey(false).collect()
join操作
cogroup、join、leftOuterJoin、rightOuterJoin 、fullOuterJoin
cogroup:类似全外JOIN
join、leftOuterJoin、rightOuterJoin 、fullOuterJoin调用的还是 cogroup
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.cogroup(rdd2).collect.foreach(println)
//(1,(CompactBuffer(Spark),CompactBuffer()))
//(2,(CompactBuffer(Hadoop),CompactBuffer()))
//(3,(CompactBuffer(Scala),CompactBuffer(20K)))
//(4,(CompactBuffer(Java),CompactBuffer(18K)))
//(5,(CompactBuffer(),CompactBuffer(25K)))
//(6,(CompactBuffer(),CompactBuffer(10K)))
rdd1.join(rdd2).collect
rdd1.leftOuterJoin(rdd2).collect
rdd1.rightOuterJoin(rdd2).collect
rdd1.fullOuterJoin(rdd2).collect
//join
//Array((3,(Kylin,李四)), (4,(Flink,王五)))
//leftOuterJoin:
//Array((1,(Spark,None)), (2,(Hadoop,None)), (3,(Kylin,Some(李四))), (4,(Flink,Some(王五))))
//rightOuterJoin:
//Array((6,(None,冯七)), (3,(Some(Kylin),李四)), (4,(Some(Flink),王五)), (5,(None,赵六)))
//fullOuterJoin:
//Array((6,(None,Some(冯七))), (1,(Some(Spark),None)), (2,(Some(Hadoop),None)), (3,(Some(Kylin),Some(李四))), (4,(Some(Flink),Some(王五))), (5,(None,Some(赵六))))
Action操作
Action 用来触发RDD的计算,得到相关计算结果
Action触发Job。一个Spark程序(Driver程序)包含了多少 Action算子,那么就有多少Job;
典型的Action算子: collect / count
collect() => sc.runJob() => … => dagScheduler.runJob() => 触发了Job
collect
collect 将分布式的 RDD 返回为一个单机的 scala Array 数组,在这个数组上运用 scala 的函数式操作
sc.makeRDD(1 to 10).collect
//结果 Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
collectAsMap
collectAsMap对(K,V)型的RDD数据返回一个单机HashMap。 对于重复K的RDD元素,后面的元素覆盖前面的元素
val rdd = sc.makeRDD(Array(("1","Spark1"),("1","Spark2"),("2","Hadoop"), ("3","Scala")))
rdd.collectAsMap()
// 结果Map(2 -> Hadoop, 1 -> Spark2, 3 -> Scala)
countByKey
针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数
val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)))
//统计每种key的个数
rdd.countByKey
//结果 Map(3 -> 2, 1 -> 3, 2 -> 1)
lookup(key)
lookup(key):高效的查找方法,只查找对应分区的数据(如果RDD有分区器的话)
val rdd = sc.makeRDD(Array(("1","Spark"),("2","Hadoop"),("3","Scala"),("1","Java")))
rdd.lookup("1")
//结果WrappedArray(Spark, Java)
stats 、count 、mean 、stdev 、max 、 min
val rdd = sc.range(1, 10)
rdd.stats
rdd.count
// 平均值
rdd.mean
// 方差
rdd.stdev
rdd.max
rdd.min
//(count: 9, mean: 5.000000, stdev: 2.581989, max: 9.000000, min: 1.000000)
//9
//5.0
//2.5819888974716116
//9
//1
first()、 take(n)、 top(n)
first():返回RDD中的第一个元素,类似于take(1)
take(n):返回一个由RDD的前n个元素组成的数组
top(n):按照默认(降序)或者指定的排序规则,返回前num个元素
val rdd = sc.range(1, 10)
rdd.first()
rdd.take(3).toBuffer
rdd.top(3).toBuffer
// 1
// ArrayBuffer(1, 2, 3)
// ArrayBuffer(9, 8, 7)
takeSample(withReplacement, num, [seed])
参数:是否放回/采集多少个数据/种子。种子固定,采集到的数据不会变
val rdd = sc.range(1, 10)
rdd.takeSample(false,5).toBuffer
//结果ArrayBuffer(4, 6, 5, 8, 3)
foreach(func) 、foreachPartition(func)
与map、mapPartitions类似,区别是map、mapPartitions是Transformation操作,而foreach(func) / foreachPartition(func)是Action操作
saveAsTextFile(path) 、 saveAsSequenceFile(path) 、saveAsObjectFile(path)
保存文件到指定路径(rdd有多少分区,就保存为多少文件,保存文件时注意小文件问题)
可以使用coalesce减少分区数: 减少成一个分区rdd.coalesce(1).saveAsTextFile(“data/t1”)
rdd.coalesce(1).saveAsTextFile("hdfs://linux11:9000/wcinput/part.txt")
案列
import org.apache.spark.{SparkConf, SparkContext}
/**
* <p>假设点击日志文件(click.log)中每行记录格式如下:</p>
*
* INFO 2019-09-01 00:29:53 requestURI:/click?app=1&p=1&adid=18005472&industry=469&adid=31
* INFO 2019-09-01 00:30:31 requestURI:/click?app=2&p=1&adid=18005472&industry=469&adid=31
* INFO 2019-09-01 00:31:03 requestURI:/click?app=1&p=1&adid=18005472&industry=469&adid=32
* INFO 2019-09-01 00:31:51 requestURI:/click?app=1&p=1&adid=18005472&industry=469&adid=33
*
* 另有曝光日志(imp.log)格式如下:
*
* INFO 2019-09-01 00:29:53 requestURI:/imp?app=1&p=1&adid=18005472&industry=469&adid=31
* INFO 2019-09-01 00:29:53 requestURI:/imp?app=1&p=1&adid=18005472&industry=469&adid=31
* INFO 2019-09-01 00:29:53 requestURI:/imp?app=1&p=1&adid=18005472&industry=469&adid=34
*
* <p>3.1、用Spark-Core实现统计每个adid的曝光数与点击数,将结果输出到hdfs文件;</p>
*
* <p>输出文件结构为adid、曝光数、点击数。注意:数据不能有丢失(存在某些adid有imp,没有clk;或有clk没有imp)</p>
*
* <p>3.2、你的代码有多少个shuffle,是否能减少?</p>
*
* <p>(提示:仅有1次shuffle是最优的)</p>
*/
object Part {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Part").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
// 进行数据装载
val clict: Array[String] = Array(
"INFO 2019-09-01 00:29:53 requestURI:/click?app=1&p=1&adid=18005472&industry=469&adid=31",
"INFO 2019-09-01 00:30:31 requestURI:/click?app=2&p=1&adid=18005472&industry=469&adid=31",
"INFO 2019-09-01 00:31:03 requestURI:/click?app=1&p=1&adid=18005472&industry=469&adid=32",
"INFO 2019-09-01 00:31:51 requestURI:/click?app=1&p=1&adid=18005472&industry=469&adid=33"
)
val imp: Array[String] = Array(
"INFO 2019-09-01 00:29:53 requestURI:/imp?app=1&p=1&adid=18005472&industry=469&adid=31",
"INFO 2019-09-01 00:29:53 requestURI:/imp?app=1&p=1&adid=18005472&industry=469&adid=31",
"INFO 2019-09-01 00:29:53 requestURI:/imp?app=1&p=1&adid=18005472&industry=469&adid=34"
)
// =================== 方法一 ===============================
// 标记 点击日志文件(click.log)
val clictRdd1 = sc.makeRDD(clict).map(line =>{
val index = line.lastIndexOf("=")
val key = line.substring(index + 1, line.length)
(key, "clict")
})
// 标记 曝光日志(imp.log)
val impRdd1 = sc.makeRDD(imp).map(line =>{
val index = line.lastIndexOf("=")
val key = line.substring(index + 1, line.length)
(key, "imp")
})
// 合并后进行处理
clictRdd1.union(impRdd1).aggregateByKey((0,0))(
// 分区内 合并
(x1, y1) => {
if("clict".equals(y1)) (x1._1 + 1, x1._2)
else (x1._1, x1._2 + 1)
},
// 分区间合并
(x2, y2) => (x2._1 + y2._1, x2._2 + y2._2)
).map(x => (x._1,x._2._1,x._2._2)).collect().foreach(println)
// ================打印结果===============
// (33,1,0)
// (34,0,1)
// (31,2,2)
// (32,1,0)
// =================== 方法二 ===============================
println("==================================================")
// 标记 点击日志文件(click.log)
val clictRdd2 = sc.makeRDD(clict).map(line =>{
val index = line.lastIndexOf("=")
val key = line.substring(index + 1, line.length)
(key, (1,0))
})
// 标记 曝光日志(imp.log)
val impRdd2 = sc.makeRDD(imp).map(line =>{
val index = line.lastIndexOf("=")
val key = line.substring(index + 1, line.length)
(key, (0, 1))
})
// 合并后进行处理
val resultRdd = clictRdd2.union(impRdd2).reduceByKey((x,y) => (x._1 + y._1, x._2 + y._2))
.map(x => (x._1,x._2._1,x._2._2))
resultRdd.collect().foreach(println)
// ================打印结果===============
// (33,1,0)
// (34,0,1)
// (31,2,2)
// (32,1,0)
// 对结果保存到HDFS
resultRdd.coalesce(1).saveAsTextFile("hdfs://linux111:9000/wcinput/part.txt")
sc.stop()
}
}