Spark RDD案例(三)连续分布数据按照条件rollup

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

Spark RDD案例(三)连续分布数据按照条件rollup

1. 背景

  1. Spark作为大数据分析引擎,本身可以做离线和准实时数据处理
  2. Spark抽象出的操作对象如RDD、dataSet、dataFrame、DStream等都是高层级的抽象,屏蔽了分布式数据处理代码细节,操作分布式数据和处理就像使用scala集合接口一样便利。这样可以很大降低编程使用和理解门槛。
  3. 在实际生产中,大数据处理面临的业务需求和正常java 业务需求一样,都是基于数据做处理。不同的是正常java业务数据相对较少,如mysql中适合存储的数据是小而美的如500万行数据及以下,而大数据存储500万行才达到海量数据存储的门槛。
  4. 实际生产中,大数据和小批量Java数据处理需求往往类似,如连续分布的一些数据按照一定条件累积汇总,如电信商统计用户流量是按照时间间隔分段统计,如汽车速度区间统计,例如跑步app对配速的分段精确统计等等。

2. 案例

  1. 需求
  • 这里是要求将2条数据的下载流量汇总,第一个字段是用户id,第二个第三个是开始和结束时间戳,第三个是下行流量
  • 汇总条件是,同一用户,上一条数据的结束时间和当前一条数据的开始时间间隔不超过10min,超过则重新汇总
  1. 数据
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代码尝试,如果可行再工程化编码,遵守公司代码规范,将代码编写,测试,验证,部署到测试环境中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值