数据格式:
数据文件中每行数据采用下划线分隔数据
每一行数据表示用户的一次行为,这个行为只能是 4 种行为的一种
如果搜索关键字为 null,表示数据不是搜索数据
如果点击的品类 ID 和产品 ID 为-1,表示数据不是点击数据
针对于下单行为,一次可以下单多个商品,所以品类 ID 和产品 ID 可以是多个,id 之 间采用逗号分隔,如果本次不是下单行为,则数据采用 null 表示
支付行为和下单行为类似
需求:按照每个品类的点击、下单、支付的量来统计热门品类(先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数)
分析:数据可以统计成(品类,点击总数)(品类,下单总数)(品类,支付总数)这种格式来进行汇总,就简化成了WordCount问题
在实际的解决中我们可以进一步分析解决将数据转化成(品类,(点击,下单,支付))来解决
代码:
object demand {
def main(args: Array[String]): Unit = {
val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("demand"))
val rdd: RDD[String] = sc.textFile("datas/user_visit_action.txt")
// TODO 分别统计每个品类点击的次数,下单的次数和支付的次数: (品类,点击总数)(品类,下单总数)(品类,支付总数)
val accumulator = new MyAccumulator
sc.register(accumulator,"demand")
// 第一种 :将数据扁平化整理成(品类,(点击,下单,支付)),按key值进行累加即可
// rdd.flatMap(
// value => {
// val values: Array[String] = value.split("_")
// if (values(6) != "-1") {
// List((values(6), (1, 0, 0)))
// } else if (values(8) != "null") {
// val ids: Array[String] = values(8).split(",")
// ids.map(
// id => (id, (0, 1, 0))
// )
// } else if (values(10) != "null") {
// val ids: Array[String] = values(10).split(",")
// ids.map(
// id => (id, (0, 0, 1))
// )
// }else {
// Nil // 空集合
// }
// }
// ).reduceByKey(
// (t1,t2) => {
// (t1._1+t2._1,t1._2+t2._2,t1._3+t2._3)
// }
// ).sortBy(_._2,false).take(10).foreach(println)
// 第二种:使用累加器来避免shuffle
rdd.foreach(
value => {
val values: Array[String] = value.split("_")
if (values(6) != "-1") {
accumulator.add(values(6),"click")
} else if (values(8) != "null") {
val ids: Array[String] = values(8).split(",")
ids.foreach(
id => accumulator.add(id,"order")
)
} else if (values(10) != "null") {
val ids: Array[String] = values(10).split(",")
ids.foreach(
id => accumulator.add(id,"pay")
)
}
}
)
// 要自定义比较规则
accumulator.value.map(_._2).toList.sortWith(
(l,r) =>{
if (l.clickCnt > r.clickCnt){
true
}else if (l.clickCnt == r.clickCnt){
if (l.orderCnt > r.orderCnt){
true
}else if (l.orderCnt == r.orderCnt){
if (l.payCnt > r.payCnt){
true
}else false
}else false
}else false
}
).take(10).foreach(println)
sc.stop()
}
// 辅助类
case class UserVisitAction(cid:String,var clickCnt:Int,var orderCnt:Int,var payCnt:Int)
// 自定义累加器
class MyAccumulator extends AccumulatorV2[(String,String),mutable.Map[String, UserVisitAction]]{
private var values: mutable.Map[String,UserVisitAction] = mutable.Map[String,UserVisitAction]()
// 累加器是否为初始状态
override def isZero: Boolean = values.isEmpty
// 复制累加器
override def copy(): AccumulatorV2[(String,String), mutable.Map[String,UserVisitAction]] = {
new MyAccumulator()
}
// 重置累加器
override def reset(): Unit = values.clear()
// 向累加器中添加数据
override def add(v: (String,String)): Unit = {
val cid: String = v._1
val action: String = v._2
// 获取品类对应的UserVisitAction数据,从而来实现对相应操作的累加赋值
val action1: UserVisitAction = values.getOrElse(cid, UserVisitAction(cid, 0, 0, 0))
if (action == "click"){
action1.clickCnt += 1
}else if (action == "order"){
action1.orderCnt += 1
}else if (action == "pay"){
action1.payCnt += 1
}
// 数据一定要进行更新
values.update(cid,action1)
}
// 合并累加器
override def merge(other: AccumulatorV2[(String,String), mutable.Map[String, UserVisitAction]]): Unit = {
// 你将要输出的map
val map1 = this.values
// 相对于输出的map以外的来自其他task任务返回的map
val map2 = other.value
// 对结果进行merge合并
map2.foreach{
case (cid,use) => {
val action: UserVisitAction = map1.getOrElse(cid, UserVisitAction(cid, 0, 0, 0))
action.clickCnt += use.clickCnt
action.orderCnt += use.orderCnt
action.payCnt += use.payCnt
// 一定要记得对数据更新到你要输出的map中去
map1.update(cid,action)
}
}
}
// 返回累加器的结果
override def value: mutable.Map[String, UserVisitAction] = values
}
}
在解决问题时,由于spark有三种数据结构:RDD、累加器、广播变量,分别解决了一些问题,我们可以配合的使用它们来优化程序,由于计算时shuffle过程涉及磁盘IO,所有可以通过累加器的使用来避免shuffle提高效率。