Spark RDD案例(三)连续分布数据按照条件rollup
1. 背景
- Spark作为大数据分析引擎,本身可以做离线和准实时数据处理
- Spark抽象出的操作对象如RDD、dataSet、dataFrame、DStream等都是高层级的抽象,屏蔽了分布式数据处理代码细节,操作分布式数据和处理就像使用scala集合接口一样便利。这样可以很大降低编程使用和理解门槛。
- 在实际生产中,大数据处理面临的业务需求和正常java 业务需求一样,都是基于数据做处理。不同的是正常java业务数据相对较少,如mysql中适合存储的数据是小而美的如500万行数据及以下,而大数据存储500万行才达到海量数据存储的门槛。
- 实际生产中,大数据和小批量Java数据处理需求往往类似,如连续分布的一些数据按照一定条件累积汇总,如电信商统计用户流量是按照时间间隔分段统计,如汽车速度区间统计,例如跑步app对配速的分段精确统计等等。
2. 案例
- 需求
- 这里是要求将2条数据的下载流量汇总,第一个字段是用户id,第二个第三个是开始和结束时间戳,第三个是下行流量
- 汇总条件是,同一用户,上一条数据的结束时间和当前一条数据的开始时间间隔不超过10min,超过则重新汇总
- 数据
1,2020-02-18 14:20:30,2020-02-18 14:46:30,20
1,2020-02-18 14:47:20,2020-02-18 15:20:30,30
1,2020-02-18 15:37:23,2020-02-18 16:05:26,40
1,2020-02-18 16:06:27,2020-02-18 17:20:49,50
1,2020-02-18 17:21:50,2020-02-18 18:03:27,60
2,2020-02-18 14:18:24,2020-02-18 15:01:40,20
2,2020-02-18 15:20:49,2020-02-18 15:30:24,30
2,2020-02-18 16:01:23,2020-02-18 16:40:32,40
2,2020-02-18 16:44:56,2020-02-18 17:40:52,50
3,2020-02-18 14:39:58,2020-02-18 15:35:53,20
3,2020-02-18 15:36:39,2020-02-18 15:24:54,30
2.1 代码一
package com.doit.practice
import java.text.SimpleDateFormat
import java.util.Date
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* 1,2020-02-18 14:20:30,2020-02-18 14:46:30,20
* 1,2020-02-18 14:47:20,2020-02-18 15:20:30,30
* 1,2020-02-18 15:37:23,2020-02-18 16:05:26,40
* 1,2020-02-18 16:06:27,2020-02-18 17:20:49,50
* 1,2020-02-18 17:21:50,2020-02-18 18:03:27,60
* 2,2020-02-18 14:18:24,2020-02-18 15:01:40,20
* 2,2020-02-18 15:20:49,2020-02-18 15:30:24,30
* 2,2020-02-18 16:01:23,2020-02-18 16:40:32,40
* 2,2020-02-18 16:44:56,2020-02-18 17:40:52,50
* 3,2020-02-18 14:39:58,2020-02-18 15:35:53,20
* 3,2020-02-18 15:36:39,2020-02-18 15:24:54,30
*
*/
object NetFlowRollUp {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("NetFlowRollUp").setMaster("local[*]")
val sc = new SparkContext(conf)
// 1, 2020-02-18 14:20:30, 2020-02-18 14:46:30, 20
// 这里是要求将2条数据的下载流量汇总,
// 第一个字段是用户id,第二个第三个是开始和结束时间戳,第三个是下行流量
// 汇总条件是,同一用户,上一条数据的结束时间和当前一条数据的开始时间间隔不超过10min,超过则重新汇总
// 处理思路是,
// 先将数据根据uid分组,分组之后,将数据根据开始时间排序(排序时需要转换为长整型数字,方便排序)
// 排序之后,还需要去重,防止有重复脏数据,这时候方便去重,将数据转换为uid,开始,结束为key,下行流量为value
// 然后使用类似lag操作,分组数据处理,将每个组的数据转换为List或者Array,,判断2条数据之间结束和开始时间间隔是否小于10min,是标记为0,不是标记为1
// 再对这个标记做累加操作,这样相同类型数据就会是0 1 2 这种统一累加标记。
// 针对这种累加标记做分组,对相应的流量做聚合,就得到想要的数据
// 如果需要可视化好一些,对时间戳做转换,格式化一下
val linesRDD: RDD[String] = sc.textFile("E:\\DOITLearning\\12.Spark\\netflowRollupSourceData.txt")
// 将数据做转换,切分,注意涉及到数据处理用到对象,使用mapPartitions更加合适,因为可以复用
// 数据还需要去重
val mapedRDD: RDD[((String, Long, Long), Double)] = linesRDD.mapPartitions(iter => {
// 2020-02-18 14:20:30
val simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
// 注意这里需要将时间字符串转为长整型数字,方便后续对比大小
iter.map(line => {
val strings: Array[String] = line.split("\\,")
val uid: String = strings(0)
val beginStr: String = strings(1)
val endStr: String = strings(2)
val downflowStr: String = strings(3)
val downflowNum: Double = downflowStr.toDouble
val beginDate: Date = simpleDateFormat.parse(beginStr)
val beginTime: Long = beginDate.getTime
val endDate: Date = simpleDateFormat.parse(endStr)
val endTime: Long = endDate.getTime
// 将数据转换为想要的数据
((uid, beginTime, endTime), downflowNum)
})
}).distinct()
println("mapedRDD: " + mapedRDD.collect().toBuffer)
/*
* mapedRDD: ArrayBuffer(((1,1582006830000,1582008390000),20.0), ((1,1582008440000,1582010430000),
* 30.0), ((1,1582011443000,1582013126000),40.0), ((1,1582013187000,1582017649000),50.0),
* ((1,1582017710000,1582020207000),60.0), ((2,1582006704000,1582009300000),20.0),
* ((2,1582010449000,1582011024000),30.0), ((2,1582012883000,1582015232000),40.0),
* ((2,1582015496000,1582018852000),50.0), ((3,1582007998000,1582011353000),20.0),
* ((3,1582011399000,1582010694000),30.0))
* */
// 然后根据uid做转换,然后排序
val reMapedRDD: RDD[(String, (Long, Long, Double))] = mapedRDD.map(ele => {
(ele._1._1, (ele._1._2, ele._1._3, ele._2))
})
println("reMapedRDD: " + reMapedRDD.collect().toBuffer)
/*
* reMapedRDD: ArrayBuffer((1,(1582017710000,1582020207000,60.0)), (2,(1582006704000,1582009300000,20.0)),
* (3,(1582011399000,1582010694000,30.0)), (1,(1582006830000,1582008390000,20.0)),
* (1,(1582008440000,1582010430000,30.0)), (1,(1582011443000,1582013126000,40.0)),
* (1,(1582013187000,1582017649000,50.0)), (2,(1582010449000,1582011024000,30.0)),
* (3,(1582007998000,1582011353000,20.0)), (2,(1582015496000,1582018852000,50.0)),
* (2,(1582012883000,1582015232000,40.0)))
* */
// 根据uid分组
val groupedRDD: RDD[(String, Iterable[(Long, Long, Double)])] = reMapedRDD.groupByKey()
println("groupedRDD: " + groupedRDD.collect().toBuffer)
/*
* groupedRDD: ArrayBuffer((2,CompactBuffer((1582006704000,1582009300000,20.0),
* (1582010449000,1582011024000,30.0), (1582015496000,1582018852000,50.0),
* (1582012883000,1582015232000,40.0))), (3,CompactBuffer((1582011399000,1582010694000,30.0),
* (1582007998000,1582011353000,20.0))), (1,CompactBuffer((1582017710000,1582020207000,60.0),
* (1582006830000,1582008390000,20.0), (1582008440000,1582010430000,30.0),
* (1582011443000,1582013126000,40.0), (1582013187000,1582017649000,50.0))))
* */
// 对数据进行排序,按照日期做排序
val sumedRDD: RDD[(String, (Long, Long, Double, Int))] = groupedRDD.flatMapValues(iter => {
// 在这里面做数据排序
val sortedList: List[(Long, Long, Double)] = iter.toList.sortBy(_._1)
// 在这里给每条数据打标记,判断是否前后间隔小于10min
var tempEndTime = 0L
var flag = 0
var sum = 0
// (1582006830000,1582008390000,20.0)
sortedList.map(x => {
val currentStartTime: Long = x._1
val currentEndTime = x._2
// 如果tempEndTime==0.说明是第一条数据,不需要做这个相减操作
if (tempEndTime != 0) {
// 如果大于10min,则标记为1
if ((currentStartTime - tempEndTime) / 60 / 1000 > 10) {
flag = 1
} else {
flag = 0
}
}
// sum就是针对flag标记位0 1做累加的字段
sum += flag
tempEndTime = currentEndTime
// 返回数据
(currentStartTime, currentEndTime, x._3, sum)
})
})
println("sumedRDD: " + sumedRDD.collect().toBuffer)
// 对数据做转换,将uid和这个sum标记字段变成key,其他变成value,都是元组形式
val reMapedSumedRDD: RDD[((String, Int), (Long, Long, Double))] = sumedRDD.map(x => {
((x._1, x._2._4), (x._2._1, x._2._2, x._2._3))
})
println("reMapedSumedRDD: " + reMapedSumedRDD.collect().toBuffer)
/*
* reMapedSumedRDD: ArrayBuffer(((2,0),(1582006704000,1582009300000,20.0)),
* ((2,1),(1582010449000,1582011024000,30.0)),
* ((2,2),(1582012883000,1582015232000,40.0)),
* ((2,2),(1582015496000,1582018852000,50.0)),
* ((3,0),(1582007998000,1582011353000,20.0)),
* ((3,0),(1582011399000,1582010694000,30.0)),
* ((1,0),(1582006830000,1582008390000,20.0)),
* ((1,0),(1582008440000,1582010430000,30.0)),
* ((1,1),(1582011443000,1582013126000,40.0)),
* ((1,1),(1582013187000,1582017649000,50.0)),
* ((1,1),(1582017710000,1582020207000,60.0)))
* */
// 使用reduceByKey对数据做聚合,保留最开始一条的开始时间,保留最后一条的结束时间,对流量做汇总
val reduceResRDD: RDD[((String, Int), (Long, Long, Double))] = reMapedSumedRDD.reduceByKey((tupleThree1, tupleThree2) => {
(Math.min(tupleThree1._1, tupleThree2._1), Math.max(tupleThree1._2, tupleThree2._2), tupleThree1._3 + tupleThree2._3)
})
println("reduceResRDD: " + reduceResRDD.collect().toBuffer)
// 对日期做格式化, 因为需要对每条数据做处理,所以使用mapPartitions可以复用日期格式化对象·
val formatedResRDD: RDD[((String, Int), (String, String, Double))] = reduceResRDD.mapPartitions(iter => {
val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
// ((1,1),(1582017710000,1582020207000,60.0)))
iter.map(ele => {
val startTime: Long = ele._2._1
val endTime: Long = ele._2._2
val startDate = new Date(startTime)
val endDate = new Date(endTime)
val startDateStr: String = format.format(startDate)
val endDateStr: String = format.format(endDate)
((ele._1._1, ele._1._2), (startDateStr, endDateStr, ele._2._3))
})
})
println("formatedResRDD: " + formatedResRDD.collect().toBuffer)
/*
* formatedResRDD: ArrayBuffer(
* ((2,1),(2020-02-18 15:20:49,2020-02-18 15:30:24,30.0)),
* ((1,0),(2020-02-18 14:20:30,2020-02-18 15:20:30,50.0)),
* ((3,0),(2020-02-18 14:39:58,2020-02-18 15:35:53,50.0)),
* ((2,2),(2020-02-18 16:01:23,2020-02-18 17:40:52,90.0)),
* ((1,1),(2020-02-18 15:37:23,2020-02-18 18:03:27,150.0)),
* ((2,0),(2020-02-18 14:18:24,2020-02-18 15:01:40,20.0)))
* */
/*
* 1,2020-02-18 14:20:30,2020-02-18 14:46:30,20
* 1,2020-02-18 14:47:20,2020-02-18 15:20:30,30
*
* 1,2020-02-18 15:37:23,2020-02-18 16:05:26,40
* 1,2020-02-18 16:06:27,2020-02-18 17:20:49,50
* 1,2020-02-18 17:21:50,2020-02-18 18:03:27,60
*
* 2,2020-02-18 14:18:24,2020-02-18 15:01:40,20
*
* 2,2020-02-18 15:20:49,2020-02-18 15:30:24,30
*
* 2,2020-02-18 16:01:23,2020-02-18 16:40:32,40
* 2,2020-02-18 16:44:56,2020-02-18 17:40:52,50
*
* 3,2020-02-18 14:39:58,2020-02-18 15:35:53,20
* 3,2020-02-18 15:36:39,2020-02-18 15:24:54,30
* */
// 符合预期,如果需要按照顺序,可以对uid再做一次排序,sortBy即可
sc.stop()
}
}
运行结果
formatedResRDD: ArrayBuffer(((2,1),(2020-02-18 15:20:49,2020-02-18 15:30:24,30.0)), ((1,0),(2020-02-18 14:20:30,2020-02-18 15:20:30,50.0)), ((3,0),(2020-02-18 14:39:58,2020-02-18 15:35:53,50.0)), ((2,2),(2020-02-18 16:01:23,2020-02-18 17:40:52,90.0)), ((1,1),(2020-02-18 15:37:23,2020-02-18 18:03:27,150.0)), ((2,0),(2020-02-18 14:18:24,2020-02-18 15:01:40,20.0)))
注意,这里的数据做转换之后,立即做了去重,因为实际生产中,经常会有各种原因导致的重复数据,这里需要根据用户id,开始结束时间为key去重
之后将转换后的数据使用mapPartitions进行分区处理,因为涉及到对象重复使用时,一般都是用这种分区处理,这样一个区只需要创建一个对象,对比map一条创建一个,性能会有提升
注意做转换时,为了方便后续对比,将时间字符串转换位长整型long类型
为了方便对数据做分组,再做一次转换,以uid为key,其他字段为value
然后使用groupByKey对数据做划分
之后使用flatMapValues对数据做转换和展平处理
首先对每个分区中数据转换,toList,并且以开始时间排序,升序。 Int的排序规则定义再Ordering文件中,包括元组等的排序规则也定义在这里,作为隐式参数调用
之后就是将List中数据使用map迭代转换,这里目标是将标记位以及标记位的累加值计算出来,最后以这个标记位累加值作为key,对数据做分组
在之后,对数据做转换,使用reduceByKey对数据做最后计算,得出需要累加的流量,连续数据的开始时间,连续数据的结束时间
最后使用map对数据做格式化转换
实际企业生产中,开发工作第一步是先确认需求,需求确认后,先使用要处理的数据集的小样本,编写样例demo代码尝试,如果可行再工程化编码,遵守公司代码规范,将代码编写,测试,验证,部署到测试环境中

本文介绍了如何使用Spark的RDD处理连续分布数据的条件rollup操作。通过示例展示了如何处理和汇总大数据,特别是针对时间间隔内的流量统计。在案例中,数据按用户ID和时间戳分段,当相邻数据时间间隔不超过10分钟时进行汇总。通过一系列转换、分组和计算,最终得到符合需求的汇总结果。
1499

被折叠的 条评论
为什么被折叠?



