第二天:Spark Core

在这里插入图片描述

Java IO回忆

字节跟字符区别(基础图),InputStream、OutputStream、Writer、Reader 。
字节流,分2种:

a.输入
b.输出

在这里插入图片描述
2.字符流,也分2种:

a.输入(读,即读取)
b.输出(写,即写入)

在这里插入图片描述
PS: 字节流是万能的,方便人类读写才出来的字符流。字符流就包装后的字节流。

文件字节流读入
InputStream in = new FIleInputStream("xxx")
缓冲流, 体现了Java装饰者模式
InputStream bufferIn = new BufferedInputStream(new FileInputStream("xxxx"))

在这里插入图片描述

InputStream in = new FIleInputStream("xxx")
一行行的读取字符, 字节到字符的转换需要注意下
Reader  reader =  new BufferedReader(new InputStreamReader(new FileInputStream("xxx"),"UTF-8"))
当我们真正当readLine的时候才会不断的向前请求需求读取数据,真正数据数据的只有一个类,其余的无非就是装饰再装饰而已。

InputStreamReader 类是从字节流到字符流的桥接器:它使用指定的字符集读取字节并将它们解码为字符
在这里插入图片描述
End:Java中的IO不断包装,包装的是结构的变化,只有在readLine的时候才会不断向上反带调。

1. RDD

模仿上面的Java IO 不断的对基础类进行装饰
在这里插入图片描述

    val sc = new SparkContext(conf)
    //3.使用sc创建RDD并执行相应的transformation和action
    val line: RDD[String] = sc.textFile(args(0))
    val words: RDD[String] = line.flatMap(_.split(" "))
    val wordToOne: RDD[(String, Int)] = words.map((_, 1))
    val wordSum: RDD[(String, Int)] = wordToOne.reduceByKey(_ + _)
    val result: Array[(String, Int)] = wordSum.collect()

在这里插入图片描述
这里面基类就是RDD。上面的流程图就是在封装逻辑思维,用到的就是Scala中的 控制抽象 功能。也就是把一个逻辑思维传递给一个函数,就是把函数当参数传递的意思。上面的各种逻辑封装只有调用了Collect 才会触发收集数据对功能

控制抽象:

def myShop(block: => Unit) {

    println("Welcome in!")
    block
    println("Thanks for coming!")

}

def main(args: Array[String]){
    myShop( println("I wanna buy a condom") )
}

什么是RDD

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据处理逻辑抽象。,工作中可以认为是一个Seq,代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
不可变:可以类比Java中的String,只是不提供可以改变的接口。操作了数据就返回一个新的String或者RDD。
可分区:Kafka、HBase都是可分区的,注意理解并发跟并行的概念。可以将一个大的数据集放到不同的分区中,而不同的分区正好可以对应上spark中的多个executor

RDD属性

 * Internally, each RDD is characterized by five main properties:
 *
 *  - A list of partitions
 *  - A function for computing each split
 *  - A list of dependencies on other RDDs
 *  - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
 *  - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
 *    an HDFS file)
  1. 一组分区(Partition),即数据集的基本组成单位;
  2. 一个计算每个分区的函数;
  3. RDD之间的依赖关系,有直接依赖或者间接依赖,也可以称血缘。
  4. 一个Partitioner,即RDD的分片函数;
  5. 一个列表,存储存取每个Partition的优先位置(preferred location)。

PS :大数据计算时候移动数据不如移动计算划算,把数据copy到代码所在位置不如把代码copy到数据所在位置。对应第五个特性。

RDD特性

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

1. 弹性

存储的弹性:内存与磁盘的自动切换;
容错的弹性:数据丢失可以自动恢复;
计算的弹性:计算出错重试机制;
分片的弹性:可根据需要重新分片。

2. 分区

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

3. 只读

如下图所示,RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD。
在这里插入图片描述
由一个RDD转换到另一个RDD,可以通过丰富的操作算子实现,不再像MapReduce那样只能写map和reduce了。
RDD的操作算子包括两类

  1. 一类叫做Transformations(转换算子),它是用来将RDD进行转化,构建RDD的血缘关系;
  2. 另一类叫做Actions(行动算子),它是用来触发RDD的计算,得到RDD的相关计算结果或者将RDD保存的文件系统中。

算子:

从认知心理学角度看解决问题其实是将问题的初始状态通过一系列的操作(算子(Operate))来对问题的状态进行转换,然后最终达到完成解决的状态。Spark 中的函数就是算子

4. 依赖

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

5. 缓存

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

6. CheckPoint

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

2. RDD 编程

编程模型

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

在这里插入图片描述
在这里插入图片描述

RDD创建

在Spark中创建RDD的创建方式可以分为三种:

  1. 从集合中创建RDD
  2. 从外部存储创建RDD
  3. 从其他RDD创建
从集合中创建RDD

从集合中创建RDD,Spark主要提供了两种函数:parallelizemakeRDD


object SparkRDD01 {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)

    //1.创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
    //2.创建SparkContext,该对象是提交Spark App的入口
    val sc = new SparkContext(conf)
    // 1. 从内存中创建RDD 底层实现就是 parallelize
    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    // 2.  从内存中创建  看调用底层会发现有关联
    val arrayRDD:RDD[Int] = sc.parallelize(Array(1, 2, 3, 4, 5, 6))
    listRDD.collect().foreach(println)
  }
}
从外部存储创建RDD

包括本地的文件系统,还有所有Hadoop支持的数据集,比如HDFS、Cassandra、HBase等。默认从文件中读取的都是String类型。

val lines:RDD[String] = sc.textFile("in")
scala> val rdd2= sc.textFile("hdfs://hadoop102:9000/RELEASE")
rdd2: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/RELEASE MapPartitionsRDD[4] at textFile at <console>:24
从其他RDD创建

参考WordCount即可。

分区

    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    listRDD.saveAsTextFile("outputfile")

输出结果:
在这里插入图片描述

  def makeRDD[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    parallelize(seq, numSlices)
  }
  ---
    def defaultParallelism: Int = {
    assertNotStopped()
    taskScheduler.defaultParallelism
  }
  ---
  def defaultParallelism(): Int  ctrl +H 看当前类子类的实现
  ---
    override def defaultParallelism(): Int = backend.defaultParallelism()
  ----
SchedulerBackend的 特质的父类,
  ---
   CoarseGrainedSchedulerBackend
  override def defaultParallelism(): Int = {
    conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
    // 此处的conf  就是sc 中定义的conf
  }
  ---
  def getInt(key: String, defaultValue: Int): Int = {
    getOption(key).map(_.toInt).getOrElse(defaultValue)
  }
  --- 
   totalCoreCount.addAndGet(cores)
   // 本地模式 所用核数 
   
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")

结论: 分区跟 local[*]有关。内存中的默认跟配置的核数有关。
在这里插入图片描述
在这里插入图片描述
指定分区个数:
在这里插入图片描述
看下 读入文件写入后的分区数:

    val listRDD: RDD[String] = sc.textFile("in")
    listRDD.saveAsTextFile("outputfile")

结果是:
在这里插入图片描述

  def textFile(
      path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
    assertNotStopped()
    hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
      minPartitions).map(pair => pair._2.toString).setName(path)
  }
  ---
    def defaultMinPartitions: Int = math.min(defaultParallelism, 2)

读入文件时候指定分区个数写出的的时候就按照分区个数输出:

    val listRDD: RDD[String] = sc.textFile("in",3)
    listRDD.saveAsTextFile("outputfile")

sc.textFile 注意点:
比如in文件夹下的word.text

12345

在这里插入图片描述
逻辑思维应该是将数据分成了2份,可是我们看下里面代码:
在这里插入图片描述
用到了Hadoop切分(Spark分区规则跟Hadoop规则完全一样), 5/2 = 2,2,1 这样的三份。
并且:你可以看到part-00000有数据其余两个是没有的!这说明计算分区跟往分区中写入数据是两个不同的操作!,Hadoop默认是按照行来搞的!更细节要参考 20P
End:

  1. 读取文件时,传递的分区参数为最小分区数,但是不一定是这个分区数, 取决于Hadoop读取文件时候分片规则。
  2. 内存中分区相对来说很简单,看参数即可。

RDD转换(重点)

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

Value型

1. map(fun)

在这里插入图片描述
作用:返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成。每个元素操作,分区不变。同时这个_ * 2应该会把执行任务分发到若干个Executor中。

    val listRDD :RDD[Int] = sc.makeRDD(1 to 10)
    val mapRDD:RDD[Int] = listRDD.map(_ * 2)
    mapRDD.collect().foreach(println)
2. mapPartitions(func)

在这里插入图片描述
作用:类似于map,但独立地在RDD的每一个分片(分区)上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U]。假设有N个元素,有M个分区,那么map的函数的将被调用N次,而mapPartitions被调用M次,一个函数一次处理所有分区。
PS:这个跟Map的算子差别就是通过网络传输分发任务的次数,效率上这个更好点。

    val listRDD :RDD[Int] = sc.makeRDD(1 to 10)
    // mapPartitions 可以对一个RDD中所有分区进行遍历
    // mapPartitions 效率优于Map 算子,减少来饿发送到执行器交互次数
    // mapPartitions 单独算子更加吃内存,可能出现内存溢出(OOM)
    val mapPartitionsRDD:RDD[Int] = listRDD.mapPartitions(datas=>{
      datas.map(data=>data*2)
      // 这里对 datas 是Scala中的 iterator, map 也是Scala中的map 。{}里面的整体逻辑算一个Spark中的一个计算。
    })
    mapPartitionsRDD.collect().foreach(println)
3. mapPartitionsWithIndex

类似于python中的enumerate,在2的基础上想要知道数据都在那个分区中。
作用:类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U];
在这里插入图片描述

   val listRDD: RDD[Int] = sc.makeRDD(1 to 10)
    val tupleRDD: RDD[(Int, String)] = listRDD.mapPartitionsWithIndex {
      case (num, datas) => {
        datas.map((_, " 分区号:" + num))
      }
      // 模式匹配
    }
    tupleRDD.collect().foreach(println)
4. map、mapPartition、mapPartitionWithIndex区别。
  1. map():每次处理一条数据。
  2. mapPartition():每次处理一个分区的数据,这个分区的数据处理完后,原RDD中分区的数据才能释放,可能导致OOM。
  3. 开发指导:当内存空间较大的时候建议使用mapPartition(),以提高处理效率。
  4. mapPartitionWithIndex则关注的是数据来自那个分区。
5. flatMap

可以认为是跟Scala中的类似,就是个扁平化的操作。

    val listRDD: RDD[List[Int]] = sc.makeRDD(Array(List(1, 2), List(3, 4)))
    val flatMapRDD: RDD[Int] = listRDD.flatMap(datas => datas)
    flatMapRDD.collect().foreach(println)
6. glom

作用:将每一个分区形成一个数组,形成新的RDD类型时RDD[Array[T]],相当于系统自动提供了一个分区函数,然后接下来你可以按照分区来求的每个分区中的min,max,sum,avg等等。

    val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
    //  如果是 1 to 18 ,4 则划分为 4 ,4,4,6
    // 将一个分区的数据自动放到一个数组中
    val glomRDD: RDD[Array[Int]] = listRDD.glom()
    glomRDD.collect().foreach(array => println(array.mkString(",")))

结果:

5,6,7,8
9,10,11,12
13,14,15,16
7. groupBy

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


    // 按照指定规则进行分组
    // 分组后的数据形成2元祖 K-V,K表示分组的key,V表示分组数据集合。
    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    val groupByRDD: RDD[(Int, Iterable[Int])] = listRDD.groupBy(_ % 2)
    groupByRDD.collect().foreach(println)
8. filter

作用:过滤。返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成。

    // 按照规则过滤数据
    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    val filterRDD: RDD[Int] = listRDD.filter(_ % 2 == 0)
    filterRDD.collect().foreach(print)
9. sample(withReplacement, fraction, seed)

功能:随机性抽取一些数据。
withReplacement:表示抽出样本后是否在放回去,true表示会放回去,这也就意味着抽出的样本可能有重复
fraction :这是一个double类型的参数(0-1之间),设定一个出现概率阈值,通过seed给每个数据评价出一个概率,大于阈值留下,否则不留。

seed:表示一个种子,根据这个seed随机抽取,一般情况下只用前两个参数就可以,那么这个参数是干嘛的呢,这个参数一般用于调试,有时候不知道是程序出问题还是数据出了问题,就可以将这个参数设置为定值。一般用System.currentTimeMilles确保种子的不同性。可以参考下python的 numpy.seed,挺好玩的一个随机数选择器。


---
  def sample(
      withReplacement: Boolean,
      fraction: Double,
      seed: Long = Utils.random.nextLong): RDD[T] = {
    require(fraction >= 0,
      s"Fraction must be nonnegative, but got ${fraction}")

    withScope {
      require(fraction >= 0.0, "Negative fraction value: " + fraction)
      if (withReplacement) {
        new PartitionwiseSampledRDD[T, T](this, new PoissonSampler[T](fraction), true, seed)
        // 柏松分布 放回
      } else {
        new PartitionwiseSampledRDD[T, T](this, new BernoulliSampler[T](fraction), true, seed)
        // 伯努利分布 不放回
      }
    }
  }
10. distinct([numPartitions]))

作用:对源RDD进行去重后返回一个新的RDD。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
    //2.创建SparkContext,该对象是提交Spark App的入口
    val sc = new SparkContext(conf)
    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 1, 5, 2, 9, 6, 1))
    // 默认是 系统全部进程来分区数据 我这里是8个
    val distinctRDD: RDD[Int] = listRDD.distinct()
    // 对数据进行 shuffle 刷新重组  然后数据还是8个分区, 因为 去重后可能少于8个,可以指定分区数
    val distinctRDD1: RDD[Int] = listRDD.distinct(2)
    distinctRDD.collect().foreach(println)
    distinctRDD.saveAsTextFile("output")

shuffle: 表示对一个分区的数据进行打乱拆分到不同的分区 ,不过如果是下面这样不算是shuffle打乱哦。
在这里插入图片描述

11. coalesce(numPartitions)

作用:缩减分区数,用于大数据集过滤后,提高小数据集的执行效率。

// 缩减分区可以认为简单对分区合并,没有涉及到shuffle
    val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
    println("缩减分区前:" + listRDD.partitions.size)
    val coalesceRDD: RDD[Int] = listRDD.coalesce(3)
    println("缩减分区后:" + coalesceRDD.partitions.size)
    coalesceRDD.saveAsTextFile("output")
12. repartition(numPartitions)

作用:根据分区数,重新通过网络随机洗牌所有数据,repartitionAndSortWithinPartitions

    val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
    println("缩减分区前:" + listRDD.partitions.size)
    val reRDD: RDD[Int] = listRDD.repartition(2)
    println("缩减分区后:" + reRDD.partitions.size)
    reRDD.saveAsTextFile("output")

在这里插入图片描述

13. coalesce和repartition的区别
  1. coalesce重新分区,suffle=false,可以选择是否进行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)
  }
14. sortBy(func,[ascending], [numTasks])

作用:使用func先对数据进行处理,按照处理后的数据比较结果排序,默认为正序,排序后的数据分区数多少也可以指定。

    val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
    listRDD.sortBy(x=>x).collect().foreach(println) // 从小到大
    listRDD.sortBy(x=>x,false).collect().foreach(println) // 从大到小
15. pipe(command, [envVars])

作用:管道,针对每个分区,都执行一个shell脚本,返回输出的RDD。
注意:脚本需要放在Worker节点可以访问到的位置

#!/bin/sh
echo "Running shell script"
while read LINE; do
   echo ${LINE}    
done

scala:

package test

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext

object PipeTest {

  def main(args: Array[String]) {
    val sparkConf = new SparkConf().setAppName("pipe Test")
    val sc = new SparkContext(sparkConf)
    val data = List("hi", "hello", "how", "are", "you")
    val dataRDD = sc.makeRDD(data)
    val scriptPath = "/home/gt/spark/bin/echo.sh"
    val pipeRDD = dataRDD.pipe(scriptPath)
    print("! "+pipeRDD.count()+" !!!")
    //输出6,如果是sc.makeRDD(data,2)输出7
    sc.stop()
  }
}

双Value类型交互

1. union(otherDataset)

作用:对源RDD和参数RDD求并集后返回一个新的RDD,有重复的也不会消除哦

    val listRDD1: RDD[Int] = sc.makeRDD(1 to 6)
    val listRDD2: RDD[Int] = sc.makeRDD(5 to 8)
    val totalRDD: RDD[Int] = listRDD1.union(listRDD2)
    //  1234565678
2. subtract (otherDataset)

计算差的一种函数,去除两个RDD中相同的元素,不同的RDD将保留下来

   val listRDD1: RDD[Int] = sc.makeRDD(1 to 5)
    val listRDD2: RDD[Int] = sc.makeRDD(2 to 4)
    val totalRDD: RDD[Int] = listRDD1.subtract(listRDD2) // 在1 不在2 
3. intersection(otherDataset)

作用:对源RDD和参数RDD求交集后返回一个新的RDD

   val listRDD1: RDD[Int] = sc.makeRDD(1 to 5)
    val listRDD2: RDD[Int] = sc.makeRDD(2 to 4)
    val totalRDD: RDD[Int] = listRDD1.intersection(listRDD2) // 在1且在2 
4. cartesian(OtherDataset)

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

5. zip(OtherDataset)

作用:将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常。

    val listRDD1: RDD[Int] = sc.makeRDD(1 to 3,3)
    val listRDD2: RDD[Int] = sc.makeRDD(Array("a","b","c"),3)
    val zipRDD: RDD[(Int, Int)] = listRDD1.zip(listRDD2)

Key-Value型

RDD集合中的每一个item 都是KV形式的。里面用到了隐式转换,在makeRDD后根据参数的不同有不同的RDD子类实现。比如KV的RDD

PairRDDFunctions
1. partitionBy

作用:对pairRDD进行分区操作,如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区, 否则会生成ShuffleRDD,即会产生shuffle过程。


package com.sowhat

import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}

/**
 * @author sowhat
 * @create 2020-06-12 15:55
 */
object WordCountLocal {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
    //2.创建SparkContext,该对象是提交Spark App的入口
    val sc = new SparkContext(conf)
    // 缩减分区可以认为简单对分区合并,没有涉及到shuffle
    val rdd = sc.parallelize(Array((1,"aaa"),(2,"bbb"),(3,"ccc"),(4,"ddd")),4)
    println(rdd.partitions.size)
    var rdd2 = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))
    rdd2 = rdd.partitionBy(new MyPartition(2))
    println(rdd2.partitions.size)
    sc.stop()
  }
}
class MyPartition(num:Int) extends Partitioner {
  override def numPartitions: Int = {
    num
  }

  override def getPartition(key: Any): Int = {
    1
  }
}
2. groupByKey

作用:groupByKey也是对每个key进行操作,但只生成一个seq。

    val words = Array("one","two","two","three","three","three")
    val wordPairsRDD: RDD[(String, Int)] = sc.makeRDD(words).map((_,1))
    val group: RDD[(String, Iterable[Int])] = wordPairsRDD.groupByKey()
    val res: RDD[(String, Int)] = group.map(t=>(t._1,t._2.sum))
    res.collect().foreach(println)
    res.saveAsTextFile("output")
    ---
    (two,2)
    (one,1)
    (three,3)
3. reduceByKey(func, [numTasks])

在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置。

  val rdd: RDD[(String, Int)] = sc.makeRDD(List(("so",1),("so",2),("what",3),("what",5)))
    val reduce: RDD[(String, Int)] = rdd.reduceByKey(_ + _)
    reduce.collect().foreach(println)
    ---
    (what,8)
    (so,3)
4. reduceByKey和groupByKey的区别
  1. reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v].
  2. groupByKey:按照key进行分组,直接进行shuffle。
  3. 开发指导:reduceByKey比groupByKey,建议使用。但是需要注意是否会影响业务逻辑。在对大数据进行复杂计算时,reduceByKey优于groupByKey。

另外,如果仅仅是group处理,那么以下函数应该优先于 groupByKey :
  (1)、combineByKey 组合数据,但是组合之后的数据类型与输入时值的类型不一样。
  (2)、foldByKey合并每一个 key 的所有值,在级联函数和“零值”中使用。
细节分析如下 :

val words = Array("one", "two", "two", "three", "three", "three")  
  
val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))  
  
val wordCountsWithReduce = wordPairsRDD.reduceByKey(_ + _)  
  
val wordCountsWithGroup = wordPairsRDD.groupByKey().map(t => (t._1, t._2.sum))  

wordCountsWithReducewordCountsWithGroup是完全一样的,但是,它们的内部运算过程是不同的。

  1. 当采用reduceByKey时,Spark可以在每个分区移动数据之前将待输出数据与一个共用的key结合。借助下图可以理解在reduceByKey里究竟发生了什么。 注意在数据对被搬移前同一机器上同样的key是怎样被组合的(reduceByKey中的lamdba函数)。然后lamdba函数在每个区上被再次调用来将所有值reduce成一个最终结果。整个过程如下:
    在这里插入图片描述
  2. 当采用groupByKey时,由于它不接收函数,spark只能先将所有的键值对(key-value pair)都移动,这样的后果是集群节点之间的开销很大,导致传输延时。整个过程如下:在这里插入图片描述
5. aggregateByKey

参数:

  def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,
      combOp: (U, U) => U): RDD[(K, U)] = self.withScope {
    aggregateByKey(zeroValue, defaultPartitioner(self))(seqOp, combOp)
  }
  // 注意这里U 类型有个ClassTag 有个运行时候的反射类型推断。所以不用写类型在后面参数中。
  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:函数用于合并每个分区中的结果。

  1. 需求:创建一个pairRDD,取出每个分区相同key对应值的最大值,然后相加。python版解释
    在这里插入图片描述
    val rdd = sc.parallelize(List(("a", 3), ("a", 2), ("c", 4), ("b", 3), ("c", 6), ("c", 8)), 2)
    val aggRDD: RDD[(String, Int)] = rdd.aggregateByKey(0)(math.max(_, _), _ + _)
    aggRDD.collect().foreach(println)
    ---
    (b,3)
    (a,3)
    (c,12)
6. foldByKey
  1. 作用:aggregateByKey的简化操作,seqop和combop相同
  2. 需求:创建一个pairRDD,计算相同key对应值的相加结果
  def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)] = self.withScope {
    foldByKey(zeroValue, defaultPartitioner(self))(func)
  }

代码:

    val rdd = sc.parallelize(List(("a", 3), ("a", 2), ("c", 4), ("b", 3), ("c", 6), ("c", 8)), 2)
    val flodRDD: RDD[(String, Int)] = rdd.foldByKey(0)(_ + _)
    flodRDD.collect().foreach(println)
---
(b,3)
(a,5)
(c,18)
---
val intRDD = sc.parallelize(List((1, 3), (1, 2), (1, 4), (2, 3), (3, 6), (3, 8)), 3)
val intFoldRDD = rdd.foldByKey(0)(_ + _)
 //  Array((3,14), (1,9), (2,3))
7. combineByKey[C]

原型:

  def combineByKey[C](
  // 注意这里的C 就是个范型,我们是不知道这个C是什么类型的,所以需要在调用的时候指明C的类型。
      createCombiner: V => C,
      // 一个分区中 每一个key下面的 value赋予个初始值
      mergeValue: (C, V) => C, 
      // 相当于在一个分区中把 同一个K下面 不同value 值进行相同操作,然后还要对出现次数操作
      mergeCombiners: (C, C) => C,
      //  不同分区相同key的 数据进行汇总
      numPartitions: Int): RDD[(K, C)] = self.withScope {
    combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners, numPartitions)(null)
  }
  1. 作用:对相同K,把V合并成一个集合。
  2. 参数描述:
  1. createCombiner: combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初始值
  2. mergeValue: 如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并
  3. mergeCombiners: 由于每个分区都是独立处理的, 因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器, 就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。
  1. 需求:创建一个pairRDD,根据key计算每种key的均值。(先计算每个key出现的次数以及可以对应值的总和再相除得到结果
  2. 需求分析:
    在这里插入图片描述
    val rdd = sc.parallelize(Array(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)), 2)
    val combine: RDD[(String, (Int, Int))] = rdd.combineByKey(
      (_, 1),
      (acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1), // 数值相加, 次数+1,
      (acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2) // 不同分区数据的汇总
    )
    combine.collect().foreach(println) // (b,(286,3))  (a,(274,3))

    val result = combine.map { case (key, value) => (key, value._1 / value._2.toDouble) }
    result.collect.foreach(println)
    // (b,95.33333333333333)
    // (a,91.33333333333333)
---
rdd.combineByKey{
	x=>x,
	(x:Int,y)=> x + y,
	(x:Int,y:Int) => x + y
}
理解类型有时候指明 有时候不指明的原因
8. sortByKey([ascending], [numTasks])
  1. 作用:在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
  2. 需求:创建一个pairRDD,按照key的正序和倒序进行排序
    val rdd = sc.parallelize(Array((3, "aa"), (6, "cc"), (2, "bb"), (1, "dd")))
    val sortRDD1: RDD[(Int, String)] = rdd.sortByKey(true)
    sortRDD1.collect().foreach(print) // (1,dd)(2,bb)(3,aa)(6,cc)
    println('-' * 20)
    val sortRDD2: RDD[(Int, String)] = rdd.sortByKey(false)
    sortRDD2.collect().foreach(print) // (6,cc)(3,aa)(2,bb)(1,dd)
9. mapValues
  1. 针对于(K,V)形式的类型只对V进行操作
  2. 需求:创建一个pairRDD,并将value添加字符串"1412"
   val listRDD: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    val resultRDD: RDD[(Int, String)] = listRDD.mapValues(_ + "1412")
    resultRDD.collect().foreach(println)
    /**
      * (1,a1412)
      * (2,b1412)
      * (3,c1412)
      * (4,d1412)
      * */
10. join(otherDataset, [numTasks])
  1. 作用:在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD。可以认为是mysql的join操作,两个RDD个数不同时候也允许。不过只显示同时有的!
  2. 需求:创建两个pairRDD,并将key相同的数据聚合到一个元组。
    val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1,1),(2,2),(3,3),(4,4)))
    val result1: RDD[(Int, (String, Int))] = rdd1.join(rdd2)
    result1.collect().foreach(println)
    // (4,(d,4)) (1,(a,1)) (2,(b,2)) (3,(c,3))

    val result2: RDD[(Int, (Int,String))] = rdd2.join(rdd1)
    result2.collect().foreach(println)
    // (4,(4,d)) (1,(1,a)) (2,(2,b)) (3,(3,c))

leftOuterJoin、rightOuterJoin

scala> val rdd = sc.makeRDD(List((1,2),(3,4),(3,6)))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[17] at makeRDD at <console>:24

scala> val other = sc.makeRDD(List((3,9)))
other: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[18] at makeRDD at <console>:24

scala> rdd.join(other).collect
res36: Array[(Int, (Int, Int))] = Array((3,(4,9)), (3,(6,9)))

scala> rdd.leftOuterJoin(other).collect
res37: Array[(Int, (Int, Option[Int]))] = Array((1,(2,None)), (3,(4,Some(9))), (3,(6,Some(9))))

scala> rdd.rightOuterJoin(other).collect
res38: Array[(Int, (Option[Int], Int))] = Array((3,(Some(4),9)), (3,(Some(6),9)))
11. cogroup(otherDataset, [numTasks])
  1. 作用:在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD,两个RDD不同的时候,会返回(独有key,(CompactBuffer(),CompactBuffer(独有V)))
  2. 需求:创建两个pairRDD,并将key相同的数据聚合到一个迭代器。
    val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1,1),(2,2),(3,3),(4,4)))
    val coRDD: RDD[(Int, (Iterable[String], Iterable[Int]))] = rdd1.cogroup(rdd2)
    coRDD.collect().foreach(println)
    /**
      * (4,(CompactBuffer(d),CompactBuffer(4)))
      * (1,(CompactBuffer(a),CompactBuffer(1)))
      * (2,(CompactBuffer(b),CompactBuffer(2)))
      * (3,(CompactBuffer(c),CompactBuffer(3)))
      * */

ps:对于 cogroup 跟 join 两元操作是可以从分区中获利的,比如 A.join(B),
尽量对AB 先用相同的分区处理下,
Anew = A.partitionBy(new spark.HashPartitioner(Num)).persist()
无非就是 分区处理然后将数据保存好, 这样防止重复性的数据混洗

12. keys values

scala>  sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
res31: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[11] at makeRDD at <console>:25

scala> res31.keys
res32: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[12] at keys at <console>:27

scala> res31.keys.collect
res33: Array[Int] = Array(1, 2, 3, 4)

scala> res31.values.collect
res34: Array[String] = Array(a, b, c, d)
13. subtracttByKey
rdd = {(1,2),(3,4),(3,6)}
other = {(3,9)}
rdd = subtracttByKey(other) 根据key 取出在rdd不在other 中
{(1,2)}

实战

  1. 数据结构:时间戳,省份,城市,用户,广告,中间字段使用空格分割。样本如下:
1516609143867 6 7 64 16
1516609143869 9 4 75 18
1516609143869 1 7 87 12
  1. 需求: 统计出每一个省份广告被点击次数的TOP3

  2. 解题思路:

  1. 先把省 + 广告 当整体看 点击数总量。
  2. 再把省当K,广告 + 点击数 当V,
  3. 对2 进行groupByKey
  4. 对3 进行排序 取值前三。
package com.sowhat

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

/**
  * @author sowhat
  * @create 2020-06-12 15:55
  */
object WordCountLocal {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
    //2.创建SparkContext,该对象是提交Spark App的入口
    val sc = new SparkContext(conf)

    // 时间戳,省份,城市,用户,广告,中间字段使用空格分割。
    val line: RDD[String] = sc.textFile("in/agent.log")

    //3.按照最小粒度聚合:((Province,AD),1)
    val provinceAdToOne = line.map { x =>
      val fields: Array[String] = x.split(" ")
      ((fields(1), fields(4)), 1)
    }

    //4.计算每个省中每个广告被点击的总数:((Province,AD),sum)
    val provinceAdToSum = provinceAdToOne.reduceByKey(_ + _)

    //5.将省份作为key,广告加点击数为value:(Province,(AD,sum))
    val provinceToAdSum = provinceAdToSum.map(x => (x._1._1, (x._1._2, x._2)))

    //6.将同一个省份的所有广告进行聚合(Province,List((AD1,sum1),(AD2,sum2)...))
    val provinceGroup = provinceToAdSum.groupByKey()

    //7.对同一个省份所有广告的集合进行排序并取前3条,排序规则为广告点击总数
    val provinceAdTop3 = provinceGroup.mapValues { x =>
      x.toList.sortWith((x, y) => x._2 > y._2).take(3)
    }

    //8.将数据拉取到Driver端并打印
    provinceAdTop3.collect().foreach(println)

    /**
      * (4,List((12,25), (2,22), (16,22)))
      * (8,List((2,27), (20,23), (11,22)))
      * (6,List((16,23), (24,21), (22,20)))
      * (0,List((2,29), (24,25), (26,24)))
      * (2,List((6,24), (21,23), (29,20)))
      * (7,List((16,26), (26,25), (1,23)))
      * (5,List((14,26), (21,21), (12,21)))
      * (9,List((1,31), (28,21), (0,20)))
      * (3,List((14,28), (28,27), (22,25)))
      * (1,List((3,25), (6,23), (5,22)))
      */
    sc.stop()
  }
}

Action

前面说到的都是逻辑转换算子,接下来说下行动算子,它是用来触发RDD的计算,得到RDD的相关计算结果或者将RDD保存的文件系统中。行动算子一定调用了sc.runJob,而逻辑算子一般返回一个新的MapPartitionsRDD

1. reduce(func)
  1. 作用:通过func函数聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据。
  2. 需求:创建一个RDD,将所有元素聚合得到结果。
    val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
    val result: Int = rdd1.reduce(_ + _)
    println(result) // 55
2. collect()
  1. 作用:在驱动程序中,以数组的形式返回数据集的所有元素。
  2. 需求:创建一个RDD,并将RDD内容收集到Driver端打印
 val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
    println(rdd1) // ParallelCollectionRDD[7] at makeRDD at WordCountLocal.scala:63
    println("-"*10)
    println(rdd1.collect()) // 返回一个Array   [I@322ec9
    val ints: Array[Int] = rdd1.collect() //此时数据已经返回到Driver了。
    ints.foreach(println) // 此时是在driver中执行的!
    val str: String = rdd1.collect().mkString(",")
3. count()
  1. 作用:返回RDD中元素的个数
  2. 需求:创建一个RDD,统计该RDD的条数
    val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
    val num: Long = rdd1.count()
4. first()
  1. 作用:返回RDD中的第一个元素
  2. 需求:创建一个RDD,返回该RDD中的第一个元素
    val rdd1: RDD[Int] = sc.makeRDD(1 to 10, 2)
    val fir: Int = rdd1.first() // 第一个元素
5. take(num:Int)
  1. 作用:返回一个由RDD的前n个元素组成的数组
  2. 需求:创建一个RDD,统计该RDD的条数
    val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3))
    val ints: Array[Int] = rdd.take(4) // 获取前N 个数据,以Array 形式返回。
    val str: String = ints.mkString(",")
6. takeOrdered(n:Int)
  1. 作用:返回该RDD排序后的前n个元素组成的数组
  2. 需求:创建一个RDD,统计该RDD的条数
    val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3))
    val ints: Array[Int] = rdd.takeOrdered(4) // 获取前N 个数据,以Array 形式返回。
    val str: String = ints.mkString(",") //  2,3,4,5
    println(str)
7. aggregate
  1. 参数:(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)
  2. 作用:aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。分区间合并的时候也要用到zeroValue
  3. 需求:创建一个RDD,将所有元素相加得到结果
  val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3),3)
    val i: Int = rdd.aggregate(0)(math.max(_,_),_ + _) //  5  +  6 +  8
    println(i) // 19 
    ---
    val rdd1 = sc.makeRDD(1 to 10,2)
    rdd1.aggregate(0)(_ + _,_ + _)// 55
    rdd1.aggregate(10)(_ + _,_ + _) // 55 + 10 + 10 + 10 ,两个分区内一个分区见
8. fold(num)(func)
  1. 作用:折叠操作,aggregate的简化操作,seqop和combop一样。原理通上。
  2. 需求:创建一个RDD,将所有元素相加得到结果
    val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3),3)
    val i: Int = rdd.fold(0)(_ + _)
    println(i) // 28
9. saveAsTextFile(path)

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

10. saveAsSequenceFile(path)

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

11. saveAsObjectFile(path)

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

12. countByKey()
  1. 作用:针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
  2. 需求:创建一个PairRDD,统计每种key的个数
    val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)
    val intToLong: collection.Map[Int, Long] = rdd.countByKey()
    intToLong.foreach(println)
    println("-"*10)
    // https://blog.csdn.net/weixin_42181200/article/details/80696369
    // countByValue把一个KV看成一个整体了
    val tupleToLong: collection.Map[(Int, Int), Long] = rdd.countByValue()
    tupleToLong.foreach(println)
----------
(3,2)
(1,3)
(2,1)
----------
((3,6),1)
((1,4),1)
((1,3),1)
((2,3),1)
((1,2),1)
((3,8),1)
16. collectAsMap lookup(key)
rdd = {(1,2),(3,4),(3,6)}
rdd.countByKey()// {(1,1),(3,2)}
rdd.collectAsMap()// Map{(1,2),(3,6)}
rdd.lookup(3)// [4,6]
15. foreach(func)
  1. 作用:在数据集的每一个元素上,运行函数func进行更新。
  2. 需求:创建一个RDD,对每个元素进行打印
rdd = sc.makeRDD(1 to 5,2)
rdd.foreach(println) // 这个代码是在 executor中执行的 跟 collect后再foreach位置不同。

rdd.foreach{
  case i =>{println(i)} //跟上面一样 执行在executor中!
}

RDD中的函数传递

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

1. 传递方法
/**
  * @author sowhat
  * @create 2020-06-15 17:40
  */
class Search() {

  def isMatch(s: String) = s.contains("H")

  //过滤出包含字符串的RDD 需考虑序列化
  def getMatch1(rdd: RDD[String]): RDD[String] = rdd.filter(isMatch)

  //过滤出包含字符串的RDD 这个不需要考虑序列化
  def getMatche2(rdd: RDD[String]): RDD[String] = rdd.filter(x => x.contains("H"))


}

object SeriTest {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("SeriTest").setMaster("local[*]")
    //2.创建SparkContext,该对象是提交Spark App的入口
    val sc = new SparkContext(conf)
    val search = new Search()
    val rdd: RDD[String] = sc.parallelize(Array("Hadoop","Spark","Hive","Sowhat"))
    val unit: RDD[String] = search.getMatch1(rdd)
    unit.collect().foreach(println)

   /**
     * Exception in thread "main" org.apache.spark.SparkException: Task not serializable
     * Caused by: java.io.NotSerializableException: com.sowhat.Search
     * */
  }
}

原因:

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

解决办法:

使类继承scala.Serializable即可。
class Search() extends Serializable{…}

2. 传递属性
class Search() {
  val query = "H"

  def isMatch(s: String) = s.contains(query)

  //过滤出包含字符串的RDD 需考虑序列化
  def getMatch1(rdd: RDD[String]): RDD[String] = rdd.filter(isMatch)

  //过滤出包含字符串的RDD 这个不需要考虑序列化
  def getMatche2(rdd: RDD[String]): RDD[String] = rdd.filter(x => x.contains(query))

}

object SeriTest {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("SeriTest").setMaster("local[*]")
    //2.创建SparkContext,该对象是提交Spark App的入口
    val sc = new SparkContext(conf)
    val search = new Search()
    val rdd: RDD[String] = sc.parallelize(Array("Hadoop","Spark","Hive","Sowhat"))
    val unit: RDD[String] = search.getMatche2(rdd)
    unit.collect().foreach(println)
  }
}

此时还是会报错,说我们任务中没有序列化,因为我们传递的属性中的query是 Search的对象,所以我们必须解决这个调用类对象属性序列化问题,
解决方法一:

class Search() extends Serializable 继承序列化类实现

解决方法二:
将类变量query赋值给局部变量,修改getMatche2为

  def getMatche2(rdd: RDD[String]): RDD[String] = {
    val query_ : String = this.query//将类变量赋值给局部变量
    rdd.filter(x => x.contains(query_))
  }

RDD依赖关系

在这里插入图片描述
RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。比如上图的ABCD四个任务存在依赖关系然后在不同的executor中,需要记录其依赖关系来保证系统稳定性。
在这里插入图片描述

scala> val listRDD = sc.makeRDD(List(1,2,3,4,4,3,2,5,4))
listRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at makeRDD at <console>:24

scala> val mapRDD = listRDD.map((_,1))
mapRDD: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[2] at map at <console>:26

scala> val resultRDD = mapRDD.reduceByKey( _ + _)
resultRDD: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[3] at reduceByKey at <console>:28

scala> resultRDD.toDebugString 
res0: String =
(16) ShuffledRDD[3] at reduceByKey at <console>:28 []
 +-(16) MapPartitionsRDD[2] at map at <console>:26 []
    |   ParallelCollectionRDD[1] at makeRDD at <console>:24 []
	这里是有一个任务调度依赖的记录的

scala> listRDD.toDebugString
res1: String = (16) ParallelCollectionRDD[1] at makeRDD at <console>:24 []

scala> mapRDD.toDebugString
res2: String =
(16) MapPartitionsRDD[2] at map at <console>:26 []
 |   ParallelCollectionRDD[1] at makeRDD at <console>:24 []

scala> resultRDD.dependencies
res3: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@28488f2e)


scala> listRDD.dependencies
res5: Seq[org.apache.spark.Dependency[_]] = List()

scala> mapRDD.dependencies
res6: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@ce51a54)

注意:RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

1. 窄依赖

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

2. 宽依赖

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

3. DAG

DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据
在这里插入图片描述
ps: 一般是从后往前推断stage,并且 Stage个数 = 1 + shuffle个数,重点理解下上图。

4. 任务划分(面试重点)

RDD任务切分中间分为:ApplicationJobStageTask

  1. Application:初始化一个SparkContext即生成一个Application,也就是一个AppMaster。
  2. Job:一个Action算子就会生成一个Job
  3. Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。
    在这里插入图片描述
  4. Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task。TaskSet的个数要看接受方有几个RDD。
    注意:Application == > Job ==> Stage ==> Task每一层都是1对n的关系。
Term(术语)Meaning(解释)
Application(Spark应用程序)运行于Spark上的用户程序,由集群上的一个driver program(包含SparkContext对象)和多个executor线程组成
Application jar(Spark应用程序JAR包)Jar包中包含了用户Spark应用程序,如果Jar包要提交到集群中运行,不需要将其它的Spark依赖包打包进行,在运行时
Driver program包含main方法的程序,负责创建SparkContext对象
Cluster manager集群资源管理器,例如Mesos,Hadoop Yarn
Deploy mode部署模式,用于区别driver program的运行方式:集群模式(cluter mode),driver在集群内部启动;客户端模式(client mode),driver进程从集群外部启动
Worker node工作节点, 集群中可以运行Spark应用程序的节点
ExecutorWorker node上的进程,该进程用于执行具体的Spark应用程序任务,负责任务间的数据维护(数据在内存中或磁盘上)。不同的Spark应用程序有不同的Executor
Task运行于Executor中的任务单元,Spark应用程序最终被划分为经过优化后的多个任务的集合
Job由多个任务构建的并行计算任务,具体为Spark中的action操作,如collect,save等)
Stage每个job将被拆分为更小的task集合,这些任务集合被称为stage,各stage相互独立(类似于MapReduce中的map stage和reduce stage),由于它由多个task集合构成,因此也称为TaskSet
5. RDD缓存

RDD通过persist方法或cache方法可以将前面的计算结果缓存,默认情况下 persist()会把数据以序列化的形式缓存在 JVM 的堆空间中。 但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。

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

6. RDD CheckPoint

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

    sc.setCheckpointDir("output") //  指定生成checkpoint目录
    val rdd = sc.parallelize(Array("sowhat"))
    val ch: RDD[String] = rdd.map(_.toString + System.currentTimeMillis())
    ch.checkpoint() // 要显示调用下
    ch.collect().foreach(println)
    Thread.sleep(1000)
    ch.collect().foreach(println) // 从显示结果来看 好像 在这一次会出发checkpoint
    Thread.sleep(1000)
    ch.collect().foreach(println)

    println("-" * 10)
    println(ch.toDebugString)
---
sowhat1592357507062
sowhat1592357508581 
sowhat1592357508581
----------
(4) MapPartitionsRDD[8] at map at WordCountLocal.scala:62 []
 |  ReliableCheckpointRDD[9] at collect at WordCountLocal.scala:64 [] // 依赖到checkpoint 即可, 

3. 键值对RDD数据分区器

Spark目前支持Hash分区和Range分区,用户也可以自定义分区,Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数
注意:

  1. 只有Key-Value类型的RDD才有分区器的,非Key-Value类型的RDD分区器的值是None
  2. 每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。
1. 获取RDD分区

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

    val pairs: RDD[(Int, Int)] = sc.makeRDD(Array((1, 1), (2, 2), (3, 3)))
    val partitioner: Option[Partitioner] = pairs.partitioner
    println(partitioner) // None
    val pared: RDD[(Int, Int)] = pairs.partitionBy(new org.apache.spark.HashPartitioner(2))
    println(pared.partitioner) // Some(org.apache.spark.HashPartitioner@2)
    println(pared.partitioner.get) // org.apache.spark.HashPartitioner@2  分区方式获取
    val partitions: Int = pared.getNumPartitions
    println(pared.glom().collect().foreach(println)) //
	
---  源码部分如下:
class HashPartitioner(partitions: Int) extends Partitioner {
  require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")

  def numPartitions: Int = partitions

  def getPartition(key: Any): Int = key match { // 对于每个KV数据要判断该数据需要存储在哪个partition中
    case null => 0
    case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
  }

  override def equals(other: Any): Boolean = other match {
    case h: HashPartitioner =>
      h.numPartitions == numPartitions
    case _ =>
      false
  }

  override def hashCode: Int = numPartitions
}

  def nonNegativeMod(x: Int, mod: Int): Int = {
    val rawMod = x % mod
    rawMod + (if (rawMod < 0) mod else 0)
  }
2. Hash分区

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

    val nopair: RDD[(Int, Int)] = sc.parallelize(List((1, 3), (1, 2), (2, 4), (2, 3), (3, 6), (3, 8)), 8)
    nopair.mapPartitionsWithIndex((index, iter) => {
      Iterator(index.toString + " : " + iter.mkString("|"))
    }).collect.foreach(println)
    // 对数据进行分区, 一共8个区域,然后
    val hashpair: RDD[(Int, Int)] = nopair.partitionBy(new org.apache.spark.HashPartitioner(7))
    println(hashpair.partitions.size) // 数据分区 一共7个,
    // 以分区未单位 处理数据,
    hashpair.mapPartitions(iter => Iterator(iter.length)).collect().foreach(print) // 0222000,注意看partitions的输入跟输出
    hashpair.saveAsTextFile("output")
-----
0 : 
1 : (1,3)
2 : (1,2)
3 : (2,4)
4 : 
5 : (2,3)
6 : (3,6)
7 : (3,8)
7
0222000
3. Ranger分区

HashPartitioner分区弊端:可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据。
RangePartitioner作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。实现过程为:

  1. 先从整个RDD中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;
  2. 判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求RDD中的KEY类型必须是可以排序的。
4. 自定义分区

要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner 类并实现下面三个方法。

  1. numPartitions: Int:返回创建出来的分区数
  2. getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)。
  3. equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同。
    需求:将相同后缀的数据写入相同的文件,通过将相同后缀的数据分区到相同的分区并保存输出来实现。
class CustomerPartitioner(numParts: Int) extends org.apache.spark.Partitioner {

  //覆盖分区数
  override def numPartitions: Int = numParts

  //覆盖分区号获取函数
  override def getPartition(key: Any): Int = {
    val ckey: String = key.toString
    ckey.substring(ckey.length - 1).toInt % numParts
  }
}
---
    val data: RDD[(Int, Int)] = sc.parallelize(Array((1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6)))
    val parRDD: RDD[(Int, Int)] = data.partitionBy(new CustomerPartitioner(2))
    val result: RDD[(Int, (Int, Int))] = parRDD.mapPartitionsWithIndex((index, items) => items.map((index, _)))
    result.collect.foreach(println)
---
(0,(2,2))
(0,(4,4))
(0,(6,6))
(1,(1,1))
(1,(3,3))
(1,(5,5))

4. 数据读取与保存

Spark的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。文件格式分为:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;
文件系统分为:本地文件系统HDFSHBASE以及数据库

文件类型读取保存

1. Text文件
  1. 数据读取:textFile(String)
scala> val hdfsFile = sc.textFile("hdfs://hadoop102:9000/fruit.txt")
hdfsFile: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/fruit.txt MapPartitionsRDD[21] at textFile at <console>:24
  1. 数据保存: saveAsTextFile(String)
scala> hdfsFile.saveAsTextFile("/fruitOut")
2. Json文件

如果JSON文件中每一行就是一个JSON记录,那么可以通过将JSON文件当做文本文件来读取,然后利用相关的JSON库对每一条数据进行JSON解析。
注意:使用RDD读取JSON文件处理很复杂,同时SparkSQL集成了很好的处理JSON文件的方式,所以应用中多是采用SparkSQL处理JSON文件

  1. 导入解析json所需的包
scala> import scala.util.parsing.json.JSON
  1. 上传json文件到HDFS
[atguigu@hadoop102 spark]$ hadoop fs -put ./examples/src/main/resources/people.json /
  1. 读取文件
scala> val json = sc.textFile("/people.json")
json: org.apache.spark.rdd.RDD[String] = /people.json MapPartitionsRDD[8] at textFile at <console>:24
  1. 解析json数据
scala> val result  = json.map(JSON.parseFull)
result: org.apache.spark.rdd.RDD[Option[Any]] = MapPartitionsRDD[10] at map at <console>:27
  1. 打印
scala> result.collect
res11: Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age -> 19.0)))
3. Sequence文件

SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。Spark 有专门用来读取 SequenceFile 的接口。在 SparkContext 中,可以调用 sequenceFile[ keyClass, valueClass]。
注意:SequenceFile文件只针对PairRDD

  1. 创建一个RDD
scala> val rdd = sc.parallelize(Array((1,2),(3,4),(5,6)))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[13] at parallelize at <console>:24
  1. 将RDD保存为Sequence文件
scala> rdd.saveAsSequenceFile("file:///opt/module/spark/seqFile")
  1. 查看该文件
[atguigu@hadoop102 seqFile]$ pwd
/opt/module/spark/seqFile
[atguigu@hadoop102 seqFile]$ ll
总用量 8
-rw-r--r-- 1 atguigu atguigu 108 109 10:29 part-00000
-rw-r--r-- 1 atguigu atguigu 124 109 10:29 part-00001
-rw-r--r-- 1 atguigu atguigu   0 109 10:29 _SUCCESS
[atguigu@hadoop102 seqFile]$ cat part-00000
SEQ org.apache.hadoop.io.IntWritable org.apache.hadoop.io.IntWritableط
  1. 读取Sequence文件
scala> val seq = sc.sequenceFile[Int,Int]("file:///opt/module/spark/seqFile")
seq: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[18] at sequenceFile at <console>:24
  1. 打印读取后的Sequence文件
scala> seq.collect
res14: Array[(Int, Int)] = Array((1,2), (3,4), (5,6))
4. 对象文件

对象文件是将对象序列化后保存的文件,采用Java的序列化机制。可以通过objectFile k,v 函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile() 实现对对象文件的输出。因为是序列化所以要指定类型。

  1. 创建一个RDD
scala> val rdd = sc.parallelize(Array(1,2,3,4))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at parallelize at <console>:24
  1. 将RDD保存为Object文件
scala> rdd.saveAsObjectFile("file:///opt/module/spark/objectFile")
  1. 查看该文件
[atguigu@hadoop102 objectFile]$ pwd
/opt/module/spark/objectFile
[atguigu@hadoop102 objectFile]$ ll
总用量 8
-rw-r--r-- 1 atguigu atguigu 142 109 10:37 part-00000
-rw-r--r-- 1 atguigu atguigu 142 109 10:37 part-00001
-rw-r--r-- 1 atguigu atguigu   0 109 10:37 _SUCCESS
[atguigu@hadoop102 objectFile]$ cat part-00000 
SEQ!org.apache.hadoop.io.NullWritable"org.apache.hadoop.io.BytesWritableW@`l
  1. 读取Object文件
scala> val objFile = sc.objectFile[Int]("file:///opt/module/spark/objectFile")
objFile: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[31] at objectFile at <console>:24
  1. 打印读取后的Sequence文件
scala> objFile.collect
res19: Array[Int] = Array(1, 2, 3, 4)

文件系统类数据读取与保存

1. HDFS

Spark的整个生态系统与Hadoop是完全兼容的,所以对于Hadoop所支持的文件类型或者数据库类型,Spark也同样支持,另外由于Hadoop的API有新旧两个版本,所以Spark为了能够兼容Hadoop所有的版本,也提供了两套创建操作接口,对于外部存储创建操作而言,hadoopRDD和newHadoopRDD是最为抽象的两个函数接口,主要包含以下四个参数.

  1. 输入格式(InputFormat): 制定数据输入的类型,如TextInputFormat等,新旧两个版本所引用的版本分别是org.apache.hadoop.mapred.InputFormatorg.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
  2. 键类型: 指定[K,V]键值对中K的类型
  3. 值类型: 指定[K,V]键值对中V的类型
  4. 分区值: 指定由外部存储生成的RDD的partition数量的最小值,如果没有指定,系统会使用默认值defaultMinSplits

注意:其他创建操作的API接口都是为了方便最终的Spark程序开发者而设置的,是这两个接口的高效实现版本.例如,对于textFile而言,只有path这个指定文件路径的参数,其他参数在系统内部指定了默认值
5. 在Hadoop中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为Hadoop本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压.
6. 如果用Spark从Hadoop中读取某种类型的数据不知道怎么读取的时候,上网查找一个使用map-reduce的时候是怎么读取这种这种数据的,然后再将对应的读取方式改写成上面的hadoopRDD和newAPIHadoopRDD两个类就行了。

2. MySQL

pom文件 添加依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>
package com.sowhat

import java.sql.{Connection, DriverManager, PreparedStatement}

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

/**
  * @author sowhat
  * @create 2020-06-17 13:50
  * spark 跟MySQL之间的 读写数据
  *
  */
object MysqlRDD {

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

    //1.创建spark配置信息
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")

    //2.创建SparkContext
    val sc = new SparkContext(sparkConf)

    //3.定义连接mysql的参数
    val driver = "com.mysql.jdbc.Driver"
    val url = "jdbc:mysql://hadoop102:3306/rdd"
    val userName = "root"
    val passWd = "000000"

    //创建JdbcRDD
    val rdd = new JdbcRDD(sc, () => {
      Class.forName(driver)
      DriverManager.getConnection(url, userName, passWd)
    },
      "select * from rddtable where id > ? and id < ?", //  mysql 语句
      1, // 从mysql中读取数据的时候一般划区域的读取数据,然后需要获得数据的起始点位置跟终止点位置
      10,
      1, // 读取数据后分区数指定,
      r => (r.getInt(1), r.getString(2))
    )
    //打印最后结果
    println(rdd.count())
    rdd.foreach(println)

    // 数据库连接比较重要,给每一个分区分配一个连接 而不是给每个数据分配一个连接。
    val dataRDD: RDD[(String, Int)] = sc.makeRDD(List(("zhangsan", 20), ("lisi", 30), ("wangsu", 40)))
    dataRDD.foreachPartition(data => {
      Class.forName(driver)
      val connection: Connection = java.sql.DriverManager.getConnection(url, userName, passWd)
      data.foreach { case (username, age) => {
        val sql = "insert into user (name,age) values (?,?)"
        val statement: PreparedStatement = connection.prepareStatement(sql)
        statement.setString(1, username)
        statement.setInt(2, age)
        statement.executeUpdate()
        statement.close()
        }
      }
    }
    )
    sc.stop()
  }
}
3.HBase

由于org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现,Spark 可以通过Hadoop输入格式访问HBase。这个输入格式会返回键值对数据,
其中键的类型为org.apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为org.apache.hadoop.hbase.client.Result。
Result。

  1. 添加依赖
<dependency>
	<groupId>org.apache.hbase</groupId>
	<artifactId>hbase-server</artifactId>
	<version>1.3.1</version>
</dependency>

<dependency>
	<groupId>org.apache.hbase</groupId>
	<artifactId>hbase-client</artifactId>
	<version>1.3.1</version>
</dependency>
  1. 从HBase读取数据
package com.sowhat


import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}
import org.apache.hadoop.hbase.client.{HBaseAdmin, Put, Result}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.{TableInputFormat, TableOutputFormat}
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapred.JobConf
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}


/**
  * @author sowhat
  * @create 2020-06-17 14:14
  */
object HBaseSpark {

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

    //创建spark配置信息
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")

    //创建SparkContext
    val sc = new SparkContext(sparkConf)

    //构建HBase配置信息
    val conf: Configuration = HBaseConfiguration.create()
    conf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")
    conf.set(TableInputFormat.INPUT_TABLE, "rddtable")

    //从HBase读取数据形成RDD
    val hbaseRDD: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
      conf,
      classOf[TableInputFormat],
      classOf[ImmutableBytesWritable],
      classOf[Result])

    val count: Long = hbaseRDD.count()
    println(count)

    //对hbaseRDD进行处理
    hbaseRDD.foreach {
      case (_, result) =>
        val key: String = Bytes.toString(result.getRow)
        val name: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name")))
        val color: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("color")))
        println("RowKey:" + key + ",Name:" + name + ",Color:" + color)
    }

    //关闭连接
    sc.stop()
  }

  def main1(args: Array[String]) {
    //获取Spark配置信息并创建与spark的连接
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("HBaseApp")
    val sc = new SparkContext(sparkConf)

    //创建HBaseConf
    val conf = HBaseConfiguration.create()
    val jobConf = new JobConf(conf)
    jobConf.setOutputFormat(classOf[TableOutputFormat])
    jobConf.set(TableOutputFormat.OUTPUT_TABLE, "fruit_spark")

    //构建Hbase表描述器
    val fruitTable = TableName.valueOf("fruit_spark")
    val tableDescr = new HTableDescriptor(fruitTable)
    tableDescr.addFamily(new HColumnDescriptor("info".getBytes))

    //创建Hbase表
    val admin = new HBaseAdmin(conf)
    if (admin.tableExists(fruitTable)) {
      admin.disableTable(fruitTable)
      admin.deleteTable(fruitTable)
    }
    admin.createTable(tableDescr)

    //定义往Hbase插入数据的方法
    def convert(triple: (Int, String, Int)) = {
      val put = new Put(Bytes.toBytes(triple._1))
      put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(triple._2))
      put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("price"), Bytes.toBytes(triple._3))
      (new ImmutableBytesWritable, put)
    }

    //创建一个RDD
    val initialRDD = sc.parallelize(List((1, "apple", 11), (2, "banana", 12), (3, "pear", 13)))

    //将RDD内容写到HBase
    val localData = initialRDD.map(convert)

    localData.saveAsHadoopDataset(jobConf)
  }
}

Spark 累加器

Spark 三大数据结构
RDD:弹性分布式数据集
广播变量: 分布式只读共享变量
累加器: 分布式只写共享变量
RDD的情况下 若干executor从Driver获得的都是数据副本,各自操作不受干扰。

累加器用来对信息进行聚合,通常在向 Spark传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。

val dataRDD:RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
val result:Int = dataRDD.reduce(_ + _)
var sum = 0
dataRDD.foreach(i=> sum = sum + i)
println(sum) // 0

在这里插入图片描述

需要Spark把数据结构返回到driver中。

val dataRDD:RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
val accumulator:LongAccumulator = sc.LongAccumulator
dataRDD.foreach{
	case i => {
		accumulator.add(i)
	}
}
println("sum = " + accumulator.value) // 10
  1. 工作节点上的任务不能访问累加器的值。从这些任务的角度来看,累加器是一个只写变量
  2. 对于要在行动操作中使用的累加器,Spark只会把每个任务对各累加器的修改应用一次。因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在 foreach() 这样的行动操作中。转化操作中累加器可能会发生不止一次更新。
自定义累加器

自定义累加器类型的功能在1.X版本中就已经提供了,但是使用起来比较麻烦,在2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型累加器的实现方式。实现自定义类型累加器需要继承AccumulatorV2并覆写要求的方法。
需求:实现一个自定义的数值累加器

package com.sowhat
import org.apache.spark.util.AccumulatorV2
class MyAccu extends AccumulatorV2[Int, Int] {

  var sum = 0

  //判断是否为空
  override def isZero: Boolean = sum == 0

  //复制
  override def copy(): AccumulatorV2[Int, Int] = {
    val accu = new MyAccu
    accu.sum = this.sum
    accu
  }

  //重置
  override def reset(): Unit = sum = 0

  //累加
  override def add(v: Int): Unit = sum += v

  // 合并
  override def merge(other: AccumulatorV2[Int, Int]): Unit = sum += other.value

  //返回值
  override def value: Int = sum
}

main

package com.sowhat

import org.apache.spark.rdd.RDD
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
object AccuTest {
  def main(args: Array[String]): Unit = {
    //创建SparkConf
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("AccuTest")
    //创建SC
    val sc = new SparkContext(sparkConf)
    //创建自定义累加器对象
    val accu = new MyAccu
    //注册累加器
    sc.register(accu)
    //创建RDD
    val value: RDD[Int] = sc.parallelize(Array(1, 2, 3, 4))
    //在行动算子中对累加器的值进行修改
    value.foreach { x =>
      accu.add(1)
      println(x)
    }
    //打印累加器的值
    println(accu.value)
    //关闭SparkContext
    sc.stop()
  }
}

Spark 广播变量(调优策略)

如果一个任务有两个Executor,每个Executor有3个Task,则Driver需要发送6次重要数据到Task,用了广播变量可以直接将数据传送到Executor中,并且是只读的。

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。 在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。
在这里插入图片描述
使用广播变量的过程如下:

  1. 通过对一个类型T的对象调用SparkContext.broadcast创建出一个Broadcast[T]对象,任何可序列化的类型都可以这么实现。
  2. 通过value属性访问该对象的值(在Java中为value()方法)。
  3. 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。
    在这里插入图片描述
val rdd1 = sc.makeRDD(List((1,"a"),(2,"b"),(3,"c")))

val list  = List((1,1),(2,2),(3,3))

val joinRDD:RDD[(Int,(String,Int))] = rdd1.join(list)
// 会涉及到笛卡尔乘积的过程,耗时大,可以参考下面的 广播变量的操作

val broadcast:Broadcast[List[(Int,Int)]] = sc.broadcast(list)

val resultRDD:RDD[(Int,(String,Any))] = rdd1.map {
	case(key,value) =>{
		var v2:Any = null 
		for(t <- broadcast.value){
			if(key == t._1){
				v2 = t._2
			}
		}
		(key,(value,v2))
	}
}
resultRDD.foreach(println)

参考

spark运行原理
Spark全套资料

SoWhat1412 CSDN认证博客专家 CSDN签约作者 后端coder
微信搜索【SoWhat1412】,第一时间阅读原创干货文章。人之患、在好为人师、不实知、谨慎言。点点滴滴、皆是学问、看到了、学到了、便是收获、便是进步。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值