Spark学习复习(6.案例实操)

Spark案例实操

1. 案例描述

​ 在之前的学习中,我们已经学习了 Spark 的基础编程方式,接下来,我们看看在实际的工作中如何使用这些 API 实现具体的需求。这些需求是电商网站的真实需求,所以在实现功能前,咱们必须先将数据准备好。

在这里插入图片描述

​ 上面的数据图是从数据文件中截取的一部分内容,表示为电商网站的用户行为数据,主要包含用户的 4 种行为:搜索,点击,下单,支付。数据规则如下:

  • 数据文件中每行数据采用下划线分隔数据;
  • 每一行数据表示用户的一次行为,这个行为只能是 4 种行为的一种;
  • 如果搜索关键字为 null,表示数据不是搜索数据;
  • 如果点击的品类 ID 和产品 ID 为-1,表示数据不是点击数据;
  • 针对于下单行为,一次可以下单多个商品,所以品类 ID 和产品 ID 可以是多个,id 之间采用逗号分隔,如果本次不是下单行为,则数据采用 null 表示;
  • 支付行为和下单行为类似。

详细字段说明:

编号字段名称字段类型字段含义
1dateString用户点击行为的日期
2user_idLong用户的 ID
3session_idStringSession 的 ID
4page_idLong某个页面的 ID
5action_timeString动作的时间点
6search_keywordString用户搜索的关键词
7click_category_idLong某一个商品品类的 ID
8click_product_idLong某一个商品的 ID
9order_category_idsString一次订单中所有品类的 ID 集合
10order_product_idsString一次订单中所有商品的 ID 集合
11pay_category_idsString一次支付中所有品类的 ID 集合
12pay_product_idsString一次支付中所有商品的 ID 集合
13city_idLong城市 id

样例类:

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

2. 需求一

在这里插入图片描述

​ 品类是指产品的分类,大型电商网站品类分多级,咱们的项目中品类只有一级,不同的公司可能对热门的定义不一样。我们按照每个品类的点击、下单、支付的量来统计热门品类。

点击数下单数支付数
衣服点击数下单数支付数
电脑点击数下单数支付数

例如,综合排名 = 点击数*20%+下单数*30%+支付数*50%

本项目需求优化为:先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。

2.1 实现方案一
  1. 首先textFile()读取数据源。
  2. 使用filter过滤出,搜索/点击/订单/支付的数据
  3. 再使用map()和reduceByKey()得到每种数据的(品类id,搜索次数/点击次数/订单次数/支付次数)RDD
  4. 使用cogroup()把上面输出的RDD联系在一起,得到(品类id,(Interable点击,Interable订单,Interable支付))RDD
  5. 使用mapValues()将上述结果RDD,得到(品类id,(点击次数,订单次数,支付次数))
  6. 最后通过sortBy()和take()拿到最终我们需要的数据。
  7. 完整代码如下:
import org.apache.spark.{SparkConf, SparkContext}

object Req01HotCategoryTop10_One {
  def main(args: Array[String]): Unit = {

    // 1、创建Spark运行配置对象
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("req1")
    // 2、创建Spark上下文环境对象(连接对象)
    val sc = new SparkContext(sparkConf)

    // 3、读取文件数据,RDD[String]是什么?弹性分布式数据集
    val rdd = sc.textFile("data/user_visit_action.txt")

    // 首先得到搜索的数据,搜索的数据datas(5)不为null
    val searchRdd = rdd.filter(line => {
      val datas = line.split("_")
      datas(5) != "null"
    })
    val searchCountRdd = searchRdd.map(action => {
      val datas = action.split("_")
      (datas(5), 1)
    }).reduceByKey(_+_)

    // 得到点击的数据,点击数据datas(6)不为-1
    // datas(6)和datas(7)分别为点击品类ID和产品ID
    val clickRdd = rdd.filter(line => {
      val datas = line.split("_")
      datas(6) != "-1"
    })
    // 获取各个品类点击的次数(电脑,999)
    val clickCountRdd = clickRdd.map(action => {
      val datas = action.split("_")
      (datas(6), 1)
    }).reduceByKey(_+_)

    // 得到下单的数据,下单数据datas(8)和datas(9)不为null
    val orderRdd = rdd.filter(line => {
      val datas = line.split("_")
      datas(8) != "null"
    })
    // 注意这里使用flatMap,这里就体现出map和flatMap的区别了
    val orderCountRdd = orderRdd.flatMap(action => {
      val datas = action.split("_")
      // 获取下单品类ID
      val cids = datas(8).split(",")
      cids.map(cid => (cid, 1))
    }).reduceByKey(_ + _)

    // 得到下单的数据,下单数据datas(8)和datas(9)不为null
    val payRdd = rdd.filter(line => {
      val datas = line.split("_")
      datas(10) != "null"
    })

    val payCountRdd = payRdd.flatMap(action => {
      val datas = action.split("_")
      // 获取下单品类ID
      val cids = datas(10).split(",")
      cids.map(cid => (cid, 1))
    }).reduceByKey(_ + _)

    // 将品类进行排序,并且取前10名
    //    点击数量排序,下单数量排序,支付数量排序
    //    元组排序:先比较第一个,再比较第二个,再比较第三个,依此类推
    //    ( 品类ID, ( 点击数量, 下单数量, 支付数量 ) )
    //
    //  cogroup = connect + group
    val cogroupRdd = clickCountRdd.cogroup(orderCountRdd, payCountRdd)
    

    val mapValuesRdd = cogroupRdd.mapValues({
      case (clickCountIter, orderCountIter, payCountIter) => {
        var clickCount = 0
        val clickIter = clickCountIter.iterator
        if (clickIter.hasNext) {
          clickCount = clickIter.next()
        }

        var orderCount = 0
        val orderkIter = orderCountIter.iterator
        if (orderkIter.hasNext) {
          orderCount = orderkIter.next()
        }
        var payCount = 0
        val payIter = payCountIter.iterator
        if (payIter.hasNext) {
          payCount = payIter.next()
        }
        (clickCount,orderCount,payCount)
      }
    })
    val resRdd = mapValuesRdd.sortBy(_._2, false).take(10)
    // 7、打印结果
    println(s"品类id_点击数_订单数_支付数")
    resRdd.foreach(println)

    // 8、关闭spark连接
    sc.stop()
  }
}
2.2 实现方式二

第一张实现方式存在的问题:

  1. rdd重复使用
  2. cogroup性能可能较低

准对以上两个问题我们做出以下优化:

  1. 通过使用rdd.cache()来优化rdd重复使用的问题。
  2. 使用union替换cogroup,然后再使用reduceByKey()进行聚合。
  3. 其他同方式一,完整代码如下:
object Req02HotCategoryTop10_Two {
  def main(args: Array[String]): Unit = {

    // 1、创建Spark运行配置对象
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("req1")
    // 2、创建Spark上下文环境对象(连接对象)
    val sc = new SparkContext(sparkConf)

    // 3、读取文件数据,RDD[String]是什么?弹性分布式数据集
    val rdd = sc.textFile("data/user_visit_action.txt")
    rdd.cache()

    // 得到点击的数据,点击数据datas(6)不为-1
    // datas(6)和datas(7)分别为点击品类ID和产品ID
    val clickRdd = rdd.filter(line => {
      val datas = line.split("_")
      datas(6) != "-1"
    })
    // 获取各个品类点击的次数(电脑,999)
    val clickCountRdd = clickRdd.map(action => {
      val datas = action.split("_")
      (datas(6), 1)
    }).reduceByKey(_ + _)

    // 得到下单的数据,下单数据datas(8)和datas(9)不为null
    val orderRdd = rdd.filter(line => {
      val datas = line.split("_")
      datas(8) != "null"
    })
    // 注意这里使用flatMap,这里就体现出map和flatMap的区别了
    val orderCountRdd = orderRdd.flatMap(action => {
      val datas = action.split("_")
      // 获取下单品类ID
      val cids = datas(8).split(",")
      cids.map(cid => (cid, 1))
    }).reduceByKey(_ + _)

    // 得到下单的数据,下单数据datas(8)和datas(9)不为null
    val payRdd = rdd.filter(line => {
      val datas = line.split("_")
      datas(10) != "null"
    })

    val payCountRdd = payRdd.flatMap(action => {
      val datas = action.split("_")
      // 获取下单品类ID
      val cids = datas(10).split(",")
      cids.map(cid => (cid, 1))
    }).reduceByKey(_ + _)

    // TODO 在这里改进
    val clickResRdd = clickCountRdd.map {
      case (id, clickCount) => {
        (id, (clickCount, 0, 0))
      }
    }
    val orderResRdd = orderCountRdd.map {
      case (id, orderCount) => {
        (id, (0, orderCount, 0))
      }
    }
    val payResRdd = payCountRdd.map {
      case (id, payCount) => {
        (id, (0, 0, payCount))
      }
    }

    // 使用union并集将上面三个RDD并在一起
    val sourceRdd = clickResRdd.union(orderResRdd).union(payResRdd)

    val resRdd = sourceRdd.reduceByKey(
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3)
      }
    )
    val top10Rdd = resRdd.sortBy(_._2, false).take(10)

    // 7、打印结果
    println(s"品类id_点击数_订单数_支付数")
    top10Rdd.foreach(println)

    // 8、关闭spark连接
    sc.stop()
  }
}
2.3 实现方式三

第二种方式存在的问题:

  1. 存在大量的shuffle操作(reduceByKey)
  2. reduceByKey 聚合算子,spark会提供优化,缓存

准对上述问题做出以下优化:

  1. 直接通过flatMap把源数据转成(品类id,(点击次数,订单次数,支付次数)),有零。
  2. 然后其他步骤同方式二,这样减少了三次reduceByKey()
  3. 全部代码如下:
import org.apache.spark.{SparkConf, SparkContext}

object Req03HotCategoryTop10_Three {
  def main(args: Array[String]): Unit = {

    // 1、创建Spark运行配置对象
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("req1")
    // 2、创建Spark上下文环境对象(连接对象)
    val sc = new SparkContext(sparkConf)

    // 3、读取文件数据,RDD[String]是什么?弹性分布式数据集
    val rdd = sc.textFile("data/user_visit_action.txt")

    val sourceRdd = rdd.flatMap(action => {
      val datas = action.split("_")
      if (datas(6) != "-1") {
        List((datas(6), (1, 0, 0)))
      } else if (datas(8) != "null") {
        val cids = datas(8).split(",")
        cids.map(cid => {
          (cid, (0, 1, 0))
        })
      } else if (datas(10) != "null") {
        val cids = datas(10).split(",")
        cids.map(cid => {
          (cid, (0, 0, 1))
        })
      } else {
        Nil
      }
    })

    val resRdd = sourceRdd.reduceByKey(
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3)
      }
    )
    val top10Rdd = resRdd.sortBy(_._2, false).take(10)

    // 7、打印结果
    println(s"品类id_点击数_订单数_支付数")
    top10Rdd.foreach(println)

    // 8、关闭spark连接
    sc.stop()
  }
}
2.4 实现方式四

方法三的问题:

  1. 仍然存在shuffle操作

解决方案:

  1. 使用自定义累加器去解决
  2. 通过自定义累加器,我们得到(cid,(cid,点击数,下单数,支付数))
  3. 然后通过acc.value.map(_._2)得到(cid,点击数,下单数,支付数)
  4. 然后进行排序,就可以得到我们需要的数据了。
package com.atguigu.sparkstudy.core.req

import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable

object Req04HotCategoryTop10_Four {
  def main(args: Array[String]): Unit = {

    // 1、创建Spark运行配置对象
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("req1")
    // 2、创建Spark上下文环境对象(连接对象)
    val sc = new SparkContext(sparkConf)

    // 3、读取文件数据,RDD[String]是什么?弹性分布式数据集
    val rdd = sc.textFile("data/user_visit_action.txt")

    // 4、注册累加器
    val acc = new HotCategoryAccumulator
    sc.register(acc, "hotCategory")

    // 5、将数据转换结构
    rdd.foreach {
      action => {
        val datas = action.split("_")
        if (datas(6) != "-1") {
          // 点击的场合
          acc.add((datas(6), "click"))
        } else if (datas(8) != "null") {
          // 下单的场合
          val ids = datas(8).split(",")
          ids.foreach(
            id => {
              acc.add((id, "order"))
            }
          )
        } else if (datas(10) != "null") {
          // 支付的场合
          val ids = datas(10).split(",")
          ids.foreach(
            id => {
              acc.add((id, "pay"))
            }
          )
        }
      }
    }

    val accVal = acc.value
    val categories = accVal.map(_._2)

    // 进行排序
    val sort = categories.toList.sortWith(
      (left, right) => {
        if (left.clickCnt > right.clickCnt) {
          true
        } else if (left.clickCnt == right.clickCnt) {
          if (left.orderCnt > right.orderCnt) {
            true
          } else if (left.orderCnt == right.orderCnt) {
            left.payCnt > right.payCnt
          } else {
            false
          }
        } else {
          false
        }
      }
    )

    sort.take(10).foreach(println)
    // 8、关闭spark连接
    sc.stop()
  }


  /**
   * 样例类:为模式匹配优化的类,使用case声明
   * 1. 构造器中的每一个参数都成为val——除非它被显式地声明为var(不建议这样做)
   * 2. 在样例类对应的伴生对象中提供apply方法让你不用new关键字就能构造出相应的对象提供unapply方法让模式匹配可以工作
   * 3. 将自动生成toString、equals、hashCode和copy方法(有点类似模板类,直接给生成,供程序员使用)
   * 这里我们使用HotCategory样例类来表示(cid,(点击数,下单数,支付数))
   *
   * @param cid
   * @param clickCnt
   * @param orderCnt
   * @param payCnt
   */
  case class HotCategory(cid: String, var clickCnt: Int, var orderCnt: Int, var payCnt: Int)

  /**
   * 自定义累加器
   * 1. 继承AccumulatorV2,定义泛型
   * IN : ( 品类ID, 行为类型 )
   * OUT : mutable.Map[String, HotCategory]
   * 2. 重写方法(6)
   */
  class HotCategoryAccumulator extends AccumulatorV2[(String, String), mutable.Map[String, HotCategory]] {

    private val hcMap = mutable.Map[String, HotCategory]()

    override def isZero: Boolean = {
      hcMap.isEmpty
    }

    override def copy(): AccumulatorV2[(String, String), mutable.Map[String, HotCategory]] = {
      new HotCategoryAccumulator()
    }

    override def reset(): Unit = {
      hcMap.clear()
    }

    /**
     * _1 品类id
     * _2 操作类型
     *
     * @param v
     */
    override def add(v: (String, String)): Unit = {
      val cid = v._1
      val actionType = v._2
      // 根据品类id从集合中获取category,如果有则返回,如果没有则返回HotCategory(cid, 0, 0, 0)
      val category = hcMap.getOrElse(cid, HotCategory(cid, 0, 0, 0))
      if (actionType == "click") {
        category.clickCnt += 1
      } else if (actionType == "order") {
        category.orderCnt += 1
      } else if (actionType == "pay") {
        category.payCnt += 1
      }
      // 更新map
      hcMap.update(cid, category)
    }

    override def merge(other: AccumulatorV2[(String, String), mutable.Map[String, HotCategory]]): Unit = {
      val map1 = this.hcMap
      val map2 = other.value
      map2.foreach {
        case (cid, hc) => {
          val category: HotCategory = map1.getOrElse(cid, HotCategory(cid, 0, 0, 0))
          category.clickCnt += hc.clickCnt
          category.orderCnt += hc.orderCnt
          category.payCnt += hc.payCnt
          map1.update(cid, category)
        }
      }
    }

    override def value: mutable.Map[String, HotCategory] = hcMap
  }


3. 需求二

Top10 热门品类中每个品类的 Top10 活跃 Session 统计

在需求一的基础上,增加每个品类用户 session 的点击统计

解决思路:

  1. 根据需求一的结果获取Top10Cids
  2. 根据Top10Cids去过滤原始数据(只要点击数据并cid在Top10Cids中)
  3. 先使用map和reduceByKey得到((cid,sessionId),count)
  4. 再使用map转换结构(cid,(sessionId,count))
  5. 通过groupByKey进行分组
  6. 最后通过mapValues根据点击量排序取前十
  7. 完整代码如下:
package com.atguigu.sparkstudy.core.req

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Req05HotCategoryTop10Session {
  def main(args: Array[String]): Unit = {

    // 1、创建Spark运行配置对象
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("req1")
    // 2、创建Spark上下文环境对象(连接对象)
    val sc = new SparkContext(sparkConf)

    // 3、读取文件数据,RDD[String]是什么?弹性分布式数据集
    val rdd = sc.textFile("data/user_visit_action.txt")

    // 根据需求一获得的Top10的cids
    val top10Cids = top10Category(rdd)

    // 根据Top10的cids过滤,点击类数据且是Top10
    val filterRdd = rdd.filter {
      action => {
        val datas = action.split("_")
        if (datas(6) != "-1") {
          top10Cids.contains(datas(6))
        } else {
          false
        }
      }
    }

    // 根据(品类id,session)进行聚合
    val reduceByKeyRdd = filterRdd.map {
      action => {
        val datas = action.split("_")
        // ((品类id,session),1)
        ((datas(6), datas(2)), 1)
      }
    }.reduceByKey(_ + _)

    //  将统计的结果进行结构的转换
    //  (( 品类ID,sessionId ),sum) => ( 品类ID,(sessionId, sum) )
    val mapRdd = reduceByKeyRdd.map {
      // 改变数据的结构
      case ((cid, sessionId), count) => {
        (cid, (sessionId, count))
      }
    }

    // 根据cid进行分组
    val groupByKeyRdd = mapRdd.groupByKey()

    // 将分组后的数据进行点击量的排序,取前10名
    val resRdd = groupByKeyRdd.mapValues {
      iter => {
        iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(10)
      }
    }
    // 7、打印结果
    resRdd.foreach(println)

    // 8、关闭spark连接
    sc.stop()
  }

  /**
   * 获取top10的cid
   *
   * @param rdd
   */
  def top10Category(rdd: RDD[String]): Array[String] = {
    val sourceRdd = rdd.flatMap(action => {
      val datas = action.split("_")
      if (datas(6) != "-1") {
        List((datas(6), (1, 0, 0)))
      } else if (datas(8) != "null") {
        val cids = datas(8).split(",")
        cids.map(cid => {
          (cid, (0, 1, 0))
        })
      } else if (datas(10) != "null") {
        val cids = datas(10).split(",")
        cids.map(cid => {
          (cid, (0, 0, 1))
        })
      } else {
        Nil
      }
    })

    val resRdd = sourceRdd.reduceByKey(
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3)
      }
    )
    val top10Rdd = resRdd.sortBy(_._2, false).take(10)

    top10Rdd.map(_._1)
  }
}

4. 需求三

3.1 需求说明

页面单跳转化率

  • 计算页面单跳转化率,什么是页面单跳转换率,比如一个用户在一次 Session 过程中访问的页面路径 3,5,7,9,10,21,那么页面 3 跳到页面 5 叫一次单跳,7-9 也叫一次单跳, 那么单跳转化率就是要统计页面点击的概率。
  • 比如:计算 3-5 的单跳转化率,先获取符合条件的 Session 对于页面 3 的访问次数(PV) 为 A,然后获取符合条件的 Session 中访问了页面 3 又紧接着访问了页面 5 的次数为 B,那么 B/A 就是 3-5 的页面单跳转化率。
    -在这里插入图片描述

统计页面单跳转化率意义

  • 产品经理和运营总监,可以根据这个指标,去尝试分析,整个网站,产品,各个页面的表现怎么样,是不是需要去优化产品的布局;吸引用户最终可以进入最后的支付页面。
  • 数据分析师,可以此数据做更深一步的计算和分析。
  • 企业管理层,可以看到整个公司的网站,各个页面的之间的跳转的表现如何,可以适当调整公司的经营战略或策略。
3.2 图解分析

在这里插入图片描述
在这里插入图片描述

3.3 实现方式

实现思路:根据图解我们需要分别计算分子和分母

package com.atguigu.sparkstudy.core.req

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Req06PageflowAnalysis {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Pageflow")
    val sc = new SparkContext(sparkConf)
    // 读取文件
    val rdd = sc.textFile("data/user_visit_action.txt")
    val actionRDD = rdd.map(
      action => {
        val datas = action.split("_")
        UserVisitAction(
          datas(0),
          datas(1).toLong,
          datas(2),
          datas(3).toLong,
          datas(4),
          datas(5),
          datas(6).toLong,
          datas(7).toLong,
          datas(8),
          datas(9),
          datas(10),
          datas(11),
          datas(12).toLong
        )
      }
    )
    // 根据图解我们需要分别计算分子和分母


    // TODO 计算分母
    val ids = List[Long](1, 2, 3, 4, 5, 6, 7)
    // List((1,2), (2,3), (3,4), (4,5), (5,6), (6,7))
    val okflowIds = ids.zip(ids.tail)

    val tempRdd = actionRDD.filter(
      action => {
        ids.contains(action.page_id)
      }
    ).map(
      action => {
        // 先获取想获取页面的(page_id,1)
        (action.page_id, 1L)
      }
      // 通过reduceByKey进行聚合(page_id,sum)
    ).reduceByKey(_ + _)
    val denominatorRDD = tempRdd.collect().toMap

    // TODO 计算分子
    // 根据sessionId进行分组
    val groupRDD = actionRDD.groupBy(_.session_id)

    /**
     * (1b338bbe-2ed4-43da-984f-1160ac311e8d,
     * CompactBuffer(
     * UserVisitAction(2019-07-23,99,1b338bbe-2ed4-43da-984f-1160ac311e8d,3,2019-07-23 03:09:03,null,2,59,null,null,null,null,24),
     * UserVisitAction(2019-07-23,99,1b338bbe-2ed4-43da-984f-1160ac311e8d,16,2019-07-23 03:09:11,苹果,-1,-1,null,null,null,null,14)
     * )
     * )
     */
    val mvRDD = groupRDD.mapValues(
      iter => {
        // 分组后,根据访问时间进行排序(升序)
        val sortList = iter.toList.sortBy(_.action_time)
        val flowIds = sortList.map(_.page_id)
        val pageflowIds = flowIds.zip(flowIds.tail)
        // 将不合法的页面跳转进行过滤
        pageflowIds.filter(
          t => {
            okflowIds.contains(t)
          }
        ).map(
          t => {
            (t, 1)
          }
        )
      }
    )
    val flatRDD: RDD[((Long, Long), Int)] = mvRDD.map(_._2).flatMap(list=>list)

    val dataRDD = flatRDD.reduceByKey(_ + _)

    // TODO 计算单跳转换率 分子除以分母
    dataRDD.foreach{
      case ( (pageid1, pageid2), sum ) => {
        val lon: Long = denominatorRDD.getOrElse(pageid1, 0L)

        println(s"页面${pageid1}跳转到页面${pageid2}单跳转换率为:" + ( sum.toDouble/lon ))
      }
    }
  }

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

5. 工程化

MVC三层架构

在这里插入图片描述

三层架构

在这里插入图片描述

WordCountApplication
object WordCountApplication extends App with TApplication{
  start(){
    val controller = new WordCountController()
    controller.dispatch()
  }
}
WordCountController
/**
 * 控制层
 */
class WordCountController extends TController{
  private val wordCountService = new WordCountService()

  def dispatch(): Unit ={

    val wordCountArray = wordCountService.dataAnalysis()

    wordCountArray.foreach(println)
  }
}
WordCountService
/**
 * 服务层
 */
class WordCountService extends TService{
  private val wordCountDao = new WordCountDao()

  def dataAnalysis() = {
    val fileRDD: RDD[String] = wordCountDao.readFile("data/word.txt")
    val words: RDD[String] = fileRDD.flatMap(_.split(" "))
    val wordGroup: RDD[(String, Iterable[String])] = words.groupBy(word => word)
    val wordToCount = wordGroup.map {
      case (word, list) => {
        (word, list.size)
      }
    }
    val resArray: Array[(String, Int)]  = wordToCount.collect()
    resArray
  }
}
WordCountDao
/**
 * 持久层
 */
class WordCountDao extends TDao{

}
common
TApplication
trait TApplication  {
  def start(master:String ="local[*]", app:String = "Application")( op : => Unit ): Unit = {
    val sparkConf = new SparkConf().setMaster(master).setAppName(app)
    val sc = new SparkContext(sparkConf)
    EnvUtil.put(sc)
    try {
      op
    } catch {
      case ex => println(ex.getMessage)
    }
    sc.stop()
    EnvUtil.clear()
  }
}
TController
trait TController {
    def dispatch(): Unit
}
TService
trait TService {
    def dataAnalysis():Any
}
TDao
trait TDao {

    def readFile(path:String) = {
        EnvUtil.take().textFile(path)
    }
}
ThreadLocal的妙用-EnvUtil
package com.atguigu.sparkstudy.core.framework.utils
import org.apache.spark.SparkContext

object EnvUtil {
  private val scLocal = new ThreadLocal[SparkContext]()

  def put( sc : SparkContext ): Unit = {
    scLocal.set(sc)
  }

  def take(): SparkContext = {
    scLocal.get()
  }

  def clear(): Unit = {
    scLocal.remove()
  }
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱敲代码的小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值