spark-core需求分析

在这里插入图片描述
数据说明:

  1. 数据采用_分割字段
  2. 每一行表示用户的一个行为, 所以每一行只能是四种行为中的一种.
  3. 如果搜索关键字是 null, 表示这次不是搜索
  4. 如果点击的品类 id 和产品 id 是 -1 表示这次不是点击
  5. 下单行为来说一次可以下单多个产品, 所以品类 id 和产品 id 都是多个, id 之间使用逗号,分割. 如果本次不是下单行为, 则他们相关数据用null来表示
  6. 支付行为和下单行为类似.
    需求 1: Top10 热门品类
    按照每个品类的 点击、下单、支付 的量来统计热门品类.
    思路:遍历一次能够计算出来上述的 3 个指标.
    使用累加器可以达成我们的需求.
    ·遍历全部日志数据, 根据品类 id 和操作类型分别累加. 需要用到累加器
    ·定义累加器
    ·当碰到订单和支付业务的时候注意拆分字段才能得到品类 id
    ·遍历完成之后就得到每个每个品类 id 和操作类型的数量.
    ·按照点击下单支付的顺序来排序
    ·取出 Top10

用来封装用户行为的bean类
new一个bean包

/**
 * 用户访问动作表
 *
 * @param date               用户点击行为的日期
 * @param user_id            用户的ID
 * @param session_id         Session的ID
 * @param page_id            某个页面的ID
 * @param action_time        动作的时间点
 * @param search_keyword     用户搜索的关键词
 * @param click_category_id  某一个商品品类的ID
 * @param click_product_id   某一个商品的ID
 * @param order_category_ids 一次订单中所有品类的ID集合
 * @param order_product_ids  一次订单中所有商品的ID集合
 * @param pay_category_ids   一次支付中所有品类的ID集合
 * @param pay_product_ids    一次支付中所有商品的ID集合
 * @param city_id            城市 id
 */
case class UserVisitAction(date: String,
                           user_id: Long,
                           session_id: String,
                           page_id: Long,
                           action_time: String,
                           search_keyword: String,
                           click_category_id: Long,
                           click_product_id: Long,
                           order_category_ids: String,  // "1,10,30"
                           order_product_ids: String,  // "10,11"
                           pay_category_ids: String,
                           pay_product_ids: String,
                           city_id: Long)


case class CategroyCount(cid: String,
                         clickCount: Long,
                         orderCount: Long,
                         payCount: Long)



 case class SessionInfo(sid: String,
                       count: Int) extends Ordered[SessionInfo]{
  // 对咱们业务来说, 千万不要反会0.
  // 如果返回0 在set中会去重
  override def compare(that: SessionInfo): Int = {
    if(this.count > that.count) -1
    else 1
  }
}
/*
Ordered
    让你对象具有与其他的对象排序的功能
Ordering
    比较器, 用来比较两个对象的
 */

定义累加器
new一个acc累加器包

/*
累加器类型: UserVisitAction
最终的返回值:
    计算每个品类的 点击量, 下单量, 支付量
    Map[品类, (clickCount, orderCount, payCount)]
 */
class CategoryAcc extends AccumulatorV2[UserVisitAction,mutable.Map[String,(Long,Long,Long)]]{
  //    self => // 自身类型  self === this
  // 可变map使用 val就可以了
  private val map = mutable.Map[String,(Long,Long,Long)]()
  // 判零.  判断集合是否为空
  override def isZero: Boolean = map.isEmpty
  // 复制累加器
  override def copy(): AccumulatorV2[UserVisitAction, mutable.Map[String, (Long, Long, Long)]] = {
    val acc = new CategoryAcc
    acc.map.synchronized{
      acc.map ++= this.map
    }
    acc
  }
  // 重置累加器
  override def reset(): Unit = {
    // 是可变累加器, 重置其实就是清空
    map.clear()
  }
  // 累加  (分区内聚合, 累加)
  override def add(v: UserVisitAction): Unit = {
    /*
        进来的用户行为, 有可能是 点击, 也可能是搜索(不处理), 下单, 支付

        Map[品类, (clickCount, orderCount, payCount)]
         */
    v match {
      case action if action.click_category_id != -1 =>
      // 点击的品类id
        val cid = action.click_category_id.toString
      // map中已经存储的cid的(点击量,下单量,支付量)
        val (click,order,pay) = map.getOrElse(cid,(0L,0L,0L))
        map += cid -> (click + 1L,order,pay)
      // 判断是否为下单
      case action if action.order_category_ids != "null" =>
      // "1,2,3"
        val cids: Array[String] = action.order_category_ids.split(",")
        cids.foreach(cid => {
          val (click,order,pay) = map.getOrElse(cid,(0L,0L,0L))
          map += cid -> (click,order + 1L,pay)
        })
      // 判断是否为支付
      case action if action.pay_category_ids != "null" =>
        val cids: Array[String] = action.pay_category_ids.split(",")
        cids.foreach(cid => {
          val (click,order,pay) = map.getOrElse(cid,(0L,0L,0L))
          map += cid -> (click,order,pay+1L)
        })
      // 其他行为不做处理
      case _ =>
    }
  }
  // 分区间的合并
  override def merge(other: AccumulatorV2[UserVisitAction, mutable.Map[String, (Long, Long, Long)]]): Unit = {
    // 涉及到map的合并!!!  other中的map, 合并this中的map
    other match {
      case o: CategoryAcc =>
        o.map.foreach{
          case (cid,(click,order,pay)) =>
            val (thisclick,thisorder,thispay) = map.getOrElse(cid,(0L,0L,0L))
            this.map += cid -> (thisclick+click,thisorder+order,thispay+pay)
        }
      case _ => throw new UnsupportedOperationException
    }
  }


  override def value: mutable.Map[String, (Long, Long, Long)] = map
}

具体逻辑实现
new一个app包

object ProjectApp {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("ProjectApp").setMaster("local[2]")
    val sc = new SparkContext(conf)
    //1.读数据
    val sourceRDD = sc.textFile("e:/user_visit_action.txt")
    //2.封装到样例类
    val userVisitActionRDD = sourceRDD.map(line =>{
      val splits = line.split("_")
      UserVisitAction(
        splits(0),
        splits(1).toLong,
        splits(2),
        splits(3).toLong,
        splits(4),
        splits(5),
        splits(6).toLong,
        splits(7).toLong,
        splits(8),
        splits(9),
        splits(10),
        splits(11),
        splits(12).toLong)
    })

    //userVisitActionRDD.collect().foreach(println)   //测试

    CategoryTopApp.calcCategoryTop10(sc,userVisitActionRDD)
    sc.stop()
  }
}
object CategoryTopApp {
  def calcCategoryTop10(sc: SparkContext,actionRDD: RDD[UserVisitAction]) = {
    //1.创建累加器对象
    val acc = new CategoryAcc
    //2.注册累加器
    sc.register(acc,"CategoryAcc")
    //3.遍历RDD,进行累加
    actionRDD.foreach(action => acc.add(action))
    //4.对数据进行处理top10
    val map = acc.value
//    val result = map
//      .toList
//      .sortBy(x=>x._2)(Ordering.Tuple3(Ordering.Long.reverse,Ordering.Long.reverse,Ordering.Long.reverse))
//      .take(10)
    val categoryList = map.map{
      case (cid,(click,order,pay)) =>
      CategroyCount(cid,click,order,pay)
    }.toList
    val result = categoryList
      .sortBy(x => (-x.clickCount,-x.orderCount,-x.payCount))
        .take(10)
    println(result)
  }
}

需求 2: Top10热门品类中每个品类的 Top10 活跃 Session 统计
对于排名前 10 的品类,分别获取每个品类点击次数排名前 10 的 sessionId。(注意: 这里我们只关注点击次数, 不关心下单和支付次数)
这个就是说,对于 top10 的品类,每一个都要获取对它点击次数排名前 10 的 sessionId。
这个功能,可以让我们看到,对某个用户群体最感兴趣的品类,各个品类最感兴趣最典型的用户的 session 的行为。
思路
. 过滤出来 category Top10的日志
. 需要用到需求1的结果, 然后只需要得到categoryId就可以了
. 转换结果为 RDD[(categoryId, sessionId), 1] 然后统计数量 => RDD[(categoryId, sessionId), count]
. 统计每个品类 top10. => RDD[categoryId, (sessionId, count)] => RDD[categoryId, Iterable[(sessionId, count)]]
. 对每个 Iterable[(sessionId, count)]进行排序, 并取每个Iterable的前10
. 把数据封装到 CategorySession 中
ProjectApp

object ProjectApp {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("ProjectApp").setMaster("local[2]")
    val sc = new SparkContext(conf)
    //1.读数据
    val sourceRDD = sc.textFile("e:/user_visit_action.txt")
    //2.封装到样例类
    val userVisitActionRDD = sourceRDD.map(line =>{
      val splits = line.split("_")
      UserVisitAction(
        splits(0),
        splits(1).toLong,
        splits(2),
        splits(3).toLong,
        splits(4),
        splits(5),
        splits(6).toLong,
        splits(7).toLong,
        splits(8),
        splits(9),
        splits(10),
        splits(11),
        splits(12).toLong)
    })
    //userVisitActionRDD.collect().foreach(println)
    //需求一的返回值,Top10
    val categoryCountList: List[CategroyCount] = CategoryTopApp.calcCategoryTop10(sc,userVisitActionRDD)
    //需求二的分析
    CategorySessionTopApp.statCategoryTop10Session(sc,categoryCountList,userVisitActionRDD)
    sc.stop()
  }
}

CategorySessionTopApp
方法一

object CategorySessionTopApp {
  def statCategoryTop10Session(sc: SparkContext,
                               categoryCountList: List[CategroyCount],
                               userVisitActionRDD: RDD[UserVisitAction]) = {
    // 1. 过滤出来 top10品类的所有点击记录
    // 1.1 先map出来top10的品类id
    val cids = categoryCountList.map(_.cid.toLong)
    val topCategoryActionRDD = userVisitActionRDD.filter(action => cids.contains(action.click_category_id))
    // 2. 计算每个品类 下的每个session 的点击量  rdd ((cid, sid) ,1)
    val cidAndSidCount = topCategoryActionRDD
      .map(action => ((action.click_category_id,action.session_id),1))
      .reduceByKey(_+_)
      .map{
        case ((cid,sid),count) => (cid,(sid,count))
      }
    //   3. 按照品类分组
    val cidAndSidCountGrouped: RDD[(Long, Iterable[(String, Int)])] = cidAndSidCount.groupByKey()
    // 4. 排序, 取top10
    val result = cidAndSidCountGrouped.map{
      case (cid,sidCountIt) =>
        (cid,sidCountIt.toList.sortBy(-_._2).take(10))
    }
    result.collect.foreach(println)
  }

}

缺点:toList会把所有数据加载到内存,实际生产环境中会直接oom
前面的排序是使用的 Scala 的排序操作, 由于 scala 排序的时候需要把数据全部加载到内存中才能完成排序, 所以理论上都存在内存溢出的风险.
如果使用 RDD 提供的排序功能, 可以避免内存溢出的风险, 因为 RDD 的排序需要 shuffle, 是采用了内存+磁盘来完成的排序.

方法二:

object CategorySessionTopApp {

  def statCategoryTop10Session1(sc: SparkContext,
                               categoryCountList: List[CategroyCount],
                               userVisitActionRDD: RDD[UserVisitAction]) = {
    // 1. 过滤出来 top10品类的所有点击记录
    // 1.1 先map出来top10的品类id
    val cids = categoryCountList.map(_.cid.toLong)
    val topCategoryActionRDD = userVisitActionRDD.filter(action => cids.contains(action.click_category_id))
    // 2. 计算每个品类 下的每个session 的点击量  rdd ((cid, sid) ,1)
    val cidAndSidCount = topCategoryActionRDD
      .map(action => ((action.click_category_id,action.session_id),1))
      .reduceByKey(_+_)
      .map{
        case ((cid,sid),count) => (cid,(sid,count))
      }
    // 3. 分别过滤出来没给品类的数据, 然后使用rdd的排序功能
    cidAndSidCount.cache()  //做缓存
    for(cid <- cids){
      val arr = cidAndSidCount.filter(cid == _._1)
        .sortBy(-_._2._2)
        .take(10)
      println(arr.toList)
    }
    /*优点
    利用了RDD的排序功能, 所以不会oom, 任务一定能跑完.
    缺点:
    job太多, 每个品类, 需要单独起一个job来完成.
    */
  }
}

方法三

object CategorySessionTopApp {

  def statCategoryTop10Session2(sc: SparkContext,
                               categoryCountList: List[CategroyCount],
                               userVisitActionRDD: RDD[UserVisitAction]) = {
    // 1. 过滤出来 top10品类的所有点击记录
    // 1.1 先map出来top10的品类id
    val cids = categoryCountList.map(_.cid.toLong)
    val topCategoryActionRDD = userVisitActionRDD.filter(action => cids.contains(action.click_category_id))
    // 2. 计算每个品类 下的每个session 的点击量  rdd ((cid, sid) ,1)
    val cidAndSidCount = topCategoryActionRDD
      .map(action => ((action.click_category_id,action.session_id),1))
      .reduceByKey(_+_)
      .map{
        case ((cid,sid),count) => (cid,(sid,count))
      }
    //   3. 按照品类分组
    val cidAndSidCountGrouped = cidAndSidCount.groupByKey()
    // 4. 排序, 取top10
    val result = cidAndSidCountGrouped.map{
      case (cid,sidCountIt) =>
      // sidCountIt 要排序, 但是又不想转成容器式的集合? 怎么做?
      // 如果不转, 绝对不能用scala的sortBy !
      // 找一个可以自动排序的集合(TreeSet), 只需要让TreeSet集合的长度保持10就行了.
      var set = mutable.TreeSet[SessionInfo]()
        sidCountIt.foreach {
          case (sid,count) => 
            val info = SessionInfo(sid, count)
            set += info
            if(set.size > 10) set = set.take(10)
        }
        (cid,set.toList)
    }

  }
}


找一个能够自动排序的集合: TreeSet
借助样例类SessionInfo的排序方法
只要保证TreeSet的集合的长度不超过10, 就不会内存溢出.

必须要让你的元素具有排序功能. 让样例类具有排序功能: Ordered (Comparable)

优点:不会内存溢出, 而且只有一个job
缺点:reduceByKey和groupByKey都是需要shuffle效率会很低

方法四

object CategorySessionTopApp {
  def statCategoryTop10Session(sc: SparkContext, categoryCountList: List[CategroyCount], userVisitActionRDD: RDD[UserVisitAction]) = {
    val cids = categoryCountList.map(_.cid.toLong)
    val topCategoryActionRDD = userVisitActionRDD.filter(action => cids.contains(action.click_category_id))
    val cidAndSidCount = topCategoryActionRDD
      .map(action => ((action.click_category_id,action.session_id),1))
      .reduceByKey(new CategoryPartitioner(cids),_+_)
      .map{
        case ((cid,sid),count) => (cid,(sid,count))
      }

    //3.排序取top10,mappartitions分区内为一个集体处理
    val result = cidAndSidCount.mapPartitions(it => {
      var treeSet = mutable.TreeSet[SessionInfo]()
      var id = 0L
      it.foreach{
        case (cid,(sid,count)) =>
          id = cid
          treeSet += SessionInfo(sid,count)
          if (treeSet.size > 10) treeSet = treeSet.take(10)
      }
      //treeSet.toIterator.map((id,_))
      Iterator((id,treeSet.toList))
    })
    result.collect.foreach(println)
    Thread.sleep(100000)
  }
}

//自定义分区器,有多少个品类就有多少个分区
class CategoryPartitioner(cids: List[Long]) extends Partitioner{

  private val cidWithIndex = cids.zipWithIndex.toMap

  //放回cids的长度,就是分区数
  override def numPartitions: Int = cids.length

  override def getPartition(key: Any): Int = {
    key match {
        //根据cid返回分区id
      case (cid:Long,_) =>
        //cid % 10 返回0-9分区,不过(比如10,20,30都会进入同一个分区),所以错误
        //cidwithindex转成map,传入k,就可以获取v的值
        cidWithIndex(cid)
    }
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值