看了就会,Spark转换算子的用法(一)

1,356 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文针对大数据中Spark计算引擎的转换算子内容进行讲解

推荐阅读时间:15-20分钟


Value类型算子

map算子

函数签名

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

函数说明

将处理的数据逐条进行映射转换,这里的转换可以是类型的转换,也可以是值的转换。 演示代码

val conf: SparkConf = new SparkConf().setAppName("test").setMaster("local[*]")
val sc = new SparkContext(conf)
val dataRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4))
val dataRDD1: RDD[Int] = dataRDD.map(
    num => {
        num * 2
    }
)
val dataRDD2: RDD[String] = dataRDD1.map(
    num => {
        "" + num
    }
)

println(dataRDD1.collect().mkString("\t"))
println(dataRDD2.collect().mkString("\t"))

第一个map是值得转换,原始list中每个值*2

第二个map是类型的转换,将list中每个值得类型从int类型转换为String类型 运行结果

2	4	6	8
2	4	6	8

mapPartitions算子

函数签名

def mapPartitions[U: ClassTag](
    f: Iterator[T] => Iterator[U],
    preservesPartitioning: Boolean = false): RDD[U]

函数说明

mapPartitions是按分区进行处理数据 map是每次对一条数据进行处理 内存空间较大的时候推荐这样使用 mapPartitions和map的区别: map是对数据一个一个处理,mapPartitions是按照分区处理

演示代码

listRD.mapPartitions(datas=>{
 datas.map(data=>data*2)
})

优点: mapPartitions算子效率优于map,减少了发送到执行器执行的交互(按分区交互,不是每条数据都交互)

缺点: 可能会出现内存溢出(大量数据涌入到一个分区内)

mapPartitionsWithIndex算子

函数签名

def mapPartitionsWithIndex[U: ClassTag](
  f: (Int, Iterator[T]) => Iterator[U],
  preservesPartitioning: Boolean = false): RDD[U]

函数说明

带有了分区号 将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据,在处理时同时可以获取当前分区索引。 演示代码

val res: RDD[(Int, String)] = dataRDD.mapPartitionsWithIndex {
      case (num, datas) => {
        datas.map((_, "分区号:" + num))
      }
    }
println(res.collect().mkString("\n"))

val dataRDD1 = dataRDD.mapPartitionsWithIndex(
    (index, datas) => {
         datas.map(index, _)
    }
)

运行结果

(1,分区号:0)
(2,分区号:0)
(3,分区号:1)
(4,分区号:1)

flatMap算子

函数签名

def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]

函数说明

map:获取一个新元素(原本几个元素还是几个元素)

flatmap :获取一个或者多个新元素(比原来的元素多) flatmap 将处理的数据进行扁平化后再进行映射处理,所以算子也称之为扁平映射 演示代码

val lineArray = Array("hello you","hello me","hello world")
    val lines = sc.parallelize(lineArray, 1)
    val res1: RDD[Array[String]] = lines.map(_.split(" "))
    val res2: RDD[String] = lines.flatMap(_.split(" "))

    println("map:")
    res1.foreach(
      line=> println(line.mkString("\t"))
    )
    println()
    println("flatMap:")
    res2.foreach(
      line=> println(line.mkString("\t"))
    )

运行结果

map:
hello	you
hello	me
hello	world

flatMap:
h	e	l	l	o
y	o	u
h	e	l	l	o
m	e
h	e	l	l	o
w	o	r	l	d

glom算子

函数签名

def glom(): RDD[Array[T]]

函数说明

将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变适用于以分区为单位的数据统计 演示代码

val rdd: RDD[Int] = sc.makeRDD(List(1,5,6,9,8,5,6,2,4,65,2),4)
val arrayrdd: RDD[Array[Int]] = rdd.glom()
arrayrdd.collect().foreach(array=>{
  println(array.max)
})
}

groupBy算子

函数签名

def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]

函数说明

作用:分组,按照传入函数的返回值进行分组,将相同的key对应的值放入一个迭代器中。 演示代码

val rdd: RDD[Int] = sc.makeRDD(List(1,5,6,9,8,5,6,2,4,65,2))
val grouprdd: RDD[(Int, Iterable[Int])] = rdd.groupBy(i => i%2)
grouprdd.foreach(println)

filter算子

函数签名

def filter(f: T => Boolean): RDD[T]

函数说明

将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃。 当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜。

演示代码

val dataRDD = sparkContext.makeRDD(List(
    1,2,3,4
),1)
val dataRDD1 = dataRDD.filter(_%2 == 0)

sample算子

函数签名

def sample(
  withReplacement: Boolean,
  fraction: Double,
  seed: Long = Utils.random.nextLong): RDD[T]

函数说明

根据指定的规则从数据集中抽取数据 代码演示

val dataRDD = sparkContext.makeRDD(List(
    1,2,3,4
),1)

val dataRDD1 = dataRDD.sample(false, 0.5)
val dataRDD2 = dataRDD.sample(true, 2)

扩展:

抽取数据不放回(伯努利算法)

伯努利算法:又叫0、1分布。例如扔硬币,要么正面,要么反面。

具体实现:根据种子和随机算法算出一个数和第二个参数设置几率比较,小于第二个参数要,大于不要

第一个参数:抽取的数据是否放回,false:不放回

第二个参数:抽取的几率,范围在[0,1]之间,0:全不取;1:全取;

第三个参数:随机数种子

抽取数据放回(泊松算法)

第一个参数:抽取的数据是否放回,true:放回;false:不放回

第二个参数:重复数据的几率,范围大于等于0.表示每一个元素被期望抽取到的次数

第三个参数:随机数种子

distinct算子

函数签名

def distinct()(implicit ord: Ordering[T] = null): RDD[T]
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

函数说明

对数据进行去重复 直接在需要去重的RDD使用该方法

val lineArray = Array("hello you","hello me","hello world","hello me")
val lines = sc.parallelize(lineArray, 1)
val value: RDD[String] = lines.distinct()

coalesce和repartition算子

函数签名

def coalesce(numPartitions: Int, shuffle: Boolean = false,
           partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
          (implicit ord: Ordering[T] = null)
  : RDD[T]
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

函数说明

repartition实际上调用的就是coalesce,并且默认是进行shuffle的,coalesce可以选择是否进行shuffle过程

拓展:

1)如果N<M。

一般情况下N个分区有数据分布不均匀的状况,利用HashPartitioner函数将数据重新分区为M个,这时需要将shuffle设置为true(repartition实现,coalesce也实现不了)。

2)如果N>M并且N和M相差不多

(假如N是1000,M是100)那么就可以将N个分区中的若干个分区合并成一个新的分区,最终合并为M个分区,这时可以将shuff设置为false(coalesce实现),如果M>N时,coalesce是无效的,不进行shuffle过程,父RDD和子RDD之间是窄依赖关系,无法使文件数(partiton)变多。 总之如果shuffle为false时,如果传入的参数大于现有的分区数目,RDD的分区数不变,也就是说不经过shuffle,是无法将RDD的分区数变多的

3)如果N>M并且两者相差悬殊

这时你要看executor数与要生成的partition关系,如果executor数 <= 要生成partition数,coalesce效率高,反之如果用coalesce会导致(executor数-要生成partiton数)个excutor空跑从而降低效率。如果在M为1的时候,为了使coalesce之前的操作有更好的并行度,可以将shuffle设置为true。

sortby算子

函数签名

def sortBy[K](
  f: (T) => K,
  ascending: Boolean = true,
  numPartitions: Int = this.partitions.length)
  (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]

函数说明

该操作用于排序数据。在排序之前,可以将数据通过f函数进行处理,之后按照f函数处理的结果进行排序,默认为升序排列。排序后新产生的RDD的分区数与原RDD的分区数一致。如果想要降序排列可以有第二个参数,设置为false即可 演示代码

val dataRDD = sparkContext.makeRDD(List(
    1,2,3,4,1,2
),2)
 
val dataRDD1 = dataRDD.sortBy(num=>num, false, 4)

PartitionBy算子

函数签名

def partitionBy(partitioner: Partitioner): RDD[(K, V)]

函数说明

分区器 将数据按照指定Partitioner重新进行分区。Spark默认的分区器是HashPartitioner

HashPartitioner分区规则就是将当前的key进行取余操作,是默认的分区器

RangePartitioner范围分区器,要求数据是可以比较大小的可以排序的,适用性低,

自定义分区器 演示代码

val listedd: RDD[(String, String)] = sc.makeRDD(
    List(
      ("cba", "消息1"), ("nba", "消息2"), ("nba", "消息3"),
      ("cba", "消息4"), ("cba", "消息5"), ("wnba", "消息6")
    ),
    1
  )
  val rdd1: RDD[(String, String)] = listedd.partitionBy(new MyPartition(2))
  val rdd2: RDD[(Int, (String, String))] = rdd1.mapPartitionsWithIndex(
    (index, datas) =>
      datas.map(
        data => (index, data)
      )
  )
  println(rdd2.collect().foreach(println))
  sc.stop()
}
//继承Partitioner
class MyPartition(num: Int) extends Partitioner {
  //获取分区的数量
  override def numPartitions: Int = num
//根据数据的key来决定数据在哪个分区中进行处理
//方法的返回值表示分区编号(索引)
  override def getPartition(key: Any): Int = {
    key match {
      case "nba" => 0
      case _ => 1
    }
  }
}

注意: 如果重分区的分区器和当前的RDD的分区器一样,则不进行任何的处理,不会再次重分区


本文到此结束,对于本文知识,后续还有一个专题系列,感兴趣的小伙伴请继续关注!