HUBU期末复习_大数据分析与内存计算_RDD部分

Spark RDD编程

1. 创建Spark连接

// Spark连接配置,setMaster为Spark程序运行位置,一般使用local在本地运行,也可以在cluster上运行;setAppName是Spark程序的名称
val conf = new SparkConf().setMaster("local[*]").setAppName("Name")

// 设置并行度,也即当前环境可用的核数量,可以不配置,默认为totalCores(也即当前环境最大可用核数量)
//conf.set("spark.default.parallelism","5")

// 创建Driver Program
val sc = new SparkContext(conf)

2. 创建RDD

  • 从文件系统/HDFS中加载数据创建RDD——sc.textFile() / sc.wholeTextFiles()

    • 以行为单位读取文件——sc.textFile()
    // textFile默认path路径是从当前项目根目录算起,可以使用绝对路径也可以使用相对路径
    // 1. 绝对路径
    val rdd = sc.textFile("D:\\Documents\\Projects\\JavaProjects\\spark\\src\\datas\\file1.txt")
    
    // 2. 相对路径
    val rdd = sc.textFile("src\\datas\\file1.txt")
    
    // 3. 使用通配符*
    val rdd = sc.textFile("src\\datas\\*.txt")
    
    // 4. 也可以读取目录(也即读取目录下所有文件)
    val rdd = sc.textFile("src\\datas")
    
    // 5. 也可以是hdfs文件路径
    val rdd = sc.textFile("hdfs://localhost:8020/text1.txt")
    
    • 以文件为单位读取——sc.wholeTextFiles()
    // textFile以行为单位读取数据,以字符串形式存储
    // wholeTextFiles以文件为单位读取数据,以元组形式存储,第一个参数为文件路径,第二个为内容
    val rdd = sc.wholeTextFiles("src\\datas")
    
  • 通过并行集合(数组)创建RDD——sc.parallelize(seq) / sc.makeRDD(seq)

    // 数组可以提前创建,也可以在创建RDD时直接创建
    val seq = Seq[Int](1,2,3,4,5)
    val list = List(1,2,3,4,5)
    val array = Array(1,2,3,4,5)
    
    // 创建RDD
    // makeRDD方法底层其实就是调用rdd对象的parallelize方法,可以看作是一个通俗易懂的parallelize
    val rdd = sc.parallelize(seq)
    val rdd = sc.makeRDD(list)
    val rdd = sc.makeRDD(array)
    
    val rdd = sc.makeRDD(List(1,2,3,4,5))
    
  • 并行度&&分区

    makeRDD方法可以传递第二个参数——numSlices 即分区数

    ​ 如果不传递,则使用默认值defaultParallelism

    ​ 默认情况下Spark从SparkConf对象中获取配置参数spark.default.parallelism

    ​ 如果获取不到,则使用totalCores属性,该属性取值为当前环境可用的最大核数量

    // 获取RDD分区数
    rdd.getNumPartitions // 底层是partitions.length
    rdd.partitions.length
    
    // 将处理的数据保存成文件
    rdd.saveAsTextFile("path")
    

3. RDD算子(其实就是RDD的各种方法method)

  • 分类
    • 转换transformation:功能的补充和封装,将旧的RDD包装成新的RDD
    • 行动action:触发任务的调度和作业的执行
3.1 RDD转换算子

RDD根据数据处理方式的不同将算子整体上分为value类型、双value类型、key-value类型

3.1.1 value类型
  • map

    • 作用:将处理的数据逐条进行映射转换,可以是类型转换也可以是值的转换
    • 函数签名:def mapU:ClassTag: RDD[U] 传递一个函数,返回一个新的RDD
     def mapFun(num:Int):Int = {
          num * 2
        }
    
        // map
        //val mapRDD = rdd.map(mapFun)   ---很麻烦,因为函数很简单 =>使用匿名函数
        //val mapRDD = rdd.map((num:Int) => {num*2}) 当函数只有一行时,中括号可以省略
        //val mapRDD = rdd.map((num:Int) => num*2) 当类型可以推断时,可以不用指定类型
        //val mapRDD = rdd.map((num) => num*2 小括号也可以省略
        //val mapRDD = rdd.map(num => num*2) 变量较少且顺序固定时可以用_代替
        val mapRDD = rdd.map(_*2)
    
  • flatMap

    • 作用:将处理的数据进行扁平化后再进行映射处理,也即扁平映射.(将整体拆分成个体)
    • 函数签名:def flatMap[U:ClassTag](f:T => TraversableOnce[U]):RDD[U]
    val rdd1 = sc.makeRDD(List(List(1,2),List(3,4)))
    val flatRDD1 = rdd1.flatMap(
        list => {
            list
        }
    )
    flatRDD1.collect().foreach(println)
    /*
    1
    2
    3
    4
    */
    
    val rdd2 = sc.makeRDD(List("hello world","hello scala"))
    val flatRDD2 = rdd2.flatMap(
        s => {
            s.split(" ")
        }
    )
    flatRDD2.collect().foreach(println)
    /*
    hello
    world
    hello
    scala
    */
    
  • groupBy

    • 作用:将数据根据指定的规则进行分组,分区默认不变,但是数据会被打乱重新组合,这样的操作称之为Shuffle。极限情况下,数据可能被分在同一个分区中
    • 一个组的数据在一个分区中,但并不是说一个分区中只有一个组
    val rdd = sc.makeRDD(List(1,2,3,4),2)
    val gpRDD = rdd.groupBy(_ % 2)
    gpRDD.collect().foreach(println)
    /*
    (0,CompactBuffer(2, 4))
    (1,CompactBuffer(1, 3))
    */
    
    val rdd2 = sc.makeRDD(List("lcj","acx","hadoop","hive"))
    val gp2RDD = rdd2.groupBy(_.charAt(0))
    gp2RDD.collect().foreach(println)
    /*
    (h,CompactBuffer(hadoop, hive))
    (a,CompactBuffer(acx))
    (l,CompactBuffer(lcj))
    */
    
  • filter

    • 作用:将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃。当数据进行筛选过滤后,分区不变,但分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜
    • 函数签名:def filter(f:T => Boolean):RDD[T]
        val rdd = sc.makeRDD(List(1,2,3,4))
        val filterRDD = rdd.filter(_ % 2 != 0)
    
3.1.2 双value类型
  • reduceByKey

    • 作用:将数据按照相同的key对value进行聚合
    • 函数签名

    def reduceByKey(func:(V,V) => V):RDD[(K,V)]

    def reduceByKey(func:(V,V) => V, numPartitions:Int):RDD[(K,V)]

    val rdd = sc.makeRDD(List(
        ("a",1),("a",9),("b",5),("a",5)
    ))
    
    val reduceRDD = rdd.reduceByKey((x, y) => {x + y})
    
    • 注意:reduceByKey中如果key的数据只有一个,是不会参与运算的
  • groupByKey

    • 作用:将分区的数据直接转换为相同类型的内存数组进行后续处理
    • 函数签名:

    def groupByKey(): RDD[(K,Iterable[V])]

    def groupByKey(numPartitions:Int):RDD[(K,Iterable[V])]

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

  • reduceByKey和groupByKey区别

    • groupByKey会导致数据打乱重组,存在shuffle操作。
      • 在Spark中,shuffle操作必须落盘处理,不能再内存中数据等待,否则可能会导致内存溢出。Shuffle操作的性能非常低
    • reduceByKey在分区内进行预聚合操作,有效减少shuffle落盘时的数据量。
    • 从Shuffle的角度: reduceByKey和 groupByKey都存在 shuffle 的操作,但是reduceByKey可以在 shuffle前对分区内相同key的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而groupByKey只是进行分组,不存在数据量减少的问题,reduceByKey性能高。
    • 从功能的角度:reduceByKey其实包含分组和聚合的功能。GroupByKey只能分组,不能聚合,所以在分组聚合的场合下,推荐使用reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用groupByKey
3.2 RDD行动算子
  • count

    • 作用:返回RDD中元素的个数
    • 函数签名:def count():Long
    val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 10))
    println(rdd.count())
    
    // 6
    
  • collect

    • 作用:在driver program中,以数组array的形式返回数据集中的所有元素
    • 函数签名:def collect():Array[T]
    val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 10))
    val ints = rdd.collect()
    
    println(ints.mkString(","))
    
    // 1,2,3,4,5,10
    
  • take

    • 作用:返回一个由RDD的前n个元素组成的数组
    • 函数签名:def take(num:Int):Array[T]
    val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 10))
    val ints = rdd.take(3)
    
    println(ints.mkString(","))
    // 1,2,3
    
  • reduce

    • 作用:聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据
    • 函数签名:def reduce(f: (T, T) => T): T
    val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 10))
    val result = rdd.reduce(_ + _)
    
    println(result)
    // 25
    
  • foreach

    • 作用:分布式遍历RDD中的每一个元素,调用指定函数
    • 函数签名:

    def foreach(f: T => Unit): Unit = withScope {

    ​ val cleanF = sc.clean(f)

    ​ sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
    }

    val rdd = sc.makeRDD(List(1, 2, 3, 4, 5, 10))
    // 收集后打印
    rdd.map(num=>num).collect().foreach(println)
    println("--------------------")
    // 分布式打印
    rdd.foreach(println)
    
    /*
    1
    2
    3
    4
    5
    10
    --------------------
    2
    3
    5
    4
    10
    1
    */
    

4. RDD持久化和分区

4.1 RDD持久化
  • RDD惰性机制

    • 对于转换算子的操作,实际上是在对RDD做逻辑封装,给它添加不同的功能,但事实上RDD并没有执行计算;只有当执行到行动算子的操作时,RDD才会真正的触发计算,从头到尾执行。

    • RDD中是不存储数据的,如果一个RDD重复使用,则需要再次从头执行来获取数据,RDD对象是可以重用的,但是数据是不能重用的

  • RDD持久化

    • RDD可以通过cache()或persist()方法将前面的计算结果缓存,默认情况下会把数据缓存在JVM堆内存中,但并不是调用方法时立即缓存,而是触发后面的action算子时,该RDD会被缓存在计算节点的内存中以供重用。
    rdd.cache()
    // cache()方法底层是调用了persist(MEMORY_ONLY)
    rdd.persist(StorageLevel.)
    // persist()方法可以修改存储级别,见下表
    
    级别使用的空间CPU时间是否在内存中是否在磁盘上备注
    MEMORY_ONLY
    MEMORY_ONLY_SER
    MEMORY_AND_DISK中等部分部分如果数据在内存中放不下,则溢写到磁盘上
    MEMORY_AND_DISK_SER部分部分如果数据在内存中放不下,则溢写到磁盘上。在内存中存放序列化后的数据
    DISK_ONLY
    • Spark会自动对一些Shuffle操作的中间数据做持久化操作(例如:reduceByKey),这样做的目的是为了当一个节点Shuffle失败了避免重新计算整个输入。但是,在实际使用的时候,如果想要重用数据,仍然建议调用persist或cache
4.2 RDD分区
  • 分区作用:
    • 增加程序的并行度实现分布式的计算
    • 减少通信开销
  • 分区原则:分区个数 = 集群中CPU核心数目
  • 分区方法:在sparkConf中设置spark.default.parallelism属性(见1.创建Spark连接中代码)
sc.textFile(filepath,partitionNum)
// 通过设置partitionNum来设置RDD分区个数

val rdd = data.repartition()
// 可以使用repartition()来对rdd重新分区
  • 分区类型:

    注意:只有key-value类型的数据支持分区器!

    • HashPartitioner 哈希分区
    • RangePartitioner 区域分区
    • 自定义分区
  • 自定义分区方法

    • 定义Partitioner类
    • 继承自org.apache.spark.Partitioner
    • numPartitions:Int 表示要返回创建出来的分区个数。分区过程中要覆盖掉。
    • getPartition(key:Any):Int 返回给定键的分区编号(0到numPartitions-1)。它会指定返回属于哪个分区,比如输入一个key,它会返回这个key所对应的分区是哪个分区。因为在进行操作的时候,应该是(key,value)一个键值对一个键值对往里扔,这个键值对扔过去,它属于哪个分区?所以它会返回一个分区的编号,这叫getpartition()
    • equals() Java判断相对性的标准方法
// 自定义分区类
import org.apache.spark.{Partitioner,SparkContext,SparkConf}

class MyPartitioner(numParts:Int) extends Partitioner {
  override def numPartitions: Int = numParts

  override def getPartition(key: Any): Int = {
    key.toString.toInt % 10
  }

}
// 使用自定义分区测试

//构建原始RDD
val dataRDD = sc.makeRDD(1 to 10,5)

// 使用自定义分区方法分区
// 由于只有key-value类型数据有分区器,需要先将数据转换为键值对类型
val PartitionRDD = dataRDD.map((_, 1)).partitionBy(
    new MyPartitioner(10)
)

// 将数据还原
val MapRDD = PartitionRDD.map(_._1)

//存储为文件
MapRDD.saveAsTextFile("output2")

5. key-value RDD

// 方式一、从文件中读取数据创建k-vRDD
val lines = sc.textFile(filepath)
val pairRDD = lines.flatMap(_.split(" ")).map((_,1))

// 方式二、通过并行集合创建k-vRDD
val rdd = sc.makeRDD(List("Hadoop","Hive","Scala","Spark"))
val pairRDD = rdd.map((_,1))
  • 总结:先对数据进行处理(如果需要)(如对以行读入的数据做扁平化操作等),最后对单个数据做map(_,1)操作,使数据变为(key,1)的key-value键值对

  • 键值对RDD的transform操作

    • reduceByKey(func):使用func函数合并具有相同键的值
    pairRDD.reduceByKey(_+_).foreach(println)
    
    (Scala,1)
    (Hive,1)
    (Spark,2)
    (Hadoop,1)
    
    • groupByKey():对具有相同键的值进行分组
    pairRDD.groupByKey().foreach(println)
    
    (Hive,CompactBuffer(1))
    (Scala,CompactBuffer(1))
    (Spark,CompactBuffer(1, 1))
    (Hadoop,CompactBuffer(1))
    

    reduceByKey和groupByKey的区别见3.RDD算子中解释。

    pairRDD.groupByKey().map(t => (t._1,t._2.sum))
    
    pairRDD.reduceByKey(_+_)
    
    // 这时候这两个表达式是等效的
    
    • keys/values:把pair RDD中的key/value返回形成一个新的RDD
    scala> val keyRDD = pairRDD.keys
    scala> keyRDD.collect().foreach(println)
    Hadoop
    Hive
    Scala
    Spark
    
    scala> val valueRDD = pairRDD.values
    scala> valueRDD.collect().foreach(println)
    1
    1
    1
    1
    
    • sortByKey():返回一个根据键值排序的RDD
    // 默认为升序排列
    scala> pairRDD.sortByKey().foreach(println)
    (Spark,1)
    (Hadoop,1)
    (Scala,1)
    (Hive,1)
    // 传入参数false为降序(默认true)
    scala> pairRDD.sortByKey(false).foreach(println)
    (Scala,1)
    (Hadoop,1)
    (Spark,1)
    (Hive,1)
    
    • sortBy()与sortByKey()区别
      • sortByKey()只能根据键值来排序,但sortBy()可以根据特定的需求完成排序,如根据value值排序
    // 需求 分别根据Key 和 value排序
    scala> val rdd1 = sc.makeRDD(List(("c",8),("b",25),("c",17),("a",42),("b",4),("d",9),("e",17),("c",2),("f",29),("g",21),("b",9)))
    
    // 根据key降序排列
    scala> rdd1.reduceByKey(_+_).sortByKey(false).collect().foreach(print)
    (g,21)(f,29)(e,17)(d,9)(c,27)(b,38)(a,42)
    
    // 根据value降序排列
    scala> rdd1.reduceByKey(_+_).sortBy(_._2,false).collect().foreach(print)
    (a,42)(b,38)(f,29)(c,27)(g,21)(e,17)(d,9)
    
    • mapValues(func):对键值对中的每个value都应用一个函数,key值不会发生变化
    scala> pairRDD.mapValues(_+1).collect().foreach(print)
    (Hadoop,2)(Hive,2)(Scala,2)(Spark,2)
    
    • join:把几个RDD中元素key相同的进行连接
    scala> val pairRDD1 = sc.makeRDD(List(("Spark",1),("Spark",2),("Flink",3),("Hadoop",1)))
    scala> val pairRDD2 = sc.makeRDD(List(("Spark","Fast")))
    
    scala> pairRDD1.join(pairRDD2).collect().foreach(println)
    (Spark,(1,Fast))
    (Spark,(2,Fast))
    

6. RDD读写

6.1 本地文件系统数据读写
  • 从文件中读取数据创建RDD
    • 注意RDD的惰性机制,即使输入了错误的语句(路径不存在),spark-shell也不会立即报错
// textFile默认path路径是从当前项目根目录算起,可以使用绝对路径也可以使用相对路径
// 1. 绝对路径
val rdd = sc.textFile("D:\\Documents\\Projects\\JavaProjects\\spark\\src\\datas\\file1.txt")

// 2. 相对路径
val rdd = sc.textFile("src\\datas\\file1.txt")

// 3. 使用通配符*
val rdd = sc.textFile("src\\datas\\*.txt")

// 4. 也可以读取目录(也即读取目录下所有文件)
val rdd = sc.textFile("src\\datas")
  • 把RDD写入到文本文件中去

注意:saveAsTextFile是将RDD输出一个指定文件夹而非文件,且文件夹必须不存在!

scala> pairRDD.saveAsTextFile("D:\\Documents\\output")

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xbUmfqet-1624255631064)(C:\Users\Lee\AppData\Roaming\Typora\typora-user-images\image-20210620161719423.png)]

  • 再次把数据加载到RDD中

再次加载时,是将保存文件夹下的所有文件都读取加载进来生成RDD

scala> val textFile = sc.textFile("D:\\Documents\\output")
6.2 分布式文件系统HDFS的数据读写
  • 从HDFS与本地文件系统读取文件的语句是类似的,只需要将路径更换为HDFS路径即可
// 以下三条语句是等价的
val textFile = sc.textFile("hdfs://localhost:9000/usr/hadoop/word.txt")

val textFile = sc.textFile("/usr/hadoop/word.txt")

val textFile = sc.textFile("word.txt")
  • 把RDD中数据保存到HDFS文件中
textFile.saveAsTextFile("writeback")

textFile.saveAsTextFile("/usr/hadoop/writeback")
6.3 JSON文件数据读写
  • 与读取文本文件类似,json文件也是以行为单位读入,每一行是一个元素
val jsonStr = sc.textFile("filepath")
  • 解析JSON文件——JSON.parseFull()方法
    • Scala当中有个自带的json库,叫scala.util.parsing.json.JSON,可以利用这个自带库,对json文本文件进行解析。那这里面解析用的是JSON.parseFull这样一个方法,这个方法括号里传递的就是json文本中一行行的文本。第一行是{name: michael},把这行文本传到JSON.parseFull里面去,就会把name,age的字段解析出来。这就是它的功能。但是要注意的是,有些数据符合格式,有些数据可能不是json格式,那么就有可能解析成功或者解析失败。那对于parseFull这个函数,当解析成功的时候它会把解析的结果封装成some对象返回,如果解析失败会返回none。
val inputFile = "filepath"
val conf = new SparkConf().setMaster("local[*]").setAppName("json")
val sc = new SparkContext(conf)

val jsonFile = sc.textFile(inputFile)
val result = jsonFile.map(s => JSON.parseFull(s))
// 打印RDD
result.foreach({r => r match{
    //解析成功
    case Some(map:Map[String,Any]) => println(map)
    //解析失败
    case None => println("Parsing Failed")
    //无法识别
    case other => println("Unkonwn data structure:" + other)
}})
  • spark-submit命令

    首先用打包编译的命令去打包,编译之后得到jar包,然后再用spark-submit命令去运行。/usr/local/spark/bin/spark-submit 用这个命令提交,提交给参数,–class告诉我们用的程序的类,这里是JSONRead的类,后面是打包程序jar包的地址,这样回车之后,就得到执行结果,打印输出,第一个结果map(name->michael)第二个是map(name->andy,age->30.0)第三个是map(name->justin,age->19.0)这就是对于json文件相关的解析完整过程。

/usr/local/spark/bin/spark-submit \
--class "ClassName" \
/usr/local/spark/targetpath/targetName.jar

foreach({r => r match{
//解析成功
case Some(map:Map[String,Any]) => println(map)
//解析失败
case None => println(“Parsing Failed”)
//无法识别
case other => println(“Unkonwn data structure:” + other)
}})


- spark-submit命令

  首先用打包编译的命令去打包,编译之后得到jar包,然后再用spark-submit命令去运行。/usr/local/spark/bin/spark-submit 用这个命令提交,提交给参数,--class告诉我们用的程序的类,这里是JSONRead的类,后面是打包程序jar包的地址,这样回车之后,就得到执行结果,打印输出,第一个结果map(name->michael)第二个是map(name->andy,age->30.0)第三个是map(name->justin,age->19.0)这就是对于json文件相关的解析完整过程。

/usr/local/spark/bin/spark-submit
–class “ClassName”
/usr/local/spark/targetpath/targetName.jar


  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值