文章目录
第5章 Spark案例实操
在之前的学习中,我们已经学习了Spark的基础编程方式,接下来,我们看看在实际的工作中如何使用这些API实现具体的需求。这些需求是电商网站的真实需求,所以在实现功能前,咱们必须先将数据准备好。
上面的数据图是从数据文件中截取的一部分内容,表示为电商网站的用户行为数据,主要包含用户的4种行为:搜索,点击,下单,支付。数据规则如下:
-
数据文件中每行数据采用下划线隔数据
-
每一行数据表示用户的一次行为,这个行为只能是4种行为的一种
-
如果搜索关键字为null,表示数据不是搜索数据
-
如果点击的品类ID和产品ID为-1,表示数据不是点击数据
-
针对于下单行为,一次可以下单多个商品,所以品类ID和产品ID可以是多个,id之间采用逗号分隔,如果本次不是下单行为,则数据采用null表示
-
支付行为和下单行为类似
详细字段说明:
编号 | 字段名称 | 字段类型 | 字段含义 |
---|---|---|---|
1 | date | String | 用户点击行为的日期 |
2 | user_id | Long | 用户的ID |
3 | session_id | String | Session的ID |
4 | page_id | Long | 某个页面的ID |
5 | action_time | String | 动作的时间点 |
6 | search_keyword | String | 用户搜索的关键词 |
7 | click_category_id | Long | 某一个商品品类的ID |
8 | click_product_id | Long | 某一个商品的ID |
9 | order_category_ids | String | 一次订单中所有品类的ID集合 |
10 | order_product_ids | String | 一次订单中所有商品的ID集合 |
11 | pay_category_ids | String | 一次支付中所有品类的ID集合 |
12 | pay_product_ids | String | 一次支付中所有商品的ID集合 |
13 | city_id | Long | 城市 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
)
6.1 需求1:Top10热门品类
6.1.1 需求说明
品类是指产品的分类,大型电商网站品类分多级,咱们的项目中品类只有一级,不同的公司可能对热门的定义不一样。我们按照每个品类的点击、下单、支付的量来统计热门品类。
鞋 点击数 下单数 支付数
衣服 点击数 下单数 支付数
电脑 点击数 下单数 支付数
例如,综合排名 = 点击数20%+下单数30%+支付数*50%
本项目需求优化为:先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。
6.1.2 实现方案一
6.1.2.1 需求分析
分别统计每个品类点击的次数,下单的次数和支付的次数:
(品类,点击总数)(品类,下单总数)(品类,支付总数)
6.1.2.2 需求实现
package com.atguigu.bigdata.spark.core.rdd.req
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Req1_HotCategoryTop10Analysis {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local").setAppName("RDDCreate")
val sc = new SparkContext(sparkConf)
// TODO 1.获取原始数据
val actionRDD = sc.textFile("datas/user_visit_action.txt")
// TODO 2.分别统计一个品类的点击数量,下单数量,支付数量
// 2.1 品类的点击数量的统计
// 保留点击数据,其他数据不要
val clickActionRDD = actionRDD.filter(
action => {
val datas = action.split("_")
datas(6) != "-1"
}
)
//(品类ID,1 click)=> (品类ID,sum click)
val clickRDD = clickActionRDD.map(
action => {
val datas = action.split("_")
(datas(6), 1)
}
)
// (品类ID,sum click)
val totalClickRDD = clickRDD.reduceByKey(_ + _)
// 2.2 品类的下单数量的统计
// 保留下单数据,其他数据不要
val orderActionRDD = actionRDD.filter(
action => {
val datas = action.split("_")
datas(8) != "null"
}
)
// (品类ID,1 order)=> (品类ID,sum order)
val orderRDD = orderActionRDD.flatMap(
action => {
val datas = action.split("_")
val categoryids = datas(8)
val ids = categoryids.split(",")
ids.map((_, 1))
}
)
// (品类ID,sum order)
val totalOrderRDD = orderRDD.reduceByKey(_ + _)
// 2.3 品类的支付数量的统计
val payActionRDD = actionRDD.filter(
action => {
val datas = action.split("_")
datas(10) != "null"
}
)
// (品类ID,1 pay)=> (品类ID,sum pay)
val payRDD = payActionRDD.flatMap(
action => {
val datas = action.split("_")
val categoryids = datas(10)
val ids = categoryids.split(",")
ids.map((_, 1))
}
)
// (品类ID,sum pay)
val totalPayRDD = payRDD.reduceByKey(_ + _)
// TODO 3. 将统计结果进行排行(点击,下单,支付)
// Scala可以将无关的数据封装为一个整体来使用,可以使用元组
// 元组在scala中的排序方式,首先按照第一个元素排序,相同者,按照第二个元素排序,依此类推
// (品类ID,(点击数量,下单数量,支付数量))
val clickLeftJoinOrderRDD: RDD[(String, (Int, Option[Int]))] = totalClickRDD.leftOuterJoin(totalOrderRDD)
val clickOrderPayRDD: RDD[(String, ((Int, Option[Int]), Option[Int]))] = clickLeftJoinOrderRDD.leftOuterJoin(totalPayRDD)
// ((Int, Option[Int]), Option[Int]))
// ((点击,下单),支付)
val wantRDD = clickOrderPayRDD.map {
case (cid, ((click, optOrder), optPay)) => {
val order = optOrder.getOrElse(0)
val pay = optPay.getOrElse(0)
(cid, ((click, order), pay))
}
}
// TODO 4. 取前10名
//根据(click, order), pay)排序,false是降序,take取前10
val result: Array[(String, ((Int, Int), Int))] = wantRDD.sortBy(_._2, false).take(10)
result.foreach(println)
sc.stop()
}
}
6.1.3 实现方案二
6.1.3.1 需求分析
一次性统计每个品类点击的次数,下单的次数和支付的次数:
(品类,(点击总数,下单总数,支付总数))
6.1.3.2 需求实现
package com.atguigu.bigdata.spark.core.rdd.req
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Req1_HotCategoryTop10Analysis2 {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local").setAppName("RDDCreate")
val sc = new SparkContext(sparkConf)
// TODO 1. 获取原始数据
val actionRDD = sc.textFile("datas/user_visit_action.txt")
actionRDD.cache()
// TODO 2. 分别统计一个品类的点击数量,下单数量,支付数量
// Q : actionRDD,读取文件中,重复使用?
// A : 即使对象重复使用,但是数据无法重复使用,因为RDD不保存数据,从头执行数据操作
// 可以采用持久化操作
// Q : reduceByKey算子被调用的次数过多,性能下降。?
// A : 极限情况下,使用一个reduceByKey实现功能
// Q : 连接3个数据源时,会导致性能下降?
// A : 改变数据结构,提升计算性能
//点击
val clickActionRDD = actionRDD.filter(
action => {
val datas: Array[String] = action.split("_")
datas(6) != "-1"
}
)
val clickRDD = clickActionRDD.map(
action => {
val datas: Array[String] = action.split("_")
( datas(6), 1 )
}
)
// (品类ID,1)=> ( 品类,(1,0,0) )
// reduceByKey丢弃???
val totalClickRDD = clickRDD.reduceByKey(_+_)
// 订单
val orderActionRDD = actionRDD.filter(
action => {
val datas: Array[String] = action.split("_")
datas(8) != "null"
}
)
val orderRDD = orderActionRDD.flatMap(
action => {
val datas: Array[String] = action.split("_")
val categoryids = datas(8)
val ids = categoryids.split(",")
ids.map( (_, 1) )
}
)
// (品类ID,1) => (品类,(0,1,0))
// reduceByKey是否丢弃???
val totalOrderRDD: RDD[(String, Int)] = orderRDD.reduceByKey(_+_)
//支付
val payActionRDD = actionRDD.filter(
action => {
val datas: Array[String] = action.split("_")
datas(10) != "null"
}
)
val payRDD = payActionRDD.flatMap(
action => {
val datas: Array[String] = action.split("_")
val categoryids = datas(10)
val ids = categoryids.split(",")
ids.map( (_, 1) )
}
)
// (品类ID, 1)=> (品类,(0,0,1))
// reduceByKey丢弃???
val totalPayRDD: RDD[(String, Int)] = payRDD.reduceByKey(_+_)
// TODO 3. 将统计结果进行排行(降序)( 点击, 下单, 支付 )
// ( 品类ID, (点击数量, 0, 0) )
val newClickRDD = totalClickRDD.map{
case ( id, clickCnt ) => {
( id, (clickCnt, 0, 0) )
}
}
// ( 品类ID, (0, 下单数量, 0) )
val newOrderRDD = totalOrderRDD.map {
case ( id, orderCnt ) => {
( id, (0, orderCnt, 0) )
}
}
// ( 品类ID, (0, 0, 支付数量) )
val newPayRDD = totalPayRDD.map {
case ( id, payCnt ) => {
( id, (0, 0, payCnt) )
}
}
// (1, (1,0,0))
// (1, (0,2,0))
// (1, (0,0,3))
// => (1, (1,2,3))
// union
val rdd: RDD[(String, (Int, Int, Int))] = newClickRDD.union(newOrderRDD).union(newPayRDD)
// ( 品类ID, ( 点击数量,下单数量,支付数量 ) )
val wantRDD = rdd.reduceByKey{
case ( (c1, o1, p1), (c2, o2, p2) ) => {
( c1 + c2, o1 + o2, p1 + p2 )
}
}
// TODO 4. 取前10名
val result: Array[(String, (Int, Int, Int))] = wantRDD.sortBy(_._2, false).take(10)
result.foreach(println)
sc.stop
}
}
方案二优化:
package com.atguigu.bigdata.spark.core.rdd.req
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Req1_HotCategoryTop10Analysis3 {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local").setAppName("RDDCreate")
val sc = new SparkContext(sparkConf)
// TODO 1. 获取原始数据
val actionRDD = sc.textFile("datas/user_visit_action.txt")
// TODO 2. 将原始数据进行结构的转换
//点击 => ( 品类, (1,0,0) )
//下单 => ( 品类1,(0,1,0)), ( 品类2,(0,1,0))
//支付 => (品类1,(0,0,1)), (品类2,(0,0,1))
// map : 数据不会增多,只是 A => B
// flatMap : 数据可能会增多
val flatRDD: RDD[(String, (Int, Int, Int))] = actionRDD.flatMap(
action => {
val datas = action.split("_")
if (datas(6) != "-1") {
// 点击
List((datas(6), (1, 0, 0)))
} else if ((datas(8) != "null")) {
// 下单
List((datas(8), (0, 1, 0)))
} else if ((datas(10) != "null")) {
// 支付
List((datas(10), (0, 0, 1)))
} else {
Nil
}
}
)
val resultRDD: RDD[(String, (Int, Int, Int))] = flatRDD.reduceByKey {
case ((c1, o1, p1), (c2, o2, p2)) => {
(c1 + c2, o1 + o2, p1 + p2)
}
}
resultRDD.sortBy(_._2, false).take(10).foreach(println)
sc.stop()
}
}
6.1.4 实现方案三
6.1.4.1 需求分析
使用累加器的方式聚合数据
6.1.4.2 需求实现
package com.atguigu.bigdata.spark.core.rdd.req
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable
object Req1_HotCategoryTop10Analysis4 {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local").setAppName("RDDCreate")
val sc = new SparkContext(sparkConf)
// TODO 1. 获取原始数据
val actionRDD = sc.textFile("datas/user_visit_action.txt")
// 不使用shuffle操作完成数据统计
// 创建累加器
val acc = new HotCategoryAccumulator()
sc.register(acc, "hotCategory")
actionRDD.foreach(
action => {
val datas = action.split("_")
if (datas(6) != "-1") {
//点击
acc.add(datas(6), "click") //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 hotCategoryMap: mutable.Map[String, HotCategory] = acc.value //拿到map的结果
// TODO 将累加的结果进行排序(降序)和取前10名
// Map[String, HotCategory] 只需要 HotCategory
// map没有排序功能,需要toList
// sortWith,这里不是tuple,是一个一个的对象,对象需要用自定义的规则,sortWith有左右
hotCategoryMap.map(_._2).toList.sortWith(
(left, right) => {
if (left.clickCount > right.clickCount) {
true
} else if (left.clickCount == right.clickCount) {
if (left.orderCount > right.orderCount) {
true
} else if (left.orderCount == right.orderCount) {
left.payCount > right.payCount
} else {
false
}
} else {
false
}
}
).take(10).foreach(println) //取前10
sc.stop()
}
// 输入IN: (品类,行为类型)
// 输出OUT: Map[品类,HotCategory] , HotCategory中封装(点击数量,下单数量,支付数量)
class HotCategoryAccumulator extends AccumulatorV2[(String, String), mutable.Map[String, HotCategory]] {
private val hotCategoryMap = mutable.Map[String, HotCategory]()
override def isZero: Boolean = {
hotCategoryMap.isEmpty //初始化,就是没有值
}
override def copy(): AccumulatorV2[(String, String), mutable.Map[String, HotCategory]] = {
new HotCategoryAccumulator()
}
override def reset(): Unit = {
hotCategoryMap.clear() //清除
}
override def add(v: (String, String)): Unit = {
val id = v._1 //品类id
val actionType = v._2 //行为类型
// 判断map里面有没有这个id,有就拿过来,没有就用新的
val hotCategory = hotCategoryMap.getOrElse(id, HotCategory(id, 0, 0, 0))
// 行为类型之中有三种,用match匹配
actionType match {
case "click" => hotCategory.clickCount += 1 // 是哪种类型就加1
case "order" => hotCategory.orderCount += 1
case "pay" => hotCategory.payCount += 1
}
// 增加:从之前的map中取出旧的值,根据条件进行更新,更新后再放回去
// 把这个map更新
hotCategoryMap.update(id, hotCategory)
}
// merge 就是两个map的合并,合并当前和其他的
override def merge(other: AccumulatorV2[(String, String), mutable.Map[String, HotCategory]]): Unit = {
other.value.foreach { // 这个value是map:Map[String, HotCategory]
case (id, otherHC) => {
val thisHC = hotCategoryMap.getOrElse(id, HotCategory(id, 0, 0, 0))
thisHC.clickCount += otherHC.clickCount
thisHC.orderCount += otherHC.orderCount
thisHC.payCount += otherHC.payCount
// 更新一下
hotCategoryMap.update(id, thisHC)
}
}
}
// 返回
override def value: mutable.Map[String, HotCategory] = hotCategoryMap
}
// 样例类
case class HotCategory(
var categoryId: String,
var clickCount: Long,
var orderCount: Long,
var payCount: Long,
)
}
测试数据百度网盘链接: 链接:https://pan.baidu.com/s/1t-4PeV8hiGpHYs_17LCFUw?pwd=aza1 提取码:aza1