Spark面试题
个人练习,思路可能并不正确,数据合并中逻辑不完善,谨慎观看
现有如下数据文件需要处理
格式:CSV
位置:hdfs://myhdfs/input.csv
大小:100GB
字段:用户ID,位置ID,开始时间,停留时长(分钟)
4行样例:
UserA,LocationA,2018-01-01 08:00:00,60
UserA,LocationA,2018-01-01 09:00:00,60
UserA,LocationB,2018-01-01 10:00:00,60
UserA,LocationA,2018-01-01 11:00:00,60
解读:样例数据中的数据含义是:
用户UserA,在LocationA位置,从8点开始,停留了60分钟
用户UserA,在LocationA位置,从9点开始,停留了60分钟
用户UserA,在LocationB位置,从10点开始,停留了60分钟
用户UserA,在LocationA位置,从11点开始,停留了60分钟
该样例期待输出:
UserA,LocationA,2018-01-01 08:00:00,120
UserA,LocationB,2018-01-01 10:00:00,60
UserA,LocationA,2018-01-01 11:00:00,60
处理逻辑:
1、对同一个用户,在同一个位置,连续的多条记录进行合并
2、合并原则:开始时间取最早时间,停留时长加和
要求:请使用Spark、MapReduce或其他分布式计算引擎处理
思路
- 首先使用范围分区器与repartitionAndSortWithinPartitions算子将数据分成100份,每份1G左右同时根据时间排序。
- 加盐并完成第一次两两合并。此时我们可以保证每个mapTask处理的数据是有序且合并完毕的但是我们不能保证mapA的最后一条与mapB的第一条不是相关联的
- 重新设置分区,使得repartitionAndSortWithinPartitions边界发生变化,之后对数据重新排序合并。此时数据应该是符合要求的数据
函数测试时使用分区数为4与1
测试数据
UserA,LocationB,2018-01-01 07:00:00,60
UserA,LocationA,2018-01-01 08:00:00,60
UserA,LocationA,2018-01-01 09:00:00,60
UserA,LocationB,2018-01-01 10:00:00,60
UserA,LocationA,2018-01-01 11:00:00,60
UserA,LocationA,2018-01-01 12:00:00,60
UserA,LocationA,2018-01-01 13:00:00,60
UserA,LocationA,2018-01-01 14:00:00,60
结果如下:
代码如下:
import java.text.SimpleDateFormat
import java.util.Calendar
import org.apache.spark.rdd.RDD
import org.apache.spark.{RangePartitioner, SparkConf, SparkContext, TaskContext}
import scala.collection.mutable.ArrayBuffer
object Interview {
/**
* 思路:
* 首先使用范围分区器与repartitionAndSortWithinPartitions算子将数据分成100份,每份1G左右同时根据时间排序。
* 之后加盐并完成第一次两两合并。
* 此时我们可以保证每个mapTask处理的数据是有序且合并完毕的但是我们不能保证mapA的最后一条与mapB的第一条不是相关联的
* 故此重新设置分区,使得repartitionAndSortWithinPartitions边界发生变化,之后对数据重新排序合并。
* 此时数据应该是符合要求的数据
*
* 函数测试时使用分区数为4与1
*/
def main(args: Array[String]): Unit = {
// 获取核心对象
var sc = new SparkContext(new SparkConf().setAppName("SparkTest")
.setMaster("local[*]").set("spark.testing.memory", "2147480000"))
var path = "hdfs://myhdfs/input.csv" // 文件路径
var file = sc.textFile(path) // 获取输入rdd
mainLogic(file, sc) // 主要逻辑处理
}
/**
* 指定比较规则->数据拆分转换为rdd->分区且排序->合并->重新分区排序->合并
*
* @param file 输入文件
* @param sc
*/
def mainLogic(file: RDD[String], sc: SparkContext) {
// 指定repartitionAndSortWithinPartitions排序规则
implicit val my_self_Ordering: Ordering[String] = new Ordering[String] {
override def compare(a: String, b: String): Int = {
TimeComparison(a, b) // 时间排序
}
}
// 数据切片
var splitfile: RDD[(String, (String, String, String))] = file.map(input => {
var array = input.split(",") // 数据拆分
// 时间为key,其余为value
(array(2), (array(0), array(1), array(3)))
})
// 第一次分区完毕的数据,此时数据分区有序,一个reduce差不多处理1G的数据
var firstPartitionFile: RDD[(String, (String, String, String))] = splitfile.repartitionAndSortWithinPartitions(new RangePartitioner(4, splitfile))
firstPartitionFile.foreach(println)
println("⬆⬆⬆⬆⬆⬆⬆⬆⬆第一次分区排序⬆⬆⬆⬆⬆⬆⬆⬆⬆")
// 第一次合并
var firstArrangefile: RDD[(String, (String, String, String))] = mapProcess(firstPartitionFile)
firstArrangefile.foreach(println)
println("⬆⬆⬆⬆⬆⬆⬆⬆⬆第一次合并结果⬆⬆⬆⬆⬆⬆⬆⬆⬆")
// 第二次分区
var secondPartitionFile: RDD[(String, (String, String, String))] = firstArrangefile.repartitionAndSortWithinPartitions(new RangePartitioner(1, firstArrangefile))
secondPartitionFile.foreach(println)
println("⬆⬆⬆⬆⬆⬆⬆⬆⬆第二次分区排序⬆⬆⬆⬆⬆⬆⬆⬆⬆")
// 第二次合并
var secondArrangeFile: RDD[(String, (String, String, String))] = mapProcess(secondPartitionFile)
secondArrangeFile.foreach(println)
println("⬆⬆⬆⬆⬆⬆⬆⬆⬆第二次合并结果⬆⬆⬆⬆⬆⬆⬆⬆⬆")
}
/**
* 概述:传入有序rdd,进行相邻数据的单个map合并,
* 详情:加盐保证不会数据倾斜,使用groupbu获得迭代器,使用map并行合并,合并结果作为ArrayBuffer数组进行存储,使用map与flatMap算子将数组转换为rdd算子
*
* @param partitionFile
* @return
*/
def mapProcess(partitionFile: RDD[(String, (String, String, String))]): RDD[(String, (String, String, String))] = {
// 加盐,groupbykey获得迭代器
var groupFile: RDD[(Int, Iterable[(String, (String, String, String))])] = partitionFile.map((TaskContext.get.partitionId, _)).groupByKey()
// 局部变量,存储两两合并的临时结果
var transit: (String, (String, String, String)) = (":", (":", ":", ":")) // 存储当前数据
//(2018-01-01 10:00:00,(UserA,LocationB,60))
// 数组合并
var firstFile = groupFile.map {
case (partitionID, values) => {
var arrayBuffer: ArrayBuffer[(String, (String, String, String))] = ArrayBuffer[(String, (String, String, String))]()
// 为空说明是第一次,获取第一行数据
for (value <- values) {
if (transit._1 == ":") {
transit = (value._1, (value._2._1, value._2._2, value._2._3))
}
// 时间地点连续
else if (transit._2._1 == value._2._1 && transit._2._2 == value._2._2 && TimeContinuous(transit._1, value._1, transit._2._3)) {
transit = (transit._1, (transit._2._1, transit._2._2, (Integer.parseInt(transit._2._3) + Integer.parseInt(value._2._3)).toString)) // 重新设置时间
} else {
// 不是第一次、也不是连续的地点那么说明记录断开则记录上一次的地点,同时更新当前记录
var test = transit
transit = (value._1, (value._2._1, value._2._2, value._2._3))
arrayBuffer += test
}
}
arrayBuffer += transit
arrayBuffer
}
}
// 数组转换为算子
firstFile.map(values => {
var buffer = new StringBuffer
for (value <- values) {
buffer.append(value._1 + "," + value._2._1 + "," + value._2._2 + "," + value._2._3 + "#")
}
buffer.substring(0, buffer.length() - 1)
}).flatMap(_.split("#")).map(input => {
var array = input.split(",") // 数据拆分
// 时间为key,其余为value
(array(0), (array(1), array(2), array(3)))
})
}
/**
* 计算增量时间,判断是否相等
*
* @param time1 时间1
* @param time2 时间2
* @param timeIncrement 时间1的时间增量(停顿时间)
* @return
*/
def TimeContinuous(time1: String, time2: String, timeIncrement: String): Boolean = {
var simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
var date1 = simpleDateFormat.parse(time1) // 数据类型转换
var date2 = simpleDateFormat.parse(time2) // 数据类型转换
var cal = Calendar.getInstance(); // 时间计算
cal.setTime(date1);
cal.add(Calendar.MINUTE, Integer.parseInt(timeIncrement)); // 24小时制
date1 = cal.getTime
// 使用Date的compareTo()方法,大于、等于、小于分别返回1、0、-1
// 相等为true
// 不等为false
if (date1.compareTo(date2) == 0) true
else false
}
/**
* 时间比较函数,为repartitionAndSortWithinPartitions准备
*
* @param time1 时间1
* @param time2 时间2
* @return 比较结果
* 大于 1
* 等于 0
* 小于-1
*/
def TimeComparison(time1: String, time2: String): Int = {
// 获取核心对象,指定数据类型
val simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val date1 = simpleDateFormat.parse(time1) // 数据类型转换
val date2 = simpleDateFormat.parse(time2) // 数据类型转换
//使用Date的compareTo()方法,大于、等于、小于分别返回1、0、-1
date1.compareTo(date2)
}
}