数据说明:
- 数据采用_分割字段
- 每一行表示用户的一个行为, 所以每一行只能是四种行为中的一种.
- 如果搜索关键字是 null, 表示这次不是搜索
- 如果点击的品类 id 和产品 id 是 -1 表示这次不是点击
- 下单行为来说一次可以下单多个产品, 所以品类 id 和产品 id 都是多个, id 之间使用逗号,分割. 如果本次不是下单行为, 则他们相关数据用null来表示
- 支付行为和下单行为类似.
需求 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)
}
}
}