前言
本章节对项目的sessionStat
模块进行解析,该模块负责用户访问session的统计,代码实现前四个需求:
- session访问步长/访问时长占比统计
- 按比例随机抽取session
- top10热门商品类统计
- 热门品类活跃session统计
一、任务流程及逻辑分析
整个用户访问session统计任务的流程图如下
1. 从数据表user_visit_action提取所有原始数据
数据表user_visit_action的数据模型如下
2. 将原始数据根据session进行聚合
首先将原始数据转换为 K-V 结构,以原始数据里的session
作为key,以原始数据
作为value。
得到的数据结构为(session_id,(date,user_id,session_id,...,city_id))
然后根据session_id进行聚合。
聚合完成后,得到的数据结构应为:
(session_id,(date,user_id,session_id,...,city_id)
(date,user_id,session_id,...,city_id)
(date,user_id,session_id,...,city_id)
... ...)
数据类型为(session_id, Iterable[UserVisitAction])
,iterable可以通过map遍历其内元素。
3. 提取关键数据字段
每个需求的实现都需要对user_visit_action表进行数据处理,会造成开销,为避免这种浪费,将得到的聚合数据进行关键字段的提取,得到提取后的数据sessionId2GroupRDD
,其数据格式如下:
Session_id | Search_Keywords | Click_Category_id | Visit_Length | Step_Length | Start_Time
比如,需求一只会用到Visit_Length和Step_Length,多提取出的Search_Keywords、Click_Category_id、Start_Time是为后面的需求做准备。
4. 联立user_info表
实际情况下,只希望获取指定范围的user群体的session,所以要对每条数据添加用户信息字段。
sessionId2GroupRDD
与user_info
通过主键user_id
联立后,得到的完整信息数据如下:
Session_id | Search_Keywords | Click_Category_id | Visit_Length | Step_Length | Start_Time | Age | Professional | Sex | City
5. 数据过滤
按照指定条件对完整信息数据进行过滤,符合条件的session返回到sessionId2FilteredRDD。
6. 更新累加器,统计结果
过滤后符合条件的session,得到其时长和步长,判断属于哪一段后,更新累加器的值。
7. 数据封装,写入MySql
二、主体代码解析
本章节只进行主体部分的代码解析,四个需求的实现都基于主体部分的代码,将在后续章节解析
1. main函数
def main(args: Array[String]): Unit = {
//得到配置信息的json对象
val jsonStr = ConfigurationManager.config.getString(Constants.TASK_PARAMS)
val taskParam = JSONObject.fromObject(jsonStr)
//创建UUID
val taskUUID = UUID.randomUUID().toString
//创建spark入口
val sparkSession = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate()
//流程里的第一步,从数据表user_visit_action提取所有原始数据
val actionRDD = getBasicActionData(sparkSession, taskParam)
//流程里的第二步,先转换为K-V结构,再进行聚合操作
val sessionId2action1 = actionRDD.map(item => (item.session_id, item))
val sessionId2action = sessionId2action1.groupByKey()
//缓存持久化
sessionId2action.cache()
//流程里的第三步和第四步,获取关键信息数据,再根据主键user_id联立数据表user_info,添加用户信息字段,获得完整信息数据
val sessionId2FullInfoRDD = getSessionFullInfo(sparkSession, sessionId2action)
//先定义好累加器,使用前必须注册
val sessionStatisticAccumulator = new SessionStatAccumulator
sparkSession.sparkContext.register(sessionStatisticAccumulator)
//流程里的第五第六步,过滤数据,然后更新累加器
val sessionId2filteredRDD = getFilterRDD(sparkSession, taskParam, sessionStatisticAccumulator, sessionId2FullInfoRDD)
//执行一个action操作,至此就是主体代码
sessionId2filteredRDD.foreach(println(_))
//以下是四个需求的方法声明
//需求一:各范围session占比统计
getSessionRatio(sparkSession, taskUUID, sessionStatisticAccumulator.value)
// 需求二:Session随机抽取
sessionRandomExtract(sparkSession, taskUUID, sessionId2FullInfoRDD)
//需求三:Top10热门品类统计
//先得到符合过滤条件的action数据
val sessionId2FilteredActionRDD = sessionId2action1.join(sessionId2filteredRDD).map {
case (session, (action, fullInfo)) => (session, action)
}
//为第四个需求准备,此时top10Category是Array(sortKey,fullInfo[cid|clickCount|orderCount|payCount])
val top10Category = getTop10PopularCategories(sparkSession, taskUUID, sessionId2FilteredActionRDD)
//需求四:统计每一个Top10热门品类的top10活跃session
//sessionId2filteredRDD:过滤后符合条件的action:(sessionId,action)
getTop10ActiveSession(sparkSession,taskUUID,top10Category,sessionId2FilteredActionRDD)
}
2. getBasicActionData()
getBasicActionData()方法,从数据表user_visit_action提取所有原始数据。
def getBasicActionData(sparkSession: SparkSession, taskParm: JSONObject) = {
val startDate = ParamUtils.getParam(taskParm, Constants.PARAM_START_DATE)
val endDate = ParamUtils.getParam(taskParm, Constants.PARAM_END_DATE)
val sql = "select * from user_visit_action where date >='" + startDate + "' and date <= '" + endDate + "'"
import sparkSession.implicits._
sparkSession.sql(sql).as[UserVisitAction].rdd
}
返回值解析:sparkSession.sql(sql)
的返回值类型为DataFrame
也就是DataSet[Row]
; sparkSession.sql(sql).as[UserVisitAction]
的返回值类型为DataSet[UserVisitAction]
; sparkSession.sql(sql).as[UserVisitAction].rdd
的返回值类型为rdd[UserVisitAction]
3. getSessionFullInfo()
getSessionFullInfo()方法,获得完整信息数据。
首先获取关键信息数据,再根据主键user_id联立数据表user_info,添加用户信息字段,从而获得完整信息数据
def getSessionFullInfo(sparkSession: SparkSession,
sessionId2action: RDD[(String, Iterable[UserVisitAction])]) = {
//首先获取关键信息数据
val userId2AggrInfoRDD = sessionId2action.map {
case (sessionId, iterableAction) =>
val startTime
val endTime
val searchKeyWords
val clickCategories
val userId
val stepLength
for (action <- iterableAction) { update field }
//拼接数据字段
val aggrInfo = sessionId | searchKw | clickCG | visitLength | stepLength | startTime
//返回以userId作为key的 键值对
(userId, aggrInfo)
}
//从用户信息表user_info提取数据,并转换为同样以user_id为主键的 键值对
val sql = "select * from user_info"
val userId2UserInfoRDD = sparkSession.sql(sql).as[UserInfo].rdd.map(item => (item.user_id, item))
//联立两张表
//userIdAggrInfoRDD --(userId,aggrInfo)
//userId2UserInfo --(userId,UserInfo)
val session2FullInfoRDD = userId2AggrInfoRDD.join(userId2UserInfoRDD).map {
case (userId, (aggrInfo, userInfo)) =>
val fullInfo = aggrInfo | Age | Professional | Sex | City
//至此,已经得到了想要的完整信息数据 fullInfo,不再需要user_id作为key,而是,从aggrInfo中取出session_id作为key
val sessionId = StringUtils.getFieldFromConcatString(aggrInfo, "\\|", Constants.FIELD_SESSION_ID)
(sessionId, fullInfo)
}
session2FullInfoRDD
}
4. getFilteredRDD
getFilteredRDD()方法,根据条件获取过滤后的数据,并基于合格数据更新累加器
def getFilteredRDD(sparkSession: SparkSession,
taskParam: JSONObject,
sessionStatisticAccumulator: SessionStatisticAccumulator,
sessionId2FullInfoRDD: RDD[(String, String)]) :RDD[(String, String)]= {
//传入过滤条件
val startAge = ParamUtils.getParam(taskParam, Constants.PARAM_START_AGE)
val endAge =
val professional =
val cities =
val sex =
val searchKeywords =
val clickCategories =
//拼接过滤条件
val filterInfo = startAge= |endAge= |... |clickCategories=
//执行过滤,获得过滤后的数据
val sessionId2FilterRDD = sessionId2FullInfoRDD.filter{
case(sessionId, fullInfo) =>
var success = true
if (!ValidUtils.between/in/equal(fullInfo, Field, filterInfo, Parma)){
success = false
}
//判断success的状态,如果为true说明该sessionId2FullInfoRDD是满足条件的需要使用自定义累加器对相关需求累加
if (success){
//对符合筛选条件的session数目进行累加
sessionStatisticAccumulator.add(Constants.SESSION_COUNT)
//对session时长进行分段累加
def calculateVisitLength(visitLength: Long): Unit = {
if(){
}else if(){
...}
//对session步长进行分段累加
def calculateStepLength(stepLength: Long): Unit ={if... else if...}
// 从sessionId2FullInfoRDD中取出session时长和步长
val stepLength = StringUtils.getFieldFromConcatString(fullInfo, "\\|", Constants.FIELD_STEP_LENGTH).toLong
val visitLength = StringUtils.getFieldFromConcatString(fullInfo, "\\|", Constants.FIELD_VISIT_LENGTH).toLong
// 判断session中的步长和时长位于哪个段中,累加
calculateStepLength(stepLength)
calculateVisitLength(visitLength)
}
success
}
sessionId2FilterRDD
}
5.SessionStatisticAccumulator
自定义累加器,用于对session数目、各段步长数目和各段时长数目进行累计
class SessionStatisticAccumulator extends AccumulatorV2[String, mutable.HashMap[String, Int]]{
// 累加器对象,真正存储的容器
val countMap = new mutable.HashMap[String, Int]()
// 返回这个累加器是否为零值
override def isZero: Boolean = countMap.isEmpty
// 复制得到这个累加器的一个新副本
override def copy(): AccumulatorV2[String, mutable.HashMap[String, Int]] = {
val acc = new SessionStatisticAccumulator
acc.countMap ++= this.countMap
acc
}
// 把累加器重置为零值
override def reset(): Unit = countMap.clear()
// Takes the inputs and accumulates.输入一个string,如果countMap里匹配到key,就加一,如果没有,就创建一个新key,加一
override def add(v: String): Unit = {
// 如果匹配不到,就添加一个key,其value为0
if (!countMap.contains(v)){
countMap += (v -> 0)
}
// 对该key的value加1
countMap.update(v, countMap(v)+1)
}
// 将另一个相同类型的累加器合并到这个累加器中,并更新其状态
override def merge(other: AccumulatorV2[String, mutable.HashMap[String, Int]]): Unit = {
other match {
case acc:SessionStatisticAccumulator =>
acc.countMap.foldLeft(this.countMap){
case (map, (k, v))=> map += (k -> (map.getOrElse(k, 0) + v))
}
}
}
// 此累加器的当前值
override def value: mutable.HashMap[String, Int] = {
this.countMap
}
}
- sessionStat模块主体部分的代码解析就ok了,后续章节进行具体需求的代码实现。