一、算子介绍(RDD方法既称为算子)
RDD方法分为转换和行动两个内容。
转换:功能的补充,也就是复杂业务逻辑,生产多个RDD的过程,旧的RDD包装成新的RDD,相互包装补充;
行动:触发任务调度和作业的执行。封装并不会触发任务的执行,如flatmap,map等,只有collect等才能触发。
二、转换算子(RDD方法)
根据数据类型不同可以把算法分为三大类型单value ,双value,key-value;
2.1 map
map函数式将处理的数据逐条进行映射转换,可以是值或者类型的转换。
scala集合中使用最多的转换映射方法
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo map算子,映射转换
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
//匿名函数格式:() => {} 如(num: Int) => { num * 2 }
// val makeRDD = rdd.map((num: Int) => {num * 2})
//根据简化原则 将代码优化
//一行计算可省略{}
// val makeRDD = rdd.map((num: Int) => num * 2)
//参数num可以自动推断出来类型,可省略
// val makeRDD = rdd.map((num) => num * 2)
//参数只有一个,可省略()
// val makeRDD = rdd.map(num => num * 2)
//参数只在计算逻辑num * 2出现一次,并且是按顺序出现可省略 参数,用_代替
val makeRDD = rdd.map(_* 2)
makeRDD.collect().foreach(println)
sc.stop()
}
}
在data文件夹下创建一个文件log.log,内容为
127.0.0.0 - - 2021/06/23:23:00:00 /log/information
127.0.0.0 - - 2021/06/23:09:00:00 /log/car
代码如下:
package com.byxrs.spark_core.operator.transform
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTrans_test {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo map算子,映射转换
val rdd = sc.textFile("data/log.log")
val mapRDD = rdd.map(line => {
val datas = line.split(" ")
datas(4)
})
mapRDD.collect().foreach(println)
sc.stop()
}
}
运行结果:
/log/information
/log/car
2.2 map的并行计算
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTrans_Par {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo map算子,映射转换
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
val mapRDD = rdd.map(num => {
println("====>" + num)
num
})
val mapRDD1 = mapRDD.map(num => {
println("++++++++>" + num)
num
})
mapRDD1.collect()
sc.stop()
}
}
运行结果,无序:
====>4
====>1
====>2
====>3
++++++++>2
++++++++>1
++++++++>4
++++++++>3
设置分区数量为1时
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4),1)
运行结果:
====>1
++++++++>1
====>2
++++++++>2
====>3
++++++++>3
====>4
++++++++>4
结论分析:
1.RDD的计算,在一个分区内是逐个进行计算;
2.1个分区内的数据执行是有顺序的;
设置分区数量为2时
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4),2)
运行结果:
====>1
====>3
++++++++>1
++++++++>3
====>2
====>4
++++++++>2
++++++++>4
结论分析:
1.不同分区的数据计算时无序的
2.3 mapPartitions(spark类似缓冲区的算子)
def mapPartitions[U: ClassTag](
f: Iterator[T] => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U] = withScope {
val cleanedF = sc.clean(f)
new MapPartitionsRDD(
this,
(context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(iter),
preservesPartitioning)
}
解析:
Iterator[T] => Iterator[U] 表示返回的也是个迭代器
如何转为迭代器?
List(2).iterator
以上效果demo,得知map算子是一个个逻辑RDD执行,并不是批量执行,这效率性能不会很高;
mapPartitions是将一个分区的数据全部拿到再操作;
mapPartitions但是有个问题,当数据处理完毕后是不会被释放的,因为iter作为一个集合,拿去引用目标数据,被拿去处理,当数据没有被处理完毕时,引用是一直存在的,引用存在即意味着数据没有被释放,内存还是在占用着,所以选择哪种方法需要看资源大小。内存较小,数据较大,使用mapPartitions可能会内存溢出,可以使用map(没有对象引用,来一条处理一条);
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform01 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo mapPartitions算子,映射转换
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
val mapParRDD = rdd.mapPartitions(iter => {
println("@@@@@@@@@@")
iter.map(_ * 2)
})
mapParRDD.collect().foreach(println)
sc.stop()
}
}
结果:
@@@@@@@@@@
@@@@@@@@@@
2
4
6
8
分析:
1.说明两个分区,走两次;
2. iter.map(_ * 2)是在内存中进行操作,性能比较高
map类似串行操作,一个个执行,mapPartitions则以分区为单位进行批量处理;
map算子操作后,数据总量不会改变,mapPartitions需要传递迭代器,返回一个迭代器,数据总量可变化;
map性能较低,mapPartitions性能较高,但是会占用内存资源,资源有限不推荐使用;
2.4 分区索引方法
def mapPartitionsWithIndex[U: ClassTag](
f: (Int, Iterator[T]) => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U] = withScope {
val cleanedF = sc.clean(f)
new MapPartitionsRDD(
this,
(context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(index, iter),
preservesPartitioning)
}
以分区为单位将数据给计算节点进行逻辑计算,可以获取当前分区索引。
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform03 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo mapPartitions算子,映射转换
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
val mapPWRDD = rdd.mapPartitionsWithIndex((index, iter) => {
if (index == 1) {
iter
} else {
Nil.iterator
}
})
mapPWRDD.collect().foreach(println)
sc.stop()
}
}
运行结果:
3
4
采用默认分区的时候怎么知道是在哪个分区?
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform02 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo mapPartitions算子,映射转换
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
val mapPWRDD = rdd.mapPartitionsWithIndex((index, iter) => {
//iter.map()会把每个num取到,再进行操作
iter.map(num => {
(index, num)
})
})
mapPWRDD.collect().foreach(println)
sc.stop()
}
}
运行结果:
(4,1)
(9,2)
(14,3)
(19,4)
2.5 flatMap
扁平化映射算子,将整体拆分成个体;
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform_flatmap {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo mapPartitions算子,映射转换
val rdd :RDD[List[Int]]= sc.makeRDD(List(List(1, 2), List(3, 4)))
//第一个list 是元素List(1, 2)和List(3, 4)
//第二个list 是flatMap后重新将1,2,3,4封装成为一个新的list
val flatMapRDD: RDD[Int] = rdd.flatMap(list => {
//first list:List(1, 2)
//first list:List(3, 4)
println("first list:"+list)
list
})
flatMapRDD.collect().foreach(println)
sc.stop()
}
}
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform_flatmap01 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo mapPartitions算子,映射转换
val rdd :RDD[String]= sc.makeRDD(List("hello world","hello spark"))
val flatMapRDD: RDD[String] = rdd.flatMap(s => {
s.split(" ")
})
flatMapRDD.collect().foreach(println)
sc.stop()
}
}
运行结果:
hello
world
hello
spark
分析以上两个demo,匿名函数里面的参数会自己推断类型,但是书写参数名建议能看明白,flatMap返回的是一个可迭代的集合;
package com.byxrs.spark_core.operator.transform
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform_flatmap02 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
val rdd = sc.makeRDD(List(List(1, 2), 5, List(3, 4)))
val flatMapRDD = rdd.flatMap(
data => {
data match {
case list: List[_] => list
case dat => List(dat)
}
})
flatMapRDD.collect().foreach(println)
sc.stop()
}
}
运行结果:
1
2
5
3
4
2.6 glom
将分区内的数据转换为数组进行处理,分区不变
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform_glom {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo glom算子
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5), 2)
val glomRDD: RDD[Array[Int]] = rdd.glom()
glomRDD.collect().foreach(data =>{
println(data.mkString(","))
})
sc.stop()
}
}
运行结果:
1,2
3,4,5
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform_glom01 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo glom算子 将数据转为数组进行操作
//需求:将两个分区内的最大值相加求和
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
val rddglom: RDD[Array[Int]] = rdd.glom()
val rddMax: RDD[Int] = rddglom.map(array => {
array.max
})
println(rddMax.collect().sum)
sc.stop()
}
}
结果为:6
2.7 分区不变(数量)的概念
map,flatmap,glom等算子执行后,数据转换后分区是不变的。
2.8 groupby
package com.byxrs.spark_core.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object SparkRDDTransform_groupby {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo groupBy算子
//根据数据源的每一个数据进行分组判断,根据返回的分组key进行分组,相同的key会放在一组
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
val groupByRDD: RDD[(Int, Iterable[Int])] = rdd.groupBy(_ % 2)
groupByRDD.collect().foreach(println)
sc.stop()
}
}
运行结果:
(0,CompactBuffer(2, 4))
(1,CompactBuffer(1, 3))
注意区分概念:分组和分区是没有必然的关系的。因为一个组的数据可以放在一个分区中,但是一个分区不一定只有一个组。
shuffle概念引入:groupby会根据指定规则将数据打乱打散,重新组合,但是分区数量默认不变,这个操作称为shuffle,极端情况是所有数据分在同一个区内。
小案例
package com.byxrs.spark_core.operator.transform
import java.text.SimpleDateFormat
import java.util.Date
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* 需求:统计不同时间段的访问点击数量
*/
object SparkRDDTransform_groupby_test {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
//todo groupBy算子
val rdd: RDD[String] = sc.textFile("data/log.log")
//Iterable迭代器
val rddGroupBy: RDD[(String, Iterable[(String, Int)])] = rdd.map(
line => {
val datas: Array[String] = line.split(" ")
val time: String = datas(3)
val sdf = new SimpleDateFormat("yyyy/MM/dd:HH:mm:ss")
//public Date parse(String source) throws ParseException
val date: Date = sdf.parse(time)
val sdf1 = new SimpleDateFormat("HH")
//public final String format(Date date)
val hour: String = sdf1.format(date)
(hour, 1)
}).groupBy(_._1) //groupBy(_._1)根据(hour, 1)第一个元素分组统计,既 hour
//模式匹配
rddGroupBy.map {
case (hour, iter) => {
(hour, iter.size)
}
}.collect().foreach(println)
sc.stop()
}
}
运行结果:
(09,1)
(23,1)