离线数仓(四)3、点击流模型的生成plus

本文介绍如何通过Spark UI定位数据倾斜问题,通过转换key(添加时间戳)、使用rangepartitioner进行均匀划分,并结合累加器收集边界数据,有效解决了用户访问过多导致的session ID生成过程中的数据倾斜。通过实例展示了如何修复边界数据,确保模型准确和性能优化。
摘要由CSDN通过智能技术生成

点击流模型的生成plus(解决数据倾斜问题)

如果某个用户访问记录过多,则生成page view模型的时候,会产生数据倾斜,此时该怎样处理呢?
思路:

  • 通过Spark UI找到产生倾斜的shuffle算子

  • 分析产生倾斜的原因

  • 对倾斜的key进行处理

    • 对key增加随机数

    • 增加并行度

    • 单独处理倾斜的key

    • rangepartioner+key转换处理

  • 编写代码进行验证

    本次倾斜解决具体思路

  • 转换key:guid–>guid+time_local,

  • 利用sortbykey对guid+time_local进行全局排序

  • 使用rangepartioner按照key的范围进行划分(保证每个分区数据大致均匀)

  • 第一次生成sessionid,使用累加器收集边界处的数据

  • 处理边界处数据获取正确的sessionid
    在这里插入图片描述
    代码实现:

package com.yyds.service

import java.util
import java.util.UUID

import com.itheima.bean.{PageViewsBeanCase, WebLogBean}
import com.itheima.util.DateUtil
import org.apache.spark.RangePartitioner
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
import org.apache.spark.util.CollectionAccumulator

import scala.collection.mutable
import scala.collection.mutable.ListBuffer

class PageViewService {

}

object PageViewService {


  def savePageViewToHdfs(filterStaticWeblogRdd: RDD[WebLogBean]) = {
    /*
    1:把key由原来uid变为uid+timeLocal字段
    2:使用rangepartitioner进行均匀分区
     */
    //key:uid+timeLocal ,value:weblogbean
    val uidTimeRdd: RDD[(String, WebLogBean)] = filterStaticWeblogRdd.map(bean => (bean.guid + "&" + bean.time_local, bean))
    //为了保证key全局有序使用sortbykey进行排序
    val sortedUidTimeRdd: RDD[(String, WebLogBean)] = uidTimeRdd.sortByKey()
    //使用rangepartitioner均匀分区
    val rangeRdd: RDD[(String, WebLogBean)] = sortedUidTimeRdd.partitionBy(new RangePartitioner(100, sortedUidTimeRdd))


    //使用累加器收集每个分区的首尾记录
    val spark: SparkSession = SparkSession.getDefaultSession.get
    //使用集合类型累加器收集分区的首尾数据
    val headTailList: CollectionAccumulator[(String, String)] = spark.sparkContext.collectionAccumulator[(String, String)]("headTailList")
    //对每个分区使用mappartitionwithindex算子进行sessionid和步长信息的生成
    val questionSessionRdd: RDD[(WebLogBean, String, Int, Long)] = generateSessionid(rangeRdd, headTailList)
    //累加器中有数据必须触发计算,使用累加器一定要注意重复计算的问题
    //对rdd数据进行cache防止重复计算
    questionSessionRdd.cache()
    questionSessionRdd.count()
    val headTailListValue: util.List[(String, String)] = headTailList.value

    //保存数据
    questionSessionRdd.saveAsTextFile("/questionSessionRdd")
    //根据累加器中的边界数据判断哪些分区的边界存在问题
    //方便在累加器数据中获取指定分区的数据,我们把累加器数据结构调整为map类型:key:index+"&first/last",value: 记录的数据
    import collection.JavaConverters._
    val buffer: mutable.Buffer[(String, String)] = headTailListValue.asScala
    //转为一个可变map,方便更新其中的数据
    val map: mutable.HashMap[String, String] = mutable.HashMap(buffer.toMap.toSeq: _*) //map装有原来累加器中的数据
    //根据首尾数据判断边界问题得到需要修复的正确数据
    val correctMap: mutable.HashMap[String, String] = processBoundaryMap(map)


    //广播正确的map数据到每个executor
    val questionBroadCast: Broadcast[mutable.HashMap[String, String]] = spark.sparkContext.broadcast(correctMap)
    //经过修复过后的正确的rdd数据,(uidtime,sessionid,step,staylong)
    val correctRdd: RDD[(WebLogBean, String, Int, Long)] =
      repairBoundarySession(questionSessionRdd, questionBroadCast)

    val pageviewRdd: RDD[PageViewsBeanCase] = correctRdd.map(
      t => {


        PageViewsBeanCase(
          t._2, t._1.remote_addr, t._1.time_local, t._1.request, t._3, t._4,
          t._1.http_referer, t._1.http_user_agent, t._1.body_bytes_sent, t._1.status, t._1.guid
        )
      }
    )
    pageviewRdd.saveAsTextFile("/pageviewrdd.txt")

  }


  //修复我们rdd边界处的数据
  def repairBoundarySession(uidTimeSessionStepLongRdd: RDD[( WebLogBean, String, Int, Long)],
                            questionBroadCast: Broadcast[mutable.HashMap[String, String]]) = {
    //key:index/first/last,value:last-->timediff,first-->correctsessionid+correctstep+quesitonsessionid
    val questionMap: mutable.HashMap[String, String] = questionBroadCast.value
    val correctRdd: RDD[(WebLogBean, String, Int, Long)] = uidTimeSessionStepLongRdd.mapPartitionsWithIndex(
      (index, iter) => {
        //uid&time,sessionid,step,staylong
        var orginList = iter.toList
        val firstLine: String = questionMap.getOrElse(index + "&first", "")
        val lastLine: String = questionMap.getOrElse(index + "&last", "")
        if (lastLine != "") {
          //当前这个分区最后一条数据他的停留时长需要修改
          val buffer: mutable.Buffer[(WebLogBean, String, Int, Long)] = orginList.toBuffer
          val lastTuple: (WebLogBean, String, Int, Long) = buffer.remove(buffer.size - 1) //只修改停留时长
          buffer += ((lastTuple._1, lastTuple._2, lastTuple._3, lastLine.toLong))
          orginList = buffer.toList
        }

        if (firstLine != "") {
          //分区第一条数据有问题,则需要修改:按照错误的sessionid找到所有需要修改的数据,改正sessionid和step
          val firstArr: Array[String] = firstLine.split("&")
          val tuples: List[(WebLogBean, String, Int, Long)] = orginList.map {
            t => {
              if (t._2.equals(firstArr(2))) {
                //错误的sessionid,修改为正确的sessionid和步长
                (t._1, firstArr(0), firstArr(1).toInt + t._3.toInt, t._4)
              } else {
                t
              }
            }
          }
          orginList=tuples
        }
        orginList.iterator
      }
    )
    correctRdd

  }

  //根据首尾数据找到有问题边界数据,以及修改的正确数据
  def processBoundaryMap(map: mutable.HashMap[String, String]) = {
    //定义一个map接收有问题分区需要修改的正确数据:key:index+"&first/last", value需要修改的正确数据
    val correctMap: mutable.HashMap[String, String] = new mutable.HashMap[String, String]()
    //遍历首尾记录map找到有问题的边界
    for (num <- 1 until (map.size / 2)) {
      //保证等于分区数据
      //获取num分区对应的首尾记录
      val numFirstMsg: String = map.get(num + "&first").get //uid+time+sessionid
      val numLastMsg: String = map.get(num + "&last").get //uid+time+sessionid+step+partition.size(分区数量)
      //获取到上一个分区的最后一条数据
      val lastPartLastMsg: String = map.get((num - 1) + "&last").get
      //判断当前分区与上个分区是否存在边界问题
      val numLastArr: Array[String] = numLastMsg.split("&")

      val lastPartLastArr: Array[String] = lastPartLastMsg.split("&")
      val numFirstArr: Array[String] = numFirstMsg.split("&")
      //判断是否同个用户
      if (lastPartLastArr(0).equals(numFirstArr(0))) {
        //判断时间差
        val timediff = DateUtil.getTimeDiff(lastPartLastArr(1), numFirstArr(1))
        if (timediff < 30 * 60 * 1000) {
          //说明当前分区第一条数据与上个分区最后一条属于同个会话
          //上个分区记录需要修改的正确的停留时长数据
          correctMap.put((num - 1) + "&last", timediff.toString)
          //当前分区的第一条数据(有可能是多条数据严谨来说应该是当前分区的第一个session的数据)需要修改的数据
          //sessionid:与上个分区最后一条数据的sessionid保持一致,step:应该是上个分区最后一条记录的step+1
          if (lastPartLastArr.size > 5) {
            //正确的sessionid+正确的step+错误的sessionid
            correctMap.put(num + "&first", lastPartLastArr(lastPartLastArr.size - 2) + "&"
              + lastPartLastArr(lastPartLastArr.size - 1) + "&" + numFirstArr(2))
          } else {

            correctMap.put(num + "&first", lastPartLastArr(2) + "&" + lastPartLastArr(3) + "&" + numFirstArr(2))
          }
          //判断当前整个分区是否属于同个会话,属于同个会话则更新map中当前分区对应的最后一条数据的sessionid和ste数据

          if (numFirstArr(2).equals(numLastArr(2))) {
            //说明是同个会话,存在了会话穿透多个分区的现象
            //更新最后一条数据的step和sessionid信息
            //numlastMsg +正确的sessionid(上个分区的最后一条数据的sessionid)+正确的步长step(上个分区最后一条数据的步长+
            // 当前分区的数量)
            if (lastPartLastArr.size > 5) {
              map.put(num + "&last", numLastMsg + "&" + lastPartLastArr(lastPartLastArr.size - 2) + "&" +
                (lastPartLastArr(lastPartLastArr.size - 1).toInt + numLastArr(4).toInt))

            } else {
              //uid+time+sessionid+step+partition.size(分区数量)+sessionid+step
              map.put(num + "&last", numLastMsg + "&" + lastPartLastArr(2) + "&" + (lastPartLastArr(3).toInt + numLastArr(4).toInt))
            }

          }
        }
      }


    }
    correctMap
  }

  //对均匀分区的rdd生成sessionid,使用累加器收集每个分区的首尾数据
  def generateSessionid(rangeRdd: RDD[(String, WebLogBean)],
                        headTailList: CollectionAccumulator[(String, String)]) = {


    //使用mappartitionwithindex算子
    val sessionidStepPageRdd: RDD[(WebLogBean, String, Int, Long)] = rangeRdd.mapPartitionsWithIndex {
      (index, iter) => {

        //iter-->list,list集合中依然是按照key有序分布的
        val list: List[(String, WebLogBean)] = iter.toList
        //准备一个list集合接收每条记录生成的sessionid的信息:数据内容:weblogbean,sessionid,step,pagestaylong
        val resultTupleList: ListBuffer[(WebLogBean, String, Int, Long)] = new ListBuffer[(WebLogBean, String, Int, Long)]()
        //准备sessionid,step,pagestaylong
        var sessionid = UUID.randomUUID().toString
        var step = 1
        var pagestaylong: Long = 60000
        //遍历list集合进行两两比较判断是否是同个用户以及时间是否小于30分钟
        import scala.util.control.Breaks._
        breakable {
          for (num <- 0 until (list.size)) {
            //取出当前遍历的数据
            val currentTuple: (String, WebLogBean) = list(num)
            //累加器收集第一条数据
            if (num == 0) {
              //把数据装入累加器中:key:分区编号+"&"+first/last,value:uid+time,sessionid
              headTailList.add((index + "&first", currentTuple._1 + "&" + sessionid))
            }
            //判断只有一条数据的情况
            if (list.size == 1) {
              //当前分区只有一条数据,不需要生成pageviewbeancase类型的数据,
              //添加数据到resulttuplelist中
              resultTupleList += ((currentTuple._2, sessionid, step, pagestaylong))
              //重新生成sessionid
              sessionid = UUID.randomUUID().toString
              //中止循环
              break()
            }

            //判断不止有一条数据的情况
            //第一条数据我们跳过从第二条开始遍历,
            //实现第一条数据continue的效果
            breakable {
              if (num == 0) {
                //说明是第一条
                break()
              }
              //从第二条开始判断
              //获取到上一条的数据然后两两比较   //生成sessionid需要uid和time字段即可生成
              val lastTuple: (String, WebLogBean) = list(num - 1)
              val currentUidTime: String = currentTuple._1
              val lastUidTime: String = lastTuple._1
              //取出uid和time
              //uid+"&"+time_local
              val currentUidTimeArr: Array[String] = currentUidTime.split("&")
              val lastUidTimeArr: Array[String] = lastUidTime.split("&")

              //计算时间差
              val timeDiff = DateUtil.getTimeDiff(lastUidTimeArr(1), currentUidTimeArr(1))
              //是不是同个用户
              if (lastUidTimeArr(0).equals(currentUidTimeArr(0)) && timeDiff < 30 * 60 * 1000) {
                //说明两条记录是同个session,保存上一条数据:sessionid,step,timediff
                resultTupleList += ((lastTuple._2, sessionid, step, timeDiff))
                //sessionid和step如何处置,sessionid无需重新生成,step必须要加1
                step += 1
              } else {
                //说明两条记录是不同的会话
                resultTupleList += ((lastTuple._2, sessionid, step, pagestaylong))
                //sessionid,step
                sessionid = UUID.randomUUID().toString
                //step重置
                step = 1
              }
              //考虑最后一条数据的输出问题
              if (num == list.size - 1) {
                //需要保存最后一条数据
                resultTupleList += ((currentTuple._2, sessionid, step, pagestaylong))
                //使用累加器收集最后一条数据,key:index+"&"last/first,value:uid+time+sessionid+step+partition.size
                headTailList.add((index + "&last", currentTuple._1 + "&" + sessionid + "&" + step + "&" + list.size))
                //sessionid
                sessionid = UUID.randomUUID().toString
              }

            }

          }

        }

        resultTupleList.toIterator
      }
    }
    sessionidStepPageRdd


  }

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值