需求:如下图的文件中有很多访问记录,第一列表示访问站点的时间戳,第二列表示访问的站点,中间用制表符分割。这里相当于学习的不同课程,如java,ui,bigdata,android,h5等,其中每门课程又分为子课程,如h5课程分为teacher,course等。现在需要统计每门课程,学习量最高的两门子课程并降序排列。
测试数据下载地址
链接:https://pan.baidu.com/s/1DjeJkeBwfzLUl0iqF5yo8w 密码:hd35 文件为:access.txt
package com.whua
import java.net.URL
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* @author: whua
* @create: 2018/10/11 18:54
*/
object ProjectCount1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("ObjectCount1").setMaster("local[*]")
val sc = new SparkContext(conf)
//获取数据
val file: RDD[String] = sc.textFile("/Users/whua/Documents/tmpTest/access.txt")
//提取url并生成为一个元组
val urlAndOne: RDD[(String, Int)] = file.map(line => {
val fields = line.split("\t")
val url = fields(1)
(url, 1)
})
//把相同的url聚合
val sumedUrl: RDD[(String, Int)] = urlAndOne.reduceByKey(_ + _)
//获取学科信息
val project: RDD[(String, String, Int)] = sumedUrl.map(x => {
val url = x._1
val count = x._2
val project = new URL(url).getHost
(project, url, count)
})
//用学科来分组,聚合后得到结果
val ans: Array[(String, List[(String, String, Int)])] = project.groupBy(_._1)
.mapValues(_.toList.sortBy(_._3).reverse.take(2)).collect()
// val ans: Array[(String, String, Int)] = project.sortBy(_._3,false).take(3)
for (item <- ans) {
println(item)
}
sc.stop()
}
}
上面的代码是比较基础的代码,接下来在此基础上,引入缓存机制和分区器,缓存机制主要是将常用的数据缓存起来,再次调用的时候效率较高;而分区器是为了解决数据倾斜的问题,在结果生成的文件中,我么可以看到,有的文件中产生的数据量多,有的文件中产生的数据量少,分区器是解决数据倾斜的方法之一。下面我们将调用Spark自带的哈希分区器,显而易见,是采用哈希的方式来放置数据产生的位置。
package com.whua
import java.net.URL
import org.apache.spark.rdd.RDD
import org.apache.spark.{HashPartitioner, SparkConf, SparkContext}
/**
* @author: whua
* @create: 2018/10/11 20:13
*/
object ProjectCount2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("ObjectCount1").setMaster("local[*]")
val sc = new SparkContext(conf)
//获取数据
val file: RDD[String] = sc.textFile("/Users/whua/Documents/tmpTest/access.txt")
//提取url并生成为一个元组
val urlAndOne: RDD[(String, Int)] = file.map(line => {
val fields = line.split("\t")
val url = fields(1)
(url, 1)
})
//把相同的url聚合
val sumedUrl: RDD[(String, Int)] = urlAndOne.reduceByKey(_ + _)
//获取学科信息并缓存
val cachedProject: RDD[(String, (String, Int))] = sumedUrl.map(x => {
val url = x._1
val count = x._2
val project = new URL(url).getHost
(project, (url, count))
}).cache()
//调用Spark自带的分区器此时会发生哈希碰撞,需要自定义分区器
val ans: RDD[(String, (String, Int))] = cachedProject.partitionBy(new HashPartitioner(3))
ans.saveAsTextFile("/Users/whua/Documents/tmpTest/out")
sc.stop()
}
}
但是,有时候在不同的数据,不同的实际问题中,自带定分区器不一定能很好地解决数据倾斜的问题,这时候就需要自定义分区器,自定义的分区器主要是继承Partitioner抽象类,重写两个方法。这里我们根据不同的课程名来进行分区,即相同的课程名的记录写到同一个文件中。
package com.whua
import java.net.URL
import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}
import scala.collection.mutable
/**
* @author: whua
* @create: 2018/10/11 20:41
*/
object ProjectCount3 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("ObjectCount1").setMaster("local[*]")
val sc = new SparkContext(conf)
//获取数据
val file: RDD[String] = sc.textFile("/Users/whua/Documents/tmpTest/access.txt")
//提取url并生成为一个元组
val urlAndOne: RDD[(String, Int)] = file.map(line => {
val fields = line.split("\t")
val url = fields(1)
(url, 1)
})
//把相同的url聚合
val sumedUrl: RDD[(String, Int)] = urlAndOne.reduceByKey(_ + _)
//获取学科信息并缓存
val cachedProject: RDD[(String, (String, Int))] = sumedUrl.map(x => {
val url = x._1
val count = x._2
val project = new URL(url).getHost
(project, (url, count))
}).cache()
//得到所有学科
val projects: Array[String] = cachedProject.keys.distinct().collect()
//调用自定义分区器并得到分区号
val partitioner: ProjectPartitioner = new ProjectPartitioner(projects)
//分区
val partitioned: RDD[(String, (String, Int))] = cachedProject.partitionBy(partitioner)
//每个分区的数据进行排序并取top2
val ans: RDD[(String, (String, Int))] = partitioned.mapPartitions(it => {
it.toList.sortBy(_._2._2).reverse.take(2).iterator
})
ans.saveAsTextFile("/Users/whua/Documents/tmpTest/out")
sc.stop()
}
}
class ProjectPartitioner(projects: Array[String]) extends Partitioner {
//用来存放学科和分区号
private val projectsAndPartNum = new mutable.HashMap[String, Int]
//计数器,用于指定分区号
var n = 0
for (pro <- projects) {
//HashMap插入
projectsAndPartNum += (pro -> n)
n += 1
}
//得到分区数
override def numPartitions: Int = projects.length
//得到分区号
override def getPartition(key: Any): Int = {
projectsAndPartNum.getOrElse(key.toString, 0)
}
}
结果如下: