SparkCore

RDD概述

1.1 什么是RDD

RDD(Resilient Distributed Dataset)叫做分布式数据集,是Spark中最基本的数据抽象。代码中是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合。

1.2 RDD的属性

在这里插入图片描述

  1. 一组分区(Partition),即数据集的基本组成单位;
  2. 一个计算每个分区的函数;
  3. RDD之间的依赖关系;
  4. 一个Partitioner,即RDD的分片函数;
  5. 一个列表,存储存取每个Partition的优先位置(preferred location)。

1.3 RDD特点

RDD表示只读的分区的数据集,对RDD进行改动,只能通过RDD的转换操作,由一个RDD得到一个新的RDD,新的RDD包含了从其他RDD衍生所必需的信息。RDDs之间存在依赖,RDD的执行是按照血缘关系延时计算的。如果血缘关系较长,可以通过持久化RDD来切断血缘关系。

1.3.1 分区

RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。
在这里插入图片描述

1.3.2 只读

如下图所示,RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD。
在这里插入图片描述
由一个RDD转换到另一个RDD,可以通过丰富的操作算子实现,不再像MapReduce那样只能写map和reduce了,如下图所示。
在这里插入图片描述
RDD的操作算子包括两类,一类叫做transformations,它是用来将RDD进行转化,构建RDD的血缘关系;另一类叫做actions,它是用来触发RDD的计算,得到RDD的相关计算结果或者将RDD保存的文件系统中。下图是RDD所支持的操作算子列表。

1.3.3 依赖

RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着这种血缘关系,也称之为依赖。如下图所示,依赖包括两种,一种是窄依赖,RDDs之间分区是一一对应的,另一种是宽依赖,下游RDD的每个分区与上游RDD(也称之为父RDD)的每个分区都有关,是多对多的关系。
在这里插入图片描述

1.3.4 缓存

如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。如下图所示,RDD-1经过一系列的转换后得到RDD-n并保存到hdfs,RDD-1在这一过程中会有个中间结果,如果将其缓存到内存,那么在随后的RDD-1转换到RDD-m这一过程中,就不会计算其之前的RDD-0了。
在这里插入图片描述

1.3.5 CheckPoint

虽然RDD的血缘关系天然地可以实现容错,当RDD的某个分区数据失败或丢失,可以通过血缘关系重建。但是对于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。为此,RDD支持checkpoint将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从checkpoint处拿到数据。

二、RDD编程

2.1 编程模型

在Spark中,RDD被表示为对象,通过对象上的方法调用来对RDD进行转换。经过一系列的transformations定义RDD之后,就可以调用actions触发RDD的计算,action可以是向应用程序返回结果(count, collect等),或者是向存储系统保存数据(saveAsTextFile等)。在Spark中,只有遇到action,才会执行RDD的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换。
要使用Spark,开发者需要编写一个Driver程序,它被提交到集群以调度运行Worker,如下图所示。Driver中定义了一个或多个RDD,并调用RDD上的action,Worker则执行RDD分区计算任务。
在这里插入图片描述
在这里插入图片描述

2.2 RDD的创建

在Spark中创建RDD的创建方式可以分为三种:从集合中创建RDD;从外部存储创建RDD;从其他RDD创建。

import org.apache.spark.{
    
    SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

object SparkOperation {
    
    

    def main(args: Array[String]): Unit = {
    
    

        //初始化配置
        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")

        //创建SparkContext对象
        val sc: SparkContext = SparkContext.getOrCreate(conf)

        //从内存中创建RDD,可以通过第二个参数指定RDD的分区数,如果不指定则内核数和2之间最大值为分区数
        val rdd: RDD[Int] = sc.makeRDD(Array(1,2,3,4,5,6,7,8),3)
        val rdd2: RDD[Int] = sc.parallelize(Array(1,2,3,4,5,6,7,8),2)

        //从外部存储文件中创建RDD,可以通过第二个参数指定RDD的分区数,如果不指定则内核数和2之间最小值为分区数,但最终分区数收到hdfs底层切片规则影响
        val rdd3: RDD[String] = sc.textFile("in/student.txt",3)

        //打印
        rdd.collect().foreach(println)
        rdd2.collect().foreach(println)
        rdd3.collect().foreach(println)

        sc.stop()
    }
}

2.3 RDD的转换

RDD整体上分为Value类型和Key-Value类型

扫描二维码关注公众号,回复: 12468757 查看本文章

2.3.1 Value类型

2.3.1.1 mapxxx

import org.apache.spark.{
    
    SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

object SparkOperation {
    
    

    def main(args: Array[String]): Unit = {
    
    

        //初始化配置
        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")

        //创建SparkContext对象
        val sc: SparkContext = SparkContext.getOrCreate(conf)

        //从内存中创建RDD,可以通过第二个参数指定RDD的分区数,如果不指定则内核数和2之间最大值为分区数
        val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5, 6, 7, 8), 3)

        // 1.返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成
        val rdd2: RDD[Int] = rdd.map(_ * 2)
        rdd2.collect().foreach(println)

        /**
         * 2.mapPartitions类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,
         * func的函数类型必须是Iterator[T] => Iterator[U]。
         * 假设有N个元素,有M个分区,那么map的函数的将被调用N次,
         * 而mapPartitions被调用M次,一个函数一次处理所有分区。
         */
        rdd.mapPartitions(x => x.map(_ * 2)).collect().foreach(println)

        /**
         *  3.mapPartitionsWithIndex(func)类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,
         * func的函数类型必须是(Int, Iterator[T]) => Iterator[U];
         */
        val rdd3: RDD[(String, Int)] = rdd.mapPartitionsWithIndex((index, x) => x.map(("所在分区:" + index, _)))
        rdd3.collect().foreach(println)

        /**
         * 4.flatMap(func)类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素)
         */
        val rdd4 = sc.makeRDD(Array(Array(1,3,5,7),Array(2,4,6,8),Array(9,10)))
        rdd4.flatMap(array=>array).collect().foreach(println)

        sc.stop()
    }
}

2.3.1.2 glom

将每一个分区形成一个数组,形成新的RDD类型时RDD[Array[T]]
在这里插入图片描述

2.3.1.3 groupBy(func)

分组,按照传入函数的返回值进行分组。将相同的key对应的值放入一个迭代器。
在这里插入图片描述

2.3.1.4 filter(func) 案例

返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成。
在这里插入图片描述

2.3.1.5 sample(withReplacement, fraction, seed)

以指定的随机种子随机抽样出数量为fraction的数据,withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样,seed用于指定随机数生成器种子。
在这里插入图片描述

2.3.1.6 distinct([numTasks]))

对源RDD进行去重后返回一个新的RDD。默认情况下,只有8个并行任务来操作,但是可以传入一个可选的numTasks参数改变它。
在这里插入图片描述

2.3.1.7 coalesce(numPartitions)

缩减分区数,用于大数据集过滤后,提高小数据集的执行效率。
在这里插入图片描述

2.3.1.8 repartition(numPartitions)

根据分区数,重新通过网络随机洗牌所有数据。
在这里插入图片描述
coalesce和repartition的区别

  1. coalesce重新分区,可以选择是否进行shuffle过程。由参数shuffle: Boolean = false/true决定。
  2. repartition实际上是调用的coalesce,默认是进行shuffle的。源码如下:
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    
    
  coalesce(numPartitions, shuffle = true)
}

2.3.1.9 sortBy(func,[ascending], [numTasks])

用func先对数据进行处理,按照处理后的数据比较结果排序,默认为正序。
在这里插入图片描述

2.3.2 双Value类型交互

2.3.2.1 union(otherDataset)

对源RDD和参数RDD求并集后返回一个新的RDD
在这里插入图片描述

2.3.2.2 subtract (otherDataset)

计算差的一种函数,去除两个RDD中相同的元素,不同的RDD将保留下来
在这里插入图片描述

2.3.2.3 intersection(otherDataset)

对源RDD和参数RDD求交集后返回一个新的RDD
在这里插入图片描述

2.3.2.4 cartesian(otherDataset)

笛卡尔积(尽量避免使用)
在这里插入图片描述

2.3.2.5 zip(otherDataset)

将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常。
在这里插入图片描述

2.3.3 Key-Value类型

2.3.3.1 partitionBy

对pairRDD进行分区操作,如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区, 否则会生成ShuffleRDD,即会产生shuffle过程。
在这里插入图片描述

2.3.3.2 groupByKey

groupByKey也是对每个key进行操作,但只生成一个sequence。
在这里插入图片描述

2.3.3.3 reduceByKey(func, [numTasks])

在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置。
在这里插入图片描述
reduceByKey和groupByKey的区别:

  1. reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v].
  2. groupByKey:按照key进行分组,直接进行shuffle。
  3. 开发指导:reduceByKey比groupByKey,建议使用。但是需要注意是否会影响业务逻辑。

2.3.3.4 aggregateByKey

参数:(zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U) => U)

  1. 作用:在kv对的RDD中,,按key将value进行分组合并,合并时,将每个value和初始值作为seq函数的参数,进行计算,返回的结果作为一个新的kv对,然后再将结果按照key进行合并,最后将每个分组的value传递给combine函数进行计算(先将前两个value进行计算,将返回结果和下一个value传给combine函数,以此类推),将key与计算结果作为一个新的kv对输出。
  2. 参数描述:
    (1)zeroValue:给每一个分区中的每一个key一个初始值;
    (2)seqOp:函数用于在每一个分区中用初始值逐步迭代value;
    (3)combOp:函数用于合并每个分区中的结果。
    在这里插入图片描述在这里插入图片描述

2.3.3.5 foldByKey

参数:(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
aggregateByKey的简化操作,seqop和combop相同
在这里插入图片描述

2.3.3.6 combineByKey[C]

(createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C)

  1. 作用:对相同K,把V合并成一个集合。
  2. 参数描述:
    (1)createCombiner: combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初始值
    (2)mergeValue: 如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并
    (3)mergeCombiners: 由于每个分区都是独立处理的, 因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器, 就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。
    在这里插入图片描述
    在这里插入图片描述

2.3.3.7 sortByKey([ascending], [numTasks])

在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
在这里插入图片描述

2.3.3.8 mapValues

针对于(K,V)形式的类型只对V进行操作
在这里插入图片描述

2.3.3.9 join(otherDataset, [numTasks])

在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
在这里插入图片描述

2.3.3.10 cogroup(otherDataset, [numTasks])

在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD
在这里插入图片描述

2.4 Action

2.4.1 reduce(func)

通过func函数聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据。
在这里插入图片描述

2.4.2 collect()

在驱动程序中,以数组的形式返回数据集的所有元素。
在这里插入图片描述

2.4.3 count()

返回RDD中元素的个数
在这里插入图片描述

2.4.4 first()

返回RDD中的第一个元素
在这里插入图片描述

2.4.5 take(n)

返回一个由RDD的前n个元素组成的数组
在这里插入图片描述

2.4.6 takeOrdered(n)

返回该RDD排序后的前n个元素组成的数组
在这里插入图片描述

2.4.7 aggregate

参数:(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)
aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。
在这里插入图片描述

2.4.8 fold(num)(func)

折叠操作,aggregate的简化操作,seqop和combop一样。
在这里插入图片描述

2.4.9 saveAsTextFile(path)

将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本

2.4.10 saveAsSequenceFile(path)

将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。

2.4.11 saveAsObjectFile(path)

用于将RDD中的元素序列化成对象,存储到文件中。

2.4.12 countByKey()

针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
在这里插入图片描述

2.4.13 foreach(func)

在数据集的每一个元素上,运行函数func进行更新。
在这里插入图片描述

2.5 RDD中的函数传递

在实际开发中我们往往需要自己定义一些对于RDD的操作,那么此时需要注意的是,初始化工作是在Driver端进行的,而实际运行程序是在Executor端进行的,这就涉及到了跨进程通信,是需要序列化的。下面我们看几个例子:

2.5.1 传递一个方法

import org.apache.spark.{
    
    SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

object SearchTest {
    
    
    def main(args: Array[String]): Unit = {
    
    

        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("search")

        val sc: SparkContext = SparkContext.getOrCreate(conf)
        
        val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))
        
        val search: Search = new Search("h")

        val rdd2: RDD[String] = search.getMatch01(rdd)
        rdd2.collect().foreach(println)
    }
}

class Search(query: String) extends Serializable {
    
    

    //过滤出包含字符串的数据
    def isMatch(s: String): Boolean = {
    
    
        s.contains(query)
    }

    //过滤出包含字符串的RDD
    def getMatch01(rdd:RDD[String]):RDD[String]={
    
    
        rdd.filter(isMatch)
    }

    //过滤出包含字符串的RDD
    def getMatch02(rdd:RDD[String]):RDD[String]={
    
    
        rdd.filter(x=>x.contains(query))
    }
}

如果类不继承Serializable会报下面的错

Exception in thread "main" org.apache.spark.SparkException: Task not serializable

这是因为getMatch1 方法中所调用的方法isMatch()是定义在Search这个类中的,实际上调用的是this. isMatch(),this表示Search这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。

2.5.2 传递一个属性

import org.apache.spark.{
    
    SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

object SearchTest {
    
    
    def main(args: Array[String]): Unit = {
    
    

        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("search")

        val sc: SparkContext = SparkContext.getOrCreate(conf)
        
        val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))
        
        val search: Search = new Search("h")

        val rdd2: RDD[String] = search.getMatch02(rdd)
        rdd2.collect().foreach(println)
    }
}

如果类不继承Serializable同样会报下面的错

Exception in thread "main" org.apache.spark.SparkException: Task not serializable

这是因为getMatche2方法中所调用的方法query是定义在Search这个类中的字段,实际上调用的是this. query,this表示Search这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。

2.6 RDD依赖关系

2.6.1 Lineage

RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
在这里插入图片描述
(1)读取一个文件并将其中内容映射成一个个元组
在这里插入图片描述
**注意:**RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

2.6.2 窄依赖

窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用,窄依赖我们形象的比喻为独生子女
在这里插入图片描述

2.6.3 宽依赖

宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition,会引起shuffle,总结:宽依赖我们形象的比喻为超生
在这里插入图片描述

2.6.4 DAG

DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。
在这里插入图片描述

2.6.5 任务划分

RDD任务切分中间分为:Application、Job、Stage和Task
1)Application:初始化一个SparkContext即生成一个Application
2)Job:一个Action算子就会生成一个Job
3)Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。
在这里插入图片描述
4)Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task。
**注意:**Application->Job->Stage-> Task每一层都是1对n的关系。

2.7 RDD缓存

RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。
但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
在这里插入图片描述
通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。
在这里插入图片描述
在存储级别的末尾加上“_2”来把持久化数据存为两份
在这里插入图片描述
缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。

将RDD转换为携带当前时间戳不做缓存
在这里插入图片描述
将RDD转换为携带当前时间戳并做缓存
在这里插入图片描述

2.8 RDD CheckPoint

Spark中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过lineage做容错的辅助,lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。
为当前RDD设置检查点。该函数将会创建一个二进制的文件,并存储到checkpoint目录中,该目录是用SparkContext.setCheckpointDir()设置的。在checkpoint的过程中,该RDD的所有依赖于父RDD中的信息将全部被移除。对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。

import org.apache.spark.rdd.RDD
import org.apache.spark.{
    
    SparkConf, SparkContext}

object ScalaTest {
    
    
    def main(args: Array[String]): Unit = {
    
    

        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("conf")

        val sc: SparkContext = SparkContext.getOrCreate(conf)

        // 设置检查点的存储目录
        sc.setCheckpointDir("hdfs://hadoop:9000/checkpoint")

        //创建字符串和系统时间拼接的字符串RDD
        val rdd: RDD[String] = sc.makeRDD(Array("hello "))
        val rdd2: RDD[String] = rdd.map(_+System.currentTimeMillis())

        // 对RDD指定checkpoint
        rdd2.checkpoint()

        // 结果显示时间数字是不变的
        for(i <- 0 to 9){
    
    
            rdd2.collect().foreach(println)
        }
    }
}

三、键值对RDD数据分区器

Spark目前支持Hash分区和Range分区,用户也可以自定义分区,Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数
注意:
(1)只有Key-Value类型的RDD才有分区器的,非Key-Value类型的RDD分区器的值是None
(2)每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。

3.1 获取RDD分区

可以通过使用RDD的partitioner 属性来获取 RDD 的分区方式。它会返回一个 scala.Option 对象, 通过get方法获取其中的值。相关源码如下:

def getPartition(key: Any): Int = key match {
    
    
  case null => 0
  case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
}
def nonNegativeMod(x: Int, mod: Int): Int = {
    
    
  val rawMod = x % mod
  rawMod + (if (rawMod < 0) mod else 0)
}

在这里插入图片描述

3.2 Hash分区

HashPartitioner分区的原理:对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于0,则用余数+分区的个数(否则加0),最后返回的值就是这个key所属的分区ID。
使用Hash分区的实操
在这里插入图片描述

3.3 Ranger分区

HashPartitioner分区弊端:可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据。
RangePartitioner作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。实现过程为:
第一步:先重整个RDD中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;
第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求RDD中的KEY类型必须是可以排序的

3.4 自定义分区

要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner 类并实现下面三个方法。
(1)numPartitions: Int:返回创建出来的分区数。
(2)getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)。
(3)equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同。
需求:将相同后缀的数据写入相同的文件,通过将相同后缀的数据分区到相同的分区并保存输出来实现。
在这里插入图片描述
使用自定义的 Partitioner 是很容易的:只要把它传给 partitionBy() 方法即可。Spark 中有许多依赖于数据混洗的方法,比如 join() 和 groupByKey(),它们也可以接收一个可选的 Partitioner 对象来控制输出数据的分区方式。

猜你喜欢

转载自blog.csdn.net/pageniao/article/details/106248578