Spark中实现分组排序(取Top-k)的四种方法。
以求每个学科最受欢迎的老师为例,假设学科下每个老师的主页访问量的多少代表该老师的受欢迎程度。
截取log日志文件中的网址数据的格式为:
数据格式:http://学科.edu360.cn/老师
首先读取文件,切分数据,构造出RDD[((String, String), Int)格式的[(学科,老师),1]的数据
val favTeacher: SparkConf = new SparkConf().setMaster("local[2]").setAppName("FavTeacher") val context: SparkContext = new SparkContext(favTeacher) // 读取一行 // 数据格式: http://bigdata.edu360.cn/laozhang val lines: RDD[String] = context.textFile("./teacher.log") // 切分形成((学科,老师),1) val sbjectTeacher: RDD[((String, String), Int)] = lines.map(line => { val strings: Array[String] = line.split("/") val sbject: String = strings(2).split(".")(0) // 学科 val teacher = strings(3) // 老师 ((sbject, teacher), 1) })
方案1:先使用reduceByKey算子,将聚合形成((学科,老师),n),再使用groupby算子按照学科进行分组,然后使用mapValues算子用迭代器获取组内数据,将组内数据放入集合内,再调用集合上的排序方法进行排序。
// 聚合相成 ((学科,老师), n), 相同(学科,老师)仅保留一条记录 val groupTeahcerReduce: RDD[((String, String), Int)] = sbjectTeacher.reduceByKey((a, b) => { a + b }) // 按照学科进行分组,【并指定分组后RDD的分区数量】 分组条件相同的数据会在一个分组中,一个分区内可以有多个分组 // ------经过分组后,相同学科的数据(一个分组内的数据)可以用一个迭代器调用 val group: RDD[(String, Iterable[((String, String), Int)])] = groupTeahcerReduce.groupBy( (t:((String, String), Int))=> t._1._1,2) // 将每个组内的数据的迭代器拿出(一个迭代器对应一个分组),按照n进行排序 val value: RDD[(String, List[((String, String), Int)])] = group.mapValues((it) => { it.toList.sortBy(u => { u._2 }).reverse.take(3) })
该方案的缺点:分组内数据进行排序,调用的是集合上的排序方法,不是RDD的,集合上的数据需要一次性加载到内存中,当组内数据较大时,可能会内存溢出
方案2:由于学科数量有限,可以将所有学科放入一个集合中,然后对上面的((学科,老师),n)格式的RDD调用filter算子进行过滤,每次过滤出仅包含一个学科的数据,过滤后的数据会存在一个新的RDD中,对新的RDD调用RDD上的sortBy算子,按照n进行排序即可。
// 获取所有的学科集合 val keys: RDD[(String, String)] = reduced.keys val subjects: RDD[String] = keys.map(u => { u._1 }) // 这里需要用一个action算子驱动subjects,并放到集合中,遍历集合中数据 // 因为不能在一个RDD上的transformation里面调用其他的transformation val strings: Array[String] = subjects.collect() // 遍历每个学科,一次过滤出一个学科的所有老师进行排序 for(sb { it._1._1==sb }) // 将每次过滤出的相同学科的数据进行排序 // RDD排序,内存加磁盘进行排序 val sorted: RDD[((String, String), Int)] = filtered.sortBy(it => { it._2 }, false) // action算子进行驱动 sorted.foreach(u=>{println(u)}) }
需要注意的是,存学科的RDD需要用action算子进行驱动一下,用集合表示。因为不能在一个transformation算子里面调用其他的transformation。
优缺点:很明显,需要多次过滤,效率慢。但是这里的排序是RDD上的排序,不用担心内存溢出的问题。
方案3:自定义分区,将相同学科的数据放在同一分区内,每个分区对应一个Task,再针对每个分区进行排序。
首先是自定义的分区器类,按照学科进行分区,这里一定需要保证每个学科对应的数据应该在不同的分区内。分区器的构造参数是学科集合。
/** * 自定义分区器,按照学科进行分区 */ class SubjectPartitioner( subjects :Array[String]) extends Partitioner{ // 设置分区规则,设置每个学科对应的分区ID,用map存。 // 不能用subject.hashMap % numPartitions,这样不能保证每个分区内只有一个学科的数据 val rules: mutable.HashMap[String, Int] = new mutable.HashMap[String, Int]() var i = 0; for(sb
排序过程:先生成构造分区器的学科集合,构造自定义分区器。然后调用partitionBy算子对数据进行分区。最后调用mapPartitions算子获取一个分区上的数据,将数据放入集合中进行排序。
// 获取所有的学科集合 val keys: RDD[(String, String)] = reduced.keys val subjects: RDD[String] = keys.map(u => { u._1 }) // 这里需要用一个action算子驱动subjects,,并放到集合中,遍历集合中数据 // 因为不能在一个RDD上的transformation里面调用其他的transformation val strings: Array[String] = subjects.collect() // 自定义一个分区器,按照学科进行分区 val sbPartitioner: SubjectPartitioner = new SubjectPartitioner(strings) val paritioner: RDD[((String, String), Int)] = reduced.partitionBy(sbPartitioner) // 在每个分区内排序,调用的是集合上的排序, // mapPartitions算子,每次拿一个分区中数据,(用迭代器获取) val sorted: RDD[((String, String), Int)] = paritioner.mapPartitions((it) => { it.toList.sortBy(u => { u._2 }).reverse.iterator })
该方案的缺点:和方案1一样,集合上进行排序,会有可能内存溢出。并且reduceByKey和partitionBy算子都需要shuffle,读写磁盘的操作就比较多。
方案四:自定义分区器,并且减少shuffle次数。在reduceByKey的同时按照学科进行分区,减少shuffle的次数。
使用方案三的自定义分区器。
排序过程:
// reduceByKey的同时按照指定分区器进行分区 // 该RDD的一个分区内仅有一个学科的数据 val partitioner: SubjectPartitioner = new SubjectPartitioner(subjects) val reduced: RDD[((String, String), Int)] = sbjectTeacher.reduceByKey(partitioner, (a, b) => { a + b }) // 在每个分区内排序,调用的是集合上的排序, // mapPartitions算子,每次拿一个分区中数据,(用迭代器获取) val sorted: RDD[((String, String), Int)] = reduced.mapPartitions(it => { it.toList.sortBy(u => { u._2 }).reverse.iterator })
方案四优缺点:和方案1一样,集合上进行排序,会有可能内存溢出。但是相比方案三,会减少shuffle的次数。
另外,您还有什么不同的方案欢迎评论留言,谢谢!!