spark core编程

目录

一、实验目的

二、实验平台

三、实验内容和要求

四、实验过程记录

1、Spark RDD实现单词计数

2、Spark RDD 实现分组求TopN 

2.1实现思路

3、Spark RDD 实现二次排序

3.1实现思路

3.2编写程序

 4、Spark RDD 计算平均成绩 (见课本124)

 4.1实现思路

 4.2完整代码

​ 5、spark RDD 倒排索引统计每日新增用户(见课本126)

5.1实现思路

5.2完整代码

6、案例分析:Spark RDD数据倾斜问题解决


一、实验目的

(1)熟悉 Spark 的 RDD 基本操作及键值对操作;

(2)熟悉使用 RDD 编程解决实际具体问题的方法

二、实验平台

操作系统:window 10

三、实验内容和要求

1、Spark RDD实现单词计数(见课本107)

2、Spark RDD 实现分组求TopN (见课本116)

3、Spark RDD 实现二次排序 (见课本120)

4、Spark RDD 计算平均成绩 (见课本124)

5、Spark RDD 倒排索引统计每日新增用户(见课本126)

6、Spark RDD 数据倾斜问题(见课本143)

以上的课本是指《Spark大数据分析实战》--张伟洋

四、实验过程记录

1、Spark RDD实现单词计数

在File->new->Project中新建项目

spark core编程_第1张图片

 spark core编程_第2张图片

spark core编程_第3张图片

导入pom依赖并刷新下载相关依赖包:



  4.0.0

  cn.hgu.spark
  demo
  1.0-SNAPSHOT

  
    1.8
    1.8
    2.11.8
    2.2.3
    2.7.5
    UTF-8
  
  
    
    
      org.scala-lang
      scala-library
      2.11.8
    
    
    
      org.apache.spark
      spark-core_2.11
      ${spark.version}
    
    
      org.apache.spark
      spark-sql_2.11
      ${spark.version}
    

    
    
      org.apache.hadoop
      hadoop-client
      ${hadoop.version}
    
    
      junit
      junit
      4.10
      provided
    
    
      mysql
      mysql-connector-java
      5.1.47
    

  
  
    
      
        
        
          net.alchim31.maven
          scala-maven-plugin
          3.2.2
        
        
        
          org.apache.maven.plugins
          maven-compiler-plugin
          3.5.1
        
      
    
    
      
        net.alchim31.maven
        scala-maven-plugin
        
          
            scala-compile-first
            process-resources
            
              add-source
              compile
            
          
          
            scala-test-compile
            process-test-resources
            
              testCompile
            
          
        
      

      
        org.apache.maven.plugins
        maven-compiler-plugin
        
          
            compile
            
              compile
            
          
        
      


      
      
        org.apache.maven.plugins
        maven-shade-plugin
        2.4.3
        
          
            package
            
              shade
            
            
              
                
                  *:*
                  
                    META-INF/*.SF
                    META-INF/*.DSA
                    META-INF/*.RSA
                  
                
              
            
          
        
      
    
  

新建一个dataset文件夹,在dataset目录下新建words.txt文件,并向其写入以下单词内容(单词之间以空格分隔),命令如下:

spark core编程_第4张图片

 编写WordCount程序,本地模式运行程序

spark core编程_第5张图片

 spark core编程_第6张图片

WordCount.scala源码

package org.example

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

/**
 * Spark RDD单词计数程序
 */
object WordCount {
  def main(args: Array[String]): Unit = {
    /**
     * 本地模式local
     */
    // 1创建sparkConf 设置AppName 以及 master
    val conf: SparkConf = new SparkConf().setMaster("local[3]").setAppName("WordCount")
    // 2 创建sparkContext 提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)
    val rdd: RDD[String] = sc.textFile("dataset/words.txt")

    //将RDD的每个元素按照空格进行拆分并将结果合并为一个新的RDD
    val rdd_split: RDD[String] = rdd.flatMap(x => x.split(" "))
    //将RDD中的每个单词和数字1放到一个元组里,即(word,1)
    val word_rdd: RDD[(String, Int)] = rdd_split.map(x=>(x,1))
    //对单词根据Key进行聚合,对相同的key进行value的累加
    val wordcountRdd: RDD[(String, Int)] = word_rdd.reduceByKey((x,y)=>x+y)
    //按照单词数量降序排列
    val WordsSortRDD: RDD[(String, Int)] = wordcountRdd.sortBy(_._2, false)
    //保存结果到指定的路径(取程序执行时传入的第二个参数)
    WordsSortRDD.saveAsTextFile("dataset/b")
    WordsSortRDD.collect().foreach(println)//开始执行
    //4 关闭sc
    sc.stop()
  }
}

运行结果:

spark core编程_第7张图片

2、Spark RDD 实现分组求TopN 

分组求 TopN 是大数据领域常见的需求,主要是根据数据的某一列进行分组,然后将分组后的每一组数据按照指定的列进行排序,最后取每一组的前N 行数据。

2.1实现思路

使用 Spark RDD groupByKey() 算子可以对 (key,value) 形式的 RD按照key 进行分组, key 相同的元素的 value 将聚合到一起,形成 (key,value-list),将 value-list 中的元素降序排列取前 N 个即可。
例如,有以下学生成绩数据:
spark core编程_第8张图片
数据成绩
Andy,98
Jack,87
Bill,99
Andy,78
Jack,85
Bill,86
Andy,90
Jack,88
Bill,76
Andy,58
Jack,67
Bill,79

源代码:

package org.example

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

/**
 * spark分组取TopN程序
 */
object RDDGroupTopN {
  def main(args: Array[String]): Unit = {
    // 1创建sparkConf对象,存储应用程序的配置信息
    // 设置集群master节点访问地址,此处为本地模式
    //setAppName设置应用程序名称为RDDGroupTopN,可以在Spark WebUI中显示
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RDDGroupTopN")
    // 2 创建sparkContext 提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)
    //1、加载本地数据
    val linesRDD :RDD[String] = sc.textFile("dataset/scores.txt")
    //2、利用map()算子将RDD元素转为(String,Int)即(姓名,成绩)形式的元组
    val tupleRDD:RDD[(String,Int)] = linesRDD.map(line=>{
      val name: String = line.split(",")(0)
      val score: String = line.split(",")(1)
      (name,score.toInt)
    })
    //3、使用groupByKey()算子将tupleRDD按照key(姓名)进行分组,姓名相同的所有成绩数据将聚合到一起
    //使用map()算子将分组后的每一组成绩数据降序排列后取前三个
    val top3 = tupleRDD.groupByKey().map(groupedData=>{
      val name: String = groupedData._1 //姓名
      //每一组的成绩降序后取前三个,groupedData._2是成绩集合,sortWith(_>_).take(3)降序取三个
      val scoreTop3: List[Int] = groupedData._2.toList.sortWith(_>_).take(3)
      (name,scoreTop3)//返回元组
    })

    //4、循环打印分组结果
    //使用foreach()算子循环将结果RDD打印到控制台
    top3.foreach(tuple=>{
      println("姓名:"+tuple._1)
      val tupleValue= tuple._2.iterator
      while(tupleValue.hasNext){
        val value = tupleValue.next()
        println("成绩:"+value)
      }
      println("*************************")
    })
  }

}

 输出结果格式跟预期输出结果不太一样

spark core编程_第9张图片

 同一个学生有多门成绩,现需要计算每个学生分数最高的前3个成绩,期望的输出结果如下:

姓名: Andy
成绩: 98
成绩: 90
成绩: 78
*******************
姓名: Bill
成绩: 99
成绩: 86
成绩: 79
*******************
姓名: Jack
成绩: 88
成绩: 87
成绩: 85
*******************

3、Spark RDD 实现二次排序

二次排序是指对需要排序的元素首先按照第一个字段进行排序, 若第一个字段相等,则按照第二个字段排序。
例如,文件 sort.txt 中有以下内容:
6 7
5 8
2 9
7 5
4 3
8 3
2 7
6 1
首先按照第一个字段升序排列,若第一个字段相等,则按照第二个字段降序排列,期望的输出结果如下:
2 9
2 7
4 3
5 8
6 7
6 1
7 5
8 3

3.1实现思路

使用 Spark 提供的 sortByKey() 算子 sortBy() 算子只能根据单个字段进行排序,若需要根据多个字段排序,则可将需要排序的元素组成(key,value)形式的元组, key 是一个自定义比较类(类似 Java 中实现Comparator接口的类),该类中存储需要排序的多个字段, value 是每行的数据。当执行计算时,仍然可以使用sortByKey() 算子根据 key 进行排序,但是需要在自定义比较类中重写排序规则。
Scala中自定义比较类需要实现排序特质 Ordered 序列化特质 Serializable Ordered主要用于对具有单个自然顺序的数据(比如整数)进行排序。

3.2编写程序

首先新建一个比较类 SecondSortKey ,该类的主构造器中需要定义两个比较参数,然后继承特质Ordered Serializable ,代码如下:
class SecondSortKey(val first:Int,val second:Int) extends Ordered[SecondSortKey] with Serializable{

}
在特质 Ordered 中定义了一个未实现的 compare() 方法,需要子类实现。因此,需要在比较类SecondSortKey 中添加 compare() 方法并将其实现,compare() 方法需要传入当前比较类 SecondSortKey 的实例作为参数,代码如下:
/**
   * 实现compare()方法
   */
  override def compare(that:SecondSortKey):Int = {
    //若第一个字段不相等,则按照第一个字段升序排列
    if(this.first-that.first!=0){
      this.first-that.first
    }else{//否则按照第二个字段降序排列
      that.second-this.second
    }
  }
上述代码的比较规则为:若第一个字段不相等,则按照第一个字段升序排列;否则按照第二个字段降序排列。接下来新建一个程序运行主类,并在该类中添加main() 方法

完整代码

package org.example

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

/**
 * 二次排序自定义key类
 *
 * @param first 每一行的第一个字段
 * @param second 每一行的第二个字段
 */
class SecondSortKey(val first:Int,val second:Int) extends Ordered[SecondSortKey] with Serializable{
  /**
   * 实现compare()方法
   */
  override def compare(that:SecondSortKey):Int = {
    //若第一个字段不相等,则按照第一个字段升序排列
    if(this.first-that.first!=0){
      this.first-that.first
    }else{//否则按照第二个字段降序排列
      that.second-this.second
    }
  }
}

/**
 * 二次排序运行主类
 */
object SecondSort {
  def main(args:Array[String]):Unit={
    //创建SparkConf对象
    val conf: SparkConf = new SparkConf().setMaster("local").setAppName("Spark-SecondSort")
    //创建SparkContext对象,该对象是提交Spark应用程序的入口
    val sc = new SparkContext(conf)

    //读取指定路径的文件内容,生产一个RDD集合
    val lines :RDD[String] = sc.textFile("dataset/sort.txt")
    //将RDD中的元素转为(SecondSortKey,String)形式的元组
    /**
     * 使用map()算子将lines RDD中的元素转为(SecondSortKey, String)形式
     * 的元组,便于后续根据SecondSortKey对象进行排序
     */
    val pair: RDD[(SecondSortKey, String)] = lines.map(line => (
      new SecondSortKey(line.split(" ")(0).toInt, line.split(" ")(1).toInt), line)
    )
    //3、按照元组的key(SecondSortKey的实例)进行排序
    /**
     * 使用sortByKey()算子对pair RDD按照key(SecondSortKey对象) 
     * 进行排序(当执行排序时,会使用SecondSortKey类中的compare()方法 定义的排序规则,
     * 若使用sortByKey(false),则将按照第一个字段降 序、第二个字段升序排列),
     * 并使用map()函数取排序后的元组中的第二个值(value值)作为最终结果
     */
    var pairSort: RDD[(SecondSortKey, String)] = pair.sortByKey()
    //取排序后的元组中的第二个值(value值)
    val result: RDD[String] = pairSort.map(line => line._2)
    //打印最终结果,最后使用foreach()算子循环打印最终结果到控制台
    result.foreach(line=>println(line))
  }
}
输出结果与预期输出结果一致。
spark core编程_第10张图片

 4、Spark RDD 计算平均成绩 (见课本124)

本例通过对输入文件中的学生 3 科成绩进行计算,得出每个学生的平均成绩。输入文件中的每行内容均为一个学生的姓名和其相应的成绩,每门学科为一个文件。要求输出结果中每行有两列数据,其中第一列代表学生的姓名,第二列代表其平均成绩。输入的3 个文件内容如下:
math.txt 文件内容如下:
张三 88
李四 99
王五 66
赵六 77

 chinese.txt文件内容如下:

张三 78
李四 89
王五 96
赵六 67

english.txt文件内容如下:

张三 80
李四 82
王五 84
赵六 86

期望输出结果如下:

张三 82
李四 90
王五 82
赵六 76 

 4.1实现思路

Spark RDD groupByKey() 算子可以根据 key 进行分组,因此可以将学生的姓名作为key ,成绩作为 value 。分组后,对 value 进行聚合求平均分即可。

 4.2完整代码

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

/**
 * 求成绩平均分
 */
object AverageScore {
  def main(args: Array[String]): Unit = {
    //创建SparkConf对象,存储应用程序的配置信息
    val conf = new SparkConf()
    //设置应用程序名称,可以在Spark WebUI中显示
    conf.setAppName("AverageScore")
    //设置集群Master节点访问地址
    conf.setMaster("local[3]")

    val sc = new SparkContext(conf)
    //1. 加载数据
    val linesRDD: RDD[String] = sc.textFile("dataset/input")
    //2. 将RDD中的元素转为(key,value)形式,便于后面进行聚合
    val tupleRDD: RDD[(String, Int)] = linesRDD.map(line => {
      val name = line.split(" ")(0)//姓名
      val score = line.split(" ")(1).toInt//成绩
      (name, score)
    })
    //3. 根据姓名进行分组,形成新的RDD
    val groupedRDD: RDD[(String, Iterable[Int])] = tupleRDD.groupByKey()
    //4. 迭代计算RDD中每个学生的平均分
    val resultRDD: RDD[(String, Int)] = groupedRDD.map(line => {
      val name = line._1//姓名
      val iteratorScore: Iterator[Int] = line._2.iterator//成绩迭代器
      var sum = 0//总分
      var count = 0//科目数量

      //迭代累加所有科目成绩
      while (iteratorScore.hasNext) {
        val score = iteratorScore.next()
        sum += score
        count += 1
      }
      //计算平均分
      val averageScore = sum / count
      (name, averageScore)//返回(姓名,平均分)形式的元组
    })
    //保存结果
    resultRDD.saveAsTextFile("dataset/output")
    resultRDD.collect().foreach(println)//开始执行
  }
}


输出结果与预期结果一致:

spark core编程_第11张图片 5、spark RDD 倒排索引统计每日新增用户(见课本126)

在搜索引擎的索引库中,每个文档(网页)都对应一系列关键词。当用户在搜索框中搜索指定的关键词时,为了加快搜索速度,搜索引擎通常会建立一系列索引,将“ 文档 - 关键词 的映射关系转换为倒排索引,即“ 关键词 - 文档 ”。这样可以快速根据某个关键词查询出所对应的文档,而不需要从所有文档中查找包含的关键词。 关键词与文档 的映射关系如表所示:

文档01

文档02

文档03

文档04

关键词1

关键词2

关键词3

关键词4

表中,纵向维度表示每个文档包含哪些关键词,横向维度表示 每个关键词存在于哪些文档中。搜索引擎则是使用倒排索引实现了“关键词-文档”映射关系的数据结构。

接下来讲解使用倒排索引实现统计某网站每日新增的用户数量。 已知有以下用户访问历史数据,第一列为用户访问网站的日期,第二列为用户名:

2020-01-01,user1
2020-01-01,user2
2020-01-01,user3
2020-01-02,user1
2020-01-02,user2
2020-01-02,user4
2020-01-03,user2
2020-01-03,user5
2020-01-03,user6
现需要根据上述数据统计每日新增的用户数量,期望的统计结果为:
2020-01-01,3
2020-01-02,1
2020-01-03,2

 即2020-01-01新增了3个用户(分别为user1user2user3), 2020-01-02新增了1个用户(user4),2020-01-03新增了两个用户(分别为user5user6)。

5.1实现思路

若将用户名看作关键词,访问日期看作文档 ID ,则用户名与访问日期的映射关系如下表所示。

2020-01-01

2020-01-02

2020-01-03

user1

user2

user3

user4

user5

user6

 若同一个用户对应多个访问日期,则最小的日期为该用户的注册日期,即新增日期,其他日期为重复访问日期,不应统计在内。因此每个用户应该只计算用户访问的最小日期即可。如下表所示,将每个用户访问的最小日期都移到第一列,第一列为有效数据,只统计第一列中每个日期的出现次数,即为对应日期的新增用户数。

5.2完整代码

package org.example

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

/**
  * Spark RDD统计每日新增用户
  */
object DayNewUser {
   def main(args: Array[String]): Unit = {
      val conf = new SparkConf()
      conf.setAppName("DayNewUser")
      conf.setMaster("local[*]")

      val sc = new SparkContext(conf)
      //1. 构建测试数据
      val tupleRDD:RDD[(String,String)] = sc.parallelize(
         Array(
            ("2020-01-01", "user1"),
            ("2020-01-01", "user2"),
            ("2020-01-01", "user3"),
            ("2020-01-02", "user1"),
            ("2020-01-02", "user2"),
            ("2020-01-02", "user4"),
            ("2020-01-03", "user2"),
            ("2020-01-03", "user5"),
            ("2020-01-03", "user6")
         )
      )
      //2. 倒排(互换RDD中元组的元素顺序)
      /**
       * 使用map()算子将(key,value)形式的RDD的每一个元素互换顺序,
       * 即(value,key),以便后续按照用户名进行分组。
       */
      val tupleRDD2:RDD[(String,String)] = tupleRDD.map(
      line => (line._2, line._1)
      )
      //3. 将倒排后的RDD按照key分组
      /**
       * 使用groupByKey()算子将倒排后的RDD按照key进行分组,
       * 即按照用户名分组,同一用户的所有访问日期将聚合到一起。
       */
      val groupedRDD: RDD[(String, Iterable[String])] = tupleRDD2.groupByKey()
      //4. 取分组后的每个日期集合中的最小日期,并计数为1
      val dateRDD:RDD[(String,Int)] = groupedRDD.map(
         line => (line._2.min, 1)
      )
      //5. 计算所有相同key(即日期)的数量
      /**
       *用 countByKey() 算 子 计 算 相 同 key 的 数 量 。
       *与reduceByKey()算子不同的是:reduceByKey()算子属于转化算子,需要传入聚合函数,
       *根据聚合逻辑进行聚合,返回RDD类型的分布式数 据;countByKey()算子属于行动算子,
       *不需要传入聚合函数,会直接对RDD中的数据按照key进行统计,计算相同key的数量,返回Map类型的结果数据(直接返回到Driver端)。
       *若不使用countByKey()算子,而是直接使用reduceByKey()算子,要达到相同的效果,则可以使用以下代码代替
       *dateRDD.reduceByKey(_+_).collectAsMap()
       */
      val resultMap: collection.Map[String, Long] = dateRDD.countByKey()
      //将结果Map循环打印到控制台
      resultMap.foreach(println)

   }

}
注意,只有当结果 Map 很小时,才可以使用 countByKey() 算子,因为整个Map 集合都会被加载到 Driver 端的内存中。要处理非常大的结果,可以考虑使用rdd.mapValues(_=>1L).reduceByKey(_+_),它返回一个RDD[T, Long] ,而不是直接返回一个 Map。
运行结果与预期结果一致:
spark core编程_第12张图片

6、案例分析:Spark RDD数据倾斜问题解决

        我们已经知道,一个Spark 应用程序会根据其内部的 Action 操作划分成多个作业(Job ),每个作业内部又会根据 Shuffle 操作划分成多个Stage,每个 Stage 由多个 Task 任务并行进行计算,每个 Task 任务只计算一个分区的数据。
        假设某个Spark 作业有两个 Stage ,分别为 Stage0 Stage1 ,那么Stage1必须等待 Stage0 计算完成才能开始。
        如果Stage0 内部分配了 n 个Task任务进行计算,其中有 1 Task 任务负责计算的分区数据量过大,执行了1 个小时还没结束,其余的 n-1 Task 任务在半小时内就执行完毕了,那么其余的所有Task 任务都需要等待这最后 1 Task 任务执行结 束后才能进入下一个 Stage 。这种由于某个 Task任务计算的数据量过大导致拖慢整个Spark 作业的完成时间的现象就是数据倾斜
数据倾斜是大数据计算中很常见的问题,例如 WordCount 单词计数中,使用reduceByKey() 算子进行聚合时,必须将各个节点上相同的key拉取到同一个节点上的同一个分区中进行处理,此时如果某个 key对应的数据量特别大,就会产生数据倾斜。
spark core编程_第13张图片

 现需要对上述内容进行单词计数统计,若单词hello的数量非常大,则Spark计算时将发生数据倾斜。使用随机key进行双重聚合的方式解决数据倾斜问题,完整代码:

data.txt内容
hello hello hadoop spark hello hello hello hello hello hello
hello hello
hello scala i love spark hello hello hello hello hello hello
hello hello
hello hello hadoop spark hello hello hello hello hello hello
hello hello
hello hello java spark hello hello hello hello hello hello
hello hello
hello hello python spark hello hello hello hello hello hello
hello hello
hello hello hello hello hello hello hello hello hello hello
hello hello
hello hello hello hello hello hello hello hello hello hello
hello hello
hello hello hello hello hello hello hello hello hello hello
hello hello
hello hello hello hello hello hello hello hello hello hello
hello hello
完整代码如下:
package org.example

import org.apache.spark.{SparkConf, SparkContext}
import scala.util.Random

/**
  * Spark RDD解决数据倾斜案例
  */
object DataLean {
   def main(args: Array[String]): Unit = {
      //创建Spark配置对象
      val conf = new SparkConf();
      conf.setAppName("DataLean")
      conf.setMaster("local");

      //创建SparkContext对象
      val sc = new SparkContext(conf);

      //1. 读取测试数据
      val linesRDD = sc.textFile("dataset/data.txt");
      //2. 统计单词数量
      linesRDD
        .flatMap(_.split(" "))
        .map((_, 1))
        .map(t => {
           val word = t._1
           val random = Random.nextInt(100)//产生0~99的随机数
           //单词加入随机数前缀,格式:(前缀_单词,数量)
           (random + "_" + word, 1)
        })
        .reduceByKey(_ + _)//局部聚合
        .map(t => {
         val word = t._1
         val count = t._2
         val w = word.split("_")(1)//去除前缀
         //单词去除随机数前缀,格式:(单词,数量)
         (w, count)
      })
        .reduceByKey(_ + _)//全局聚合
        //输出结果到指定的HDFS目录
        .saveAsTextFile("dataset/data_out")
     linesRDD.collect().foreach(println(_))
   }
}

输出结果:

spark core编程_第14张图片

你可能感兴趣的:(spark)