目录
一、实验目的
二、实验平台
三、实验内容和要求
四、实验过程记录
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大数据分析实战》--张伟洋
在File->new->Project中新建项目
导入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文件,并向其写入以下单词内容(单词之间以空格分隔),命令如下:
编写WordCount程序,本地模式运行程序
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()
}
}
运行结果:
数据成绩
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("*************************")
})
}
}
输出结果格式跟预期输出结果不太一样
同一个学生有多门成绩,现需要计算每个学生分数最高的前3个成绩,期望的输出结果如下:
姓名: Andy成绩: 98成绩: 90成绩: 78*******************姓名: Bill成绩: 99成绩: 86成绩: 79*******************姓名: Jack成绩: 88成绩: 87成绩: 85*******************
6 7 5 8 2 9 7 5 4 3 8 3 2 7 6 1
2 92 74 35 86 76 17 58 3
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
}
}
完整代码:
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))
}
}
张三 88李四 99王五 66赵六 77
chinese.txt文件内容如下:
张三 78李四 89王五 96赵六 67
english.txt文件内容如下:
张三 80李四 82王五 84赵六 86
期望输出结果如下:
张三 82李四 90王五 82赵六 76
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)//开始执行
}
}
文档01 |
文档02 |
文档03 |
文档04 |
|
关键词1 |
√ |
√ |
||
关键词2 |
√ |
√ |
||
关键词3 |
√ |
|||
关键词4 |
√ |
√ |
表中,纵向维度表示每个文档包含哪些关键词,横向维度表示 每个关键词存在于哪些文档中。搜索引擎则是使用倒排索引实现了“关键词-文档”映射关系的数据结构。
接下来讲解使用倒排索引实现统计某网站每日新增的用户数量。 已知有以下用户访问历史数据,第一列为用户访问网站的日期,第二列为用户名:
2020-01-01,user12020-01-01,user22020-01-01,user32020-01-02,user12020-01-02,user22020-01-02,user42020-01-03,user22020-01-03,user52020-01-03,user6
2020-01-01,32020-01-02,12020-01-03,2
即2020-01-01新增了3个用户(分别为user1、user2、user3), 2020-01-02新增了1个用户(user4),2020-01-03新增了两个用户(分别为user5、user6)。
2020-01-01 |
2020-01-02 |
2020-01-03 |
|
user1 |
√ |
√ |
|
user2 |
√ |
√ |
√ |
user3 |
√ |
||
user4 |
√ |
||
user5 |
√ |
||
user6 |
√ |
若同一个用户对应多个访问日期,则最小的日期为该用户的注册日期,即新增日期,其他日期为重复访问日期,不应统计在内。因此每个用户应该只计算用户访问的最小日期即可。如下表所示,将每个用户访问的最小日期都移到第一列,第一列为有效数据,只统计第一列中每个日期的出现次数,即为对应日期的新增用户数。
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)
}
}
现需要对上述内容进行单词计数统计,若单词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(_))
}
}
输出结果: