1、用户访问Session介绍
用户在电商网站上,通常会有很多的点击行为,首页通常都是进入首页;然后可能点击首页上的一些商品;点击首页上的一些品类;也可能随时在搜索框里面搜索关键词;还可能将一些商品加入购物车;对购物车中的多个商品下订单;最后对订单中的多个商品进行支付。
用户的每一次操作,其实可以理解为一个action,比如点击、搜索、下单、支付
用户session,指的就是,从用户第一次进入首页,session就开始了。然后在一定时间范围内,直到最后操作完(可能做了几十次、甚至上百次操作)。离开网站,关闭浏览器,或者长时间没有做操作;那么session就结束了。
以上用户在网站内的访问过程,就称之为一次session。简单理解,session就是某一天某一个时间段内,某个用户对网站从打开/进入,到做了大量操作,到最后关闭浏览器。的过程。就叫做session。
session实际上就是一个电商网站中最基本的数据和大数据。那么大数据,面向C端,也就是customer,消费者,用户端的,分析,基本是最基本的就是面向用户访问行为/用户访问session。
2、用户访问Session分析模块介绍
1、按条件筛选session
2、统计出符合条件的session中,访问时长在1s~3s、4s~6s、7s~9s、10s~30s、30s~60s、1m~3m、3m~10m、10m~30m、30m以上各个范围内的session占比;访问步长在1~3、4~6、7~9、10~30、30~60、60以上各个范围内的session占比
3、在符合条件的session中,按照时间比例随机抽取1000个session
4、在符合条件的session中,获取点击、下单和支付数量排名前10的品类
5、对于排名前10的品类,分别获取其点击次数排名前10的session
详细介绍
1、按条件筛选session
搜索过某些关键词的用户、访问时间在某个时间段内的用户、年龄在某个范围内的用户、职业在某个范围内的用户、所在某个城市的用户,发起的session。找到对应的这些用户的session,也就是我们所说的第一步,按条件筛选session。
这个功能,就最大的作用就是灵活。也就是说,可以让使用者,对感兴趣的和关系的用户群体,进行后续各种复杂业务逻辑的统计和分析,那么拿到的结果数据,就是只是针对特殊用户群体的分析结果;而不是对所有用户进行分析的泛泛的分析结果。比如说,现在某个企业高层,就是想看到用户群体中,28~35岁的,老师职业的群体,对应的一些统计和分析的结果数据,从而辅助高管进行公司战略上的决策制定。
2、统计出符合条件的session中,访问时长在1s~3s、4s~6s、7s~9s、10s~30s、30s~60s、1m~3m、3m~10m、10m~30m、30m以上各个范围内的session占比;访问步长在1~3、4~6、7~9、10~30、30~60、60以上各个范围内的session占比
session访问时长,也就是说一个session对应的开始的action,到结束的action,之间的时间范围;还有,就是访问步长,指的是,一个session执行期间内,依次点击过多少个页面,比如说,一次session,维持了1分钟,那么访问时长就是1m,然后在这1分钟内,点击了10个页面,那么session的访问步长,就是10.
比如说,符合第一步筛选出来的session的数量大概是有1000万个。那么里面,我们要计算出,访问时长在1s~3s内的session的数量,并除以符合条件的总session数量(比如1000万),比如是100万/1000万,那么1s~3s内的session占比就是10%。依次类推,这里说的统计,就是这个意思。
这个功能的作用,其实就是,可以让人从全局的角度看到,符合某些条件的用户群体,使用我们的产品的一些习惯。比如大多数人,到底是会在产品中停留多长时间,大多数人,会在一次使用产品的过程中,访问多少个页面。那么对于使用者来说,有一个全局和清晰的认识。
3、在符合条件的session中,按照时间比例随机抽取1000个session
这个按照时间比例是什么意思呢?随机抽取本身是很简单的,但是按照时间比例,就很复杂了。比如说,这一天总共有1000万的session。那么我现在总共要从这1000万session中,随机抽取出来1000个session。但是这个随机不是那么简单的。需要做到如下几点要求:首先,如果这一天的12:00~13:00的session数量是100万,那么这个小时的session占比就是1/10,那么这个小时中的100万的session,我们就要抽取1/10 * 1000 = 100个。然后再从这个小时的100万session中,随机抽取出100个session。以此类推,其他小时的抽取也是这样做。
这个功能的作用,是说,可以让使用者,能够对于符合条件的session,按照时间比例均匀的随机采样出1000个session,然后观察每个session具体的点击流/行为,比如先进入了首页、然后点击了食品品类、然后点击了雨润火腿肠商品、然后搜索了火腿肠罐头的关键词、接着对王中王火腿肠下了订单、最后对订单做了支付。
之所以要做到按时间比例随机采用抽取,就是要做到,观察样本的公平性。
4、在符合条件的session中,获取点击、下单和支付数量排名前10的品类
什么意思呢,对于这些session,每个session可能都会对一些品类的商品进行点击、下单和支付等等行为。那么现在就需要获取这些session点击、下单和支付数量排名前10的最热门的品类。也就是说,要计算出所有这些session对各个品类的点击、下单和支付的次数,然后按照这三个属性进行排序,获取前10个品类。
这个功能,很重要,就可以让我们明白,就是符合条件的用户,他最感兴趣的商品是什么种类。这个可以让公司里的人,清晰地了解到不同层次、不同类型的用户的心理和喜好。
5、对于排名前10的品类,分别获取其点击次数排名前10的session
这个就是说,对于top10的品类,每一个都要获取对它点击次数排名前10的session。
这个功能,可以让我们看到,对某个用户群体最感兴趣的品类,各个品类最感兴趣最典型的用户的session的行为。
3、针对Session模块5个功能的技术方案设计
1、按条件筛选session
这里首先提出第一个问题,你要按条件筛选session,但是这个筛选的粒度是不同的,比如说搜索词、访问时间,那么这个都是session粒度的,甚至是action粒度的;那么还有,就是针对用户的基础信息进行筛选,年龄、性别、职业。所以说筛选粒度是不统一的。
第二个问题,就是说,我们的每天的用户访问数据量是很大的,因为user_visit_action这个表,一行就代表了用户的一个行为,比如点击或者搜索;那么在国内一个大的电商企业里面,如果每天的活跃用户数量在千万级别的话。那么可以告诉大家,这个user_visit_action表,每天的数据量大概在至少5亿以上,在10亿左右。
那么针对这个筛选粒度不统一的问题,以及数据量巨大(10亿/day),可能会有两个问题;首先第一个,就是,如果不统一筛选粒度的话,那么就必须得对所有的数据进行全量的扫描;第二个,就是全量扫描的话,量实在太大了,一天如果在10亿左右,那么10天呢(100亿),100呢,1000亿。量太大的话,会导致Spark作业的运行速度大幅度降低。极大的影响平台使用者的用户体验。
所以为了解决这个问题,那么我们选择在这里,对原始的数据,进行聚合,什么粒度的聚合呢?session粒度的聚合。也就是说,用一些最基本的筛选条件,比如时间范围,从hive表中提取数据,然后呢,按照session_id这个字段进行聚合,那么聚合后的一条记录,就是一个用户的某个session在指定时间内的访问的记录,比如搜索过的所有的关键词、点击过的所有的品类id、session对应的userid关联的用户的基础信息。
聚合过后,针对session粒度的数据,按照使用者指定的筛选条件,进行数据的筛选。筛选出来符合条件的用session粒度的数据。其实就是我们想要的那些session了。
关键技术点:通过底层数据聚合,来减少spark作业处理数据量,从而提升spark作业的性能(从根本上提升spark性能的技巧)
2、聚合统计
如果要做这个事情,那么首先要明确,我们的spark作业是分布式的。所以也就是说,每个spark task在执行我们的统计逻辑的时候,可能就需要对一个全局的变量,进行累加操作。比如代表访问时长在1s~3s的session数量,初始是0,然后呢分布式处理所有的session,判断每个session的访问时长,如果是1s~3s内的话,那么就给1s~3s内的session计数器,累加1。
那么在spark中,要实现分布式安全的累加操作,基本上只有一个最好的选择,就是Accumulator变量。但是,问题又来了,如果是基础的Accumulator变量,那么可能需要将近20个Accumulator变量,1s~3s、4s~6s。。。。;但是这样的话,就会导致代码中充斥了大量的Accumulator变量,导致维护变得更加复杂,在修改代码的时候,很可能会导致错误。比如说判断出一个session访问时长在4s~6s,但是代码中不小心写了一个bug(由于Accumulator太多了),比如说,更新了1s~3s的范围的Accumulator变量。导致统计出错。
所以,对于这个情况,那么我们就可以使用自定义Accumulator的技术,来实现复杂的分布式计算。也就是说,就用一个Accumulator,来计算所有的指标。
关键技术点:自定义Accumulator实现复杂分布式计算的技术
3、在符合条件的session中,按照时间比例随机抽取1000个session
这个呢,需求上已经明确了。那么剩下的就是具体的实现了。具体的实现这里不多说,技术上来说,就是要综合运用Spark的countByKey、groupByKey、mapToPair等算子,来开发一个复杂的按时间比例随机均匀采样抽取的算法。
关键技术点:Spark按时间比例随机抽取算法
4、在符合条件的session中,获取点击、下单和支付数量排名前10的品类
这里的话呢,需要对每个品类的点击、下单和支付的数量都进行计算。然后呢,使用Spark的自定义Key二次排序算法的技术,来实现所有品类,按照三个字段,点击数量、下单数量、支付数量依次进行排序,首先比较点击数量,如果相同的话,那么比较下单数量,如果还是相同,那么比较支付数量。
关键技术点:Spark自定义key二次排序技术
5、对于排名前10的品类,分别获取其点击次数排名前10的session
这个需求,需要使用Spark的分组取TopN的算法来进行实现。也就是说对排名前10的品类对应的数据,按照品类id进行分组,然后求出每组点击数量排名前10的session。
关键技术点:Spark分组取TopN算法
4、表结构设计
第一表:session_aggr_stat表,存储第一个功能,session聚合统计的结果
第二个表:session_random_extract表,存储我们的按时间比例随机抽取功能抽取出来的1000个session
第三个表:top10_category表,存储按点击、下单和支付排序出来的top10品类数据
第四个表:top10_category_session表,存储top10每个品类的点击top10的session
第五个表:session_detail,用来存储随机抽取出来的session的明细数据、top10品类的session的明细数据
最后一张表:task表,用来存储J2EE平台插入其中的任务的信息
注意:该表中task_params的存储格式为:text,也就是说该字段将会以json的格式存储任务的相关信息。
5、编码实现
5.1 按条件筛选Session
5.1.1 按session粒度进行数据聚合
package cn.ctgu.sparkproject.spark;
import java.util.Iterator;
import org.apache.spark.SparkConf;
import org.apache.spark.SparkContext;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.sql.DataFrame;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SQLContext;
import org.apache.spark.sql.hive.HiveContext;
import scala.Tuple2;
import com.alibaba.fastjson.JSONObject;
import com.ibeifeng.sparkproject.conf.ConfigurationManager;
import com.ibeifeng.sparkproject.constant.Constants;
import com.ibeifeng.sparkproject.dao.ITaskDAO;
import com.ibeifeng.sparkproject.dao.impl.DAOFactory;
import com.ibeifeng.sparkproject.domain.Task;
import com.ibeifeng.sparkproject.test.MockData;
import com.ibeifeng.sparkproject.util.ParamUtils;
import com.ibeifeng.sparkproject.util.StringUtils;
/**
* 用户访问session分析Spark作业
*
* 接收用户创建的分析任务,用户可能指定的条件如下:
*
* 1、时间范围:起始日期~结束日期
* 2、性别:男或女
* 3、年龄范围
* 4、职业:多选
* 5、城市:多选
* 6、搜索词:多个搜索词,只要某个session中的任何一个action搜索过指定的关键词,那么session就符合条件
* 7、点击品类:多个品类,只要某个session中的任何一个action点击过某个品类,那么session就符合条件
*
* 我们的spark作业如何接受用户创建的任务?
*
* J2EE平台在接收用户创建任务的请求之后,会将任务信息插入MySQL的task表中,任务参数以JSON格式封装在task_param
* 字段中
*
* 接着J2EE平台会执行我们的spark-submit shell脚本,并将taskid作为参数传递给spark-submit shell脚本
* spark-submit shell脚本,在执行时,是可以接收参数的,并且会将接收的参数,传递给Spark作业的main函数
* 参数就封装在main函数的args数组中
*
* 这是spark本身提供的特性
*
* @author Administrator
*
*/
public class UserVisitSessionAnalyzeSpark {
public static void main(String[] args) {
// 构建Spark上下文
SparkConf conf = new SparkConf()
.setAppName(Constants.SPARK_APP_NAME_SESSION)
.setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
SQLContext sqlContext = getSQLContext(sc.sc());
// 生成模拟测试数据
mockData(sc, sqlContext);
// 创建需要使用的DAO组件
ITaskDAO taskDAO = DAOFactory.getTaskDAO();
// 首先得查询出来指定的任务,并获取任务的查询参数
long taskid = ParamUtils.getTaskIdFromArgs(args);
Task task = taskDAO.findById(taskid);
JSONObject taskParam = JSONObject.parseObject(task.getTaskParam());
// 如果要进行session粒度的数据聚合
// 首先要从user_visit_action表中,查询出来指定日期范围内的行为数据
JavaRDD<Row> actionRDD = getActionRDDByDateRange(sqlContext, taskParam);
// 首先,可以将行为数据,按照session_id进行groupByKey分组
// 此时的数据的粒度就是session粒度了,然后呢,可以将session粒度的数据
// 与用户信息数据,进行join
// 然后就可以获取到session粒度的数据,同时呢,数据里面还包含了session对应的user的信息
JavaPairRDD<String, String> sessionid2AggrInfoRDD =
aggregateBySession(sqlContext, actionRDD);
// 关闭Spark上下文
sc.close();
}
/**
* 获取SQLContext
* 如果是在本地测试环境的话,那么就生成SQLContext对象
* 如果是在生产环境运行的话,那么就生成HiveContext对象
* @param sc SparkContext
* @return SQLContext
*/
private static SQLContext getSQLContext(SparkContext sc) {
boolean local = ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
return new SQLContext(sc);
} else {
return new HiveContext(sc);
}
}
/**
* 生成模拟数据(只有本地模式,才会去生成模拟数据)
* @param sc
* @param sqlContext
*/
private static void mockData(JavaSparkContext sc, SQLContext sqlContext) {
boolean local = ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
MockData.mock(sc, sqlContext);
}
}
/**
* 获取指定日期范围内的用户访问行为数据
* @param sqlContext SQLContext
* @param taskParam 任务参数
* @return 行为数据RDD
*/
private static JavaRDD<Row> getActionRDDByDateRange(
SQLContext sqlContext, JSONObject taskParam) {
String startDate = ParamUtils.getParam(taskParam, Constants.PARAM_START_DATE);
String endDate = ParamUtils.getParam(taskParam, Constants.PARAM_END_DATE);
String sql =
"select * "
+ "from user_visit_action "
+ "where date>='" + startDate + "' "
+ "and date<='" + endDate + "'";
DataFrame actionDF = sqlContext.sql(sql);
return actionDF.javaRDD();
}
/**
* 对行为数据按session粒度进行聚合
* @param actionRDD 行为数据RDD
* @return session粒度聚合数据
*/
private static JavaPairRDD<String, String> aggregateBySession(
SQLContext sqlContext, JavaRDD<Row> actionRDD) {
// 现在actionRDD中的元素是Row,一个Row就是一行用户访问行为记录,比如一次点击或者搜索
// 我们现在需要将这个Row映射成<sessionid,Row>的格式
JavaPairRDD<String, Row> sessionid2ActionRDD = actionRDD.mapToPair(
/**
* PairFunction
* 第一个参数,相当于是函数的输入
* 第二个参数和第三个参数,相当于是函数的输出(Tuple),分别是Tuple第一个和第二个值
*/
new PairFunction<Row, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(Row row) throws Exception {
return new Tuple2<String, Row>(row.getString(2), row);
}
});
// 对行为数据按session粒度进行分组
JavaPairRDD<String, Iterable<Row>> sessionid2ActionsRDD =
sessionid2ActionRDD.groupByKey();
// 对每一个session分组进行聚合,将session中所有的搜索词和点击品类都聚合起来
// 到此为止,获取的数据格式,如下:<userid,partAggrInfo(sessionid,searchKeywords,clickCategoryIds)>
JavaPairRDD<Long, String> userid2PartAggrInfoRDD = sessionid2ActionsRDD.mapToPair(
new PairFunction<Tuple2<String,Iterable<Row>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(Tuple2<String, Iterable<Row>> tuple)
throws Exception {
String sessionid = tuple._1;
Iterator<Row> iterator = tuple._2.iterator();
StringBuffer searchKeywordsBuffer = new StringBuffer("");
StringBuffer clickCategoryIdsBuffer = new StringBuffer("");
Long userid = null;
// 遍历session所有的访问行为
while(iterator.hasNext()) {
// 提取每个访问行为的搜索词字段和点击品类字段
Row row = iterator.next();
if(userid == null) {
userid = row.getLong(1);
}
String searchKeyword = row.getString(5);
Long clickCategoryId = row.getLong(6);
// 实际上这里要对数据说明一下
// 并不是每一行访问行为都有searchKeyword何clickCategoryId两个字段的
// 其实,只有搜索行为,是有searchKeyword字段的
// 只有点击品类的行为,是有clickCategoryId字段的
// 所以,任何一行行为数据,都不可能两个字段都有,所以数据是可能出现null值的
// 我们决定是否将搜索词或点击品类id拼接到字符串中去
// 首先要满足:不能是null值
// 其次,之前的字符串中还没有搜索词或者点击品类id
if(StringUtils.isNotEmpty(searchKeyword)) {
if(!searchKeywordsBuffer.toString().contains(searchKeyword)) {
searchKeywordsBuffer.append(searchKeyword + ",");
}
}
if(clickCategoryId != null) {
if(!clickCategoryIdsBuffer.toString().contains(
String.valueOf(clickCategoryId))) {
clickCategoryIdsBuffer.append(clickCategoryId + ",");
}
}
}
String searchKeywords = StringUtils.trimComma(searchKeywordsBuffer.toString());
String clickCategoryIds = StringUtils.trimComma(clickCategoryIdsBuffer.toString());
// 大家思考一下
// 我们返回的数据格式,即使<sessionid,partAggrInfo>
// 但是,这一步聚合完了以后,其实,我们是还需要将每一行数据,跟对应的用户信息进行聚合
// 问题就来了,如果是跟用户信息进行聚合的话,那么key,就不应该是sessionid
// 就应该是userid,才能够跟<userid,Row>格式的用户信息进行聚合
// 如果我们这里直接返回<sessionid,partAggrInfo>,还得再做一次mapToPair算子
// 将RDD映射成<userid,partAggrInfo>的格式,那么就多此一举
// 所以,我们这里其实可以直接,返回的数据格式,就是<userid,partAggrInfo>
// 然后跟用户信息join的时候,将partAggrInfo关联上userInfo
// 然后再直接将返回的Tuple的key设置成sessionid
// 最后的数据格式,还是<sessionid,fullAggrInfo>
// 聚合数据,用什么样的格式进行拼接?
// 我们这里统一定义,使用key=value|key=value
String partAggrInfo = Constants.FIELD_SESSION_ID + "=" + sessionid + "|"
+ Constants.FIELD_SEARCH_KEYWORDS + "=" + searchKeywords + "|"
+ Constants.FIELD_CLICK_CATEGORY_IDS + "=" + clickCategoryIds;
return new Tuple2<Long, String>(userid, partAggrInfo);
}
});
// 查询所有用户数据,并映射成<userid,Row>的格式
String sql = "select * from user_info";
JavaRDD<Row> userInfoRDD = sqlContext.sql(sql).javaRDD();
JavaPairRDD<Long, Row> userid2InfoRDD = userInfoRDD.mapToPair(
new PairFunction<Row, Long, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, Row> call(Row row) throws Exception {
return new Tuple2<Long, Row>(row.getLong(0), row);
}
});
// 将session粒度聚合数据,与用户信息进行join
JavaPairRDD<Long, Tuple2<String, Row>> userid2FullInfoRDD =
userid2PartAggrInfoRDD.join(userid2InfoRDD);
// 对join起来的数据进行拼接,并且返回<sessionid,fullAggrInfo>格式的数据
JavaPairRDD<String, String> sessionid2FullAggrInfoRDD = userid2FullInfoRDD.mapToPair(
new PairFunction<Tuple2<Long,Tuple2<String,Row>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<Long, Tuple2<String, Row>> tuple)
throws Exception {
String partAggrInfo = tuple._2._1;
Row userInfoRow = tuple._2._2;
String sessionid = StringUtils.getFieldFromConcatString(
partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
int age = userInfoRow.getInt(3);
String professional = userInfoRow.getString(4);
String city = userInfoRow.getString(5);
String sex = userInfoRow.getString(6);
String fullAggrInfo = partAggrInfo + "|"
+ Constants.FIELD_AGE + "=" + age + "|"
+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
+ Constants.FIELD_CITY + "=" + city + "|"
+ Constants.FIELD_SEX + "=" + sex;
return new Tuple2<String, String>(sessionid, fullAggrInfo);
}
});
return sessionid2FullAggrInfoRDD;
}
}
5.1.2 按筛选参数对Session粒度聚合数据进行过滤
/**
* 过滤session数据
* @param sessionid2AggrInfoRDD
* @return
*/
private static JavaPairRDD<String, String> filterSession(
JavaPairRDD<String, String> sessionid2AggrInfoRDD,
final JSONObject taskParam) {
// 为了使用我们后面的ValieUtils,所以,首先将所有的筛选参数拼接成一个连接串
// 此外,这里其实大家不要觉得是多此一举
// 其实我们是给后面的性能优化埋下了一个伏笔
String startAge = ParamUtils.getParam(taskParam, Constants.PARAM_START_AGE);
String endAge = ParamUtils.getParam(taskParam, Constants.PARAM_END_AGE);
String professionals = ParamUtils.getParam(taskParam, Constants.PARAM_PROFESSIONALS);
String cities = ParamUtils.getParam(taskParam, Constants.PARAM_CITIES);
String sex = ParamUtils.getParam(taskParam, Constants.PARAM_SEX);
String keywords = ParamUtils.getParam(taskParam, Constants.PARAM_KEYWORDS);
String categoryIds = ParamUtils.getParam(taskParam, Constants.PARAM_CATEGORY_IDS);
String _parameter = (startAge != null ? Constants.PARAM_START_AGE + "=" + startAge + "|" : "")
+ (endAge != null ? Constants.PARAM_END_AGE + "=" + endAge + "|" : "")
+ (professionals != null ? Constants.PARAM_PROFESSIONALS + "=" + professionals + "|" : "")
+ (cities != null ? Constants.PARAM_CITIES + "=" + cities + "|" : "")
+ (sex != null ? Constants.PARAM_SEX + "=" + sex + "|" : "")
+ (keywords != null ? Constants.PARAM_KEYWORDS + "=" + keywords + "|" : "")
+ (categoryIds != null ? Constants.PARAM_CATEGORY_IDS + "=" + categoryIds: "");
if(_parameter.endsWith("\\|")) {
_parameter = _parameter.substring(0, _parameter.length() - 1);
}
final String parameter = _parameter;
// 根据筛选参数进行过滤
JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD = sessionid2AggrInfoRDD.filter(
new Function<Tuple2<String,String>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, String> tuple) throws Exception {
// 首先,从tuple中,获取聚合数据
String aggrInfo = tuple._2;
// 接着,依次按照筛选条件进行过滤
// 按照年龄范围进行过滤(startAge、endAge)
if(!ValidUtils.between(aggrInfo, Constants.FIELD_AGE,
parameter, Constants.PARAM_START_AGE, Constants.PARAM_END_AGE)) {
return false;
}
// 按照职业范围进行过滤(professionals)
// 互联网,IT,软件
// 互联网
if(!ValidUtils.in(aggrInfo, Constants.FIELD_PROFESSIONAL,
parameter, Constants.PARAM_PROFESSIONALS)) {
return false;
}
// 按照城市范围进行过滤(cities)
// 北京,上海,广州,深圳
// 成都
if(!ValidUtils.in(aggrInfo, Constants.FIELD_CITY,
parameter, Constants.PARAM_CITIES)) {
return false;
}
// 按照性别进行过滤
// 男/女
// 男,女
if(!ValidUtils.equal(aggrInfo, Constants.FIELD_SEX,
parameter, Constants.PARAM_SEX)) {
return false;
}
// 按照搜索词进行过滤
// 我们的session可能搜索了 火锅,蛋糕,烧烤
// 我们的筛选条件可能是 火锅,串串香,iphone手机
// 那么,in这个校验方法,主要判定session搜索的词中,有任何一个,与筛选条件中
// 任何一个搜索词相当,即通过
if(!ValidUtils.in(aggrInfo, Constants.FIELD_SEARCH_KEYWORDS,
parameter, Constants.PARAM_KEYWORDS)) {
return false;
}
// 按照点击品类id进行过滤
if(!ValidUtils.in(aggrInfo, Constants.FIELD_CLICK_CATEGORY_IDS,
parameter, Constants.PARAM_CATEGORY_IDS)) {
return false;
}
return true;
}
});
return filteredSessionid2AggrInfoRDD;
}
5.2 Session聚合统计
统计出来之前通过条件过滤的session,访问时长在0s~3s的session的数量,占总session数量的比例;4s~6s。。。。;
访问步长在1~3的session的数量,占总session数量的比例;4~6。。。;
Accumulator 1s_3s = sc.accumulator(0L);
。。
。。
。。
十几个Accumulator
可以对过滤以后的session,调用foreach也可以,遍历所有session;计算每个session的访问时长和访问步长;
访问时长:把session的最后一个action的时间,减去第一个action的时间
访问步长:session的action数量
计算出访问时长和访问步长以后,根据对应的区间,找到对应的Accumulator,1s_3s.add(1L)
同时每遍历一个session,就可以给总session数量对应的Accumulator,加1
最后用各个区间的session数量,除以总session数量,就可以计算出各个区间的占比了
这种传统的实现方式,有什么不好???
最大的不好,就是Accumulator太多了,不便于维护
首先第一,很有可能,在写后面的累加代码的时候,比如找到了一个4s~6s的区间的session,但是却代码里面不小心,累加到7s~9s里面去了;
第二,当后期,项目如果要出现一些逻辑上的变更,比如说,session数量的计算逻辑,要改变,就得更改所有Accumulator对应的代码;或者说,又要增加几个范围,那么又要增加多个Accumulator,并且修改对应的累加代码;维护成本,相当之高(甚至可能,修改一个小功能,或者增加一个小功能,耗费的时间,比做一个新项目还要多;甚至于,还修改出了bug,那就耗费更多的时间)
所以,我们这里的设计,不打算采用传统的方式,用十几个,甚至二十个Accumulator,因为维护成本太高
这里的实现思路是,我们自己自定义一个Accumulator,实现较为复杂的计算逻辑,一个Accumulator维护了所有范围区间的数量的统计逻辑
低耦合,如果说,session数量计算逻辑要改变,那么不用变更session遍历的相关的代码;只要维护一个Accumulator里面的代码即可;
如果计算逻辑后期变更,或者加了几个范围,那么也很方便,不用多加好几个Accumulator,去修改大量的代码;只要维护一个Accumulator里面的代码即可;
维护成本,大大降低
自定义Accumulator,也是Spark Core中,属于比较高端的一个技术
使用自定义Accumulator,大家就可以任意的实现自己的复杂分布式计算的逻辑
如果说,你的task,分布式,进行复杂计算逻辑,那么是很难实现的(借助于redis,维护中间状态,借助于zookeeper去实现分布式锁)
但是,使用自定义Accumulator,可以更方便进行中间状态的维护,而且不用担心并发和锁的问题
5.2.1 自定义Accumulator
package cn.ctgu.sparkproject.spark;
import org.apache.spark.AccumulatorParam;
import cn.ctgu.sparkproject.constant.Constants;
import cn.ctgu.sparkproject.util.StringUtils;
/**
* session聚合统计Accumulator
*
* 大家可以看到
* 其实使用自己定义的一些数据格式,比如String,甚至说,我们可以自己定义model,自己定义的类(必须可序列化)
* 然后呢,可以基于这种特殊的数据格式,可以实现自己复杂的分布式的计算逻辑
* 各个task,分布式在运行,可以根据你的需求,task给Accumulator传入不同的值
* 根据不同的值,去做复杂的逻辑
*
* Spark Core里面很实用的高端技术
*
* @author Administrator
*
*/
public class SessionAggrStatAccumulator implements AccumulatorParam<String> {
private static final long serialVersionUID = 6311074555136039130L;
/**
* zero方法,其实主要用于数据的初始化
* 那么,我们这里,就返回一个值,就是初始化中,所有范围区间的数量,都是0
* 各个范围区间的统计数量的拼接,还是采用一如既往的key=value|key=value的连接串的格式
*/
@Override
public String zero(String v) {
return Constants.SESSION_COUNT + "=0|"
+ Constants.TIME_PERIOD_1s_3s + "=0|"
+ Constants.TIME_PERIOD_4s_6s + "=0|"
+ Constants.TIME_PERIOD_7s_9s + "=0|"
+ Constants.TIME_PERIOD_10s_30s + "=0|"
+ Constants.TIME_PERIOD_30s_60s + "=0|"
+ Constants.TIME_PERIOD_1m_3m + "=0|"
+ Constants.TIME_PERIOD_3m_10m + "=0|"
+ Constants.TIME_PERIOD_10m_30m + "=0|"
+ Constants.TIME_PERIOD_30m + "=0|"
+ Constants.STEP_PERIOD_1_3 + "=0|"
+ Constants.STEP_PERIOD_4_6 + "=0|"
+ Constants.STEP_PERIOD_7_9 + "=0|"
+ Constants.STEP_PERIOD_10_30 + "=0|"
+ Constants.STEP_PERIOD_30_60 + "=0|"
+ Constants.STEP_PERIOD_60 + "=0";
}
/**
* addInPlace和addAccumulator
* 可以理解为是一样的
*
* 这两个方法,其实主要就是实现,v1可能就是我们初始化的那个连接串
* v2,就是我们在遍历session的时候,判断出某个session对应的区间,然后会用Constants.TIME_PERIOD_1s_3s
* 所以,我们,要做的事情就是
* 在v1中,找到v2对应的value,累加1,然后再更新回连接串里面去
*
*/
@Override
public String addInPlace(String v1, String v2) {
return add(v1, v2);
}
@Override
public String addAccumulator(String v1, String v2) {
return add(v1, v2);
}
/**
* session统计计算逻辑
* @param v1 连接串
* @param v2 范围区间
* @return 更新以后的连接串
*/
private String add(String v1, String v2) {
// 校验:v1为空的话,直接返回v2
if(StringUtils.isEmpty(v1)) {
return v2;
}
// 使用StringUtils工具类,从v1中,提取v2对应的值,并累加1
String oldValue = StringUtils.getFieldFromConcatString(v1, "\\|", v2);
if(oldValue != null) {
// 将范围区间原有的值,累加1
int newValue = Integer.valueOf(oldValue) + 1;
// 使用StringUtils工具类,将v1中,v2对应的值,设置成新的累加后的值
return StringUtils.setFieldInConcatString(v1, "\\|", v2, String.valueOf(newValue));
}
return v1;
}
}
5.2.2 Session聚合统计之计算统计结果并写入MySQL
package cn.ctgu.sparkproject.dao;
import cn.ctgu.sparkproject.domain.SessionAggrStat;
/**
* session聚合统计模块DAO接口
* @author Administrator
*
*/
public interface ISessionAggrStatDAO {
/**
* 插入session聚合统计结果
* @param sessionAggrStat
*/
void insert(SessionAggrStat sessionAggrStat);
}
package cn.ctgu.sparkproject.dao.impl;
import cn.ctgu.sparkproject.dao.ISessionAggrStatDAO;
import cn.ctgu.sparkproject.domain.SessionAggrStat;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/**
* session聚合统计DAO实现类
* @author Administrator
*
*/
public class SessionAggrStatDAOImpl implements ISessionAggrStatDAO {
/**
* 插入session聚合统计结果
* @param sessionAggrStat
*/
public void insert(SessionAggrStat sessionAggrStat) {
String sql = "insert into session_aggr_stat "
+ "values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
Object[] params = new Object[]{sessionAggrStat.getTaskid(),
sessionAggrStat.getSession_count(),
sessionAggrStat.getVisit_length_1s_3s_ratio(),
sessionAggrStat.getVisit_length_4s_6s_ratio(),
sessionAggrStat.getVisit_length_7s_9s_ratio(),
sessionAggrStat.getVisit_length_10s_30s_ratio(),
sessionAggrStat.getVisit_length_30s_60s_ratio(),
sessionAggrStat.getVisit_length_1m_3m_ratio(),
sessionAggrStat.getVisit_length_3m_10m_ratio(),
sessionAggrStat.getVisit_length_10m_30m_ratio(),
sessionAggrStat.getVisit_length_30m_ratio(),
sessionAggrStat.getStep_length_1_3_ratio(),
sessionAggrStat.getStep_length_4_6_ratio(),
sessionAggrStat.getStep_length_7_9_ratio(),
sessionAggrStat.getStep_length_10_30_ratio(),
sessionAggrStat.getStep_length_30_60_ratio(),
sessionAggrStat.getStep_length_60_ratio()};
JDBCHelper jdbcHelper = JDBCHelper.getInstance();
jdbcHelper.executeUpdate(sql, params);
}
}
UserVisitSessionAnalyzeSpark.java(这里没有使用自定义的Accumulator)
package com.ibeifeng.sparkproject.spark;
import java.util.Date;
import java.util.Iterator;
import org.apache.spark.Accumulator;
import org.apache.spark.SparkConf;
import org.apache.spark.SparkContext;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.sql.DataFrame;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SQLContext;
import org.apache.spark.sql.hive.HiveContext;
import scala.Tuple2;
import com.alibaba.fastjson.JSONObject;
import com.ibeifeng.sparkproject.conf.ConfigurationManager;
import com.ibeifeng.sparkproject.constant.Constants;
import com.ibeifeng.sparkproject.dao.ISessionAggrStatDAO;
import com.ibeifeng.sparkproject.dao.ITaskDAO;
import com.ibeifeng.sparkproject.dao.impl.DAOFactory;
import com.ibeifeng.sparkproject.domain.SessionAggrStat;
import com.ibeifeng.sparkproject.domain.Task;
import com.ibeifeng.sparkproject.test.MockData;
import com.ibeifeng.sparkproject.util.DateUtils;
import com.ibeifeng.sparkproject.util.NumberUtils;
import com.ibeifeng.sparkproject.util.ParamUtils;
import com.ibeifeng.sparkproject.util.StringUtils;
import com.ibeifeng.sparkproject.util.ValidUtils;
/**
* 用户访问session分析Spark作业
*
* 接收用户创建的分析任务,用户可能指定的条件如下:
*
* 1、时间范围:起始日期~结束日期
* 2、性别:男或女
* 3、年龄范围
* 4、职业:多选
* 5、城市:多选
* 6、搜索词:多个搜索词,只要某个session中的任何一个action搜索过指定的关键词,那么session就符合条件
* 7、点击品类:多个品类,只要某个session中的任何一个action点击过某个品类,那么session就符合条件
*
* 我们的spark作业如何接受用户创建的任务?
*
* J2EE平台在接收用户创建任务的请求之后,会将任务信息插入MySQL的task表中,任务参数以JSON格式封装在task_param
* 字段中
*
* 接着J2EE平台会执行我们的spark-submit shell脚本,并将taskid作为参数传递给spark-submit shell脚本
* spark-submit shell脚本,在执行时,是可以接收参数的,并且会将接收的参数,传递给Spark作业的main函数
* 参数就封装在main函数的args数组中
*
* 这是spark本身提供的特性
*
* @author Administrator
*
*/
public class UserVisitSessionAnalyzeSpark {
public static void main(String[] args) {
args = new String[]{"2"};
// 构建Spark上下文
SparkConf conf = new SparkConf()
.setAppName(Constants.SPARK_APP_NAME_SESSION)
.setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
SQLContext sqlContext = getSQLContext(sc.sc());
// 生成模拟测试数据
mockData(sc, sqlContext);
// 创建需要使用的DAO组件
ITaskDAO taskDAO = DAOFactory.getTaskDAO();
// 首先得查询出来指定的任务,并获取任务的查询参数
long taskid = ParamUtils.getTaskIdFromArgs(args);
Task task = taskDAO.findById(taskid);
JSONObject taskParam = JSONObject.parseObject(task.getTaskParam());
// 如果要进行session粒度的数据聚合
// 首先要从user_visit_action表中,查询出来指定日期范围内的行为数据
JavaRDD<Row> actionRDD = getActionRDDByDateRange(sqlContext, taskParam);
// 首先,可以将行为数据,按照session_id进行groupByKey分组
// 此时的数据的粒度就是session粒度了,然后呢,可以将session粒度的数据
// 与用户信息数据,进行join
// 然后就可以获取到session粒度的数据,同时呢,数据里面还包含了session对应的user的信息
// 到这里为止,获取的数据是<sessionid,(sessionid,searchKeywords,clickCategoryIds,age,professional,city,sex)>
JavaPairRDD<String, String> sessionid2AggrInfoRDD =
aggregateBySession(sqlContext, actionRDD);
// 接着,就要针对session粒度的聚合数据,按照使用者指定的筛选参数进行数据过滤
// 相当于我们自己编写的算子,是要访问外面的任务参数对象的
// 所以,大家记得我们之前说的,匿名内部类(算子函数),访问外部对象,是要给外部对象使用final修饰的
// 重构,同时进行过滤和统计
Accumulator<String> sessionAggrStatAccumulator = sc.accumulator(
"", new SessionAggrStatAccumulator());
JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD = filterSessionAndAggrStat(
sessionid2AggrInfoRDD, taskParam, sessionAggrStatAccumulator);
// 计算出各个范围的session占比,并写入MySQL
calculateAndPersistAggrStat(sessionAggrStatAccumulator.value(),
task.getTaskid());
/**
* session聚合统计(统计出访问时长和访问步长,各个区间的session数量占总session数量的比例)
*
* 如果不进行重构,直接来实现,思路:
* 1、actionRDD,映射成<sessionid,Row>的格式
* 2、按sessionid聚合,计算出每个session的访问时长和访问步长,生成一个新的RDD
* 3、遍历新生成的RDD,将每个session的访问时长和访问步长,去更新自定义Accumulator中的对应的值
* 4、使用自定义Accumulator中的统计值,去计算各个区间的比例
* 5、将最后计算出来的结果,写入MySQL对应的表中
*
* 普通实现思路的问题:
* 1、为什么还要用actionRDD,去映射?其实我们之前在session聚合的时候,映射已经做过了。多此一举
* 2、是不是一定要,为了session的聚合这个功能,单独去遍历一遍session?其实没有必要,已经有session数据
* 之前过滤session的时候,其实,就相当于,是在遍历session,那么这里就没有必要再过滤一遍了
*
* 重构实现思路:
* 1、不要去生成任何新的RDD(处理上亿的数据)
* 2、不要去单独遍历一遍session的数据(处理上千万的数据)
* 3、可以在进行session聚合的时候,就直接计算出来每个session的访问时长和访问步长
* 4、在进行过滤的时候,本来就要遍历所有的聚合session信息,此时,就可以在某个session通过筛选条件后
* 将其访问时长和访问步长,累加到自定义的Accumulator上面去
* 5、就是两种截然不同的思考方式,和实现方式,在面对上亿,上千万数据的时候,甚至可以节省时间长达
* 半个小时,或者数个小时
*
* 开发Spark大型复杂项目的一些经验准则:
* 1、尽量少生成RDD
* 2、尽量少对RDD进行算子操作,如果有可能,尽量在一个算子里面,实现多个需要做的功能
* 3、尽量少对RDD进行shuffle算子操作,比如groupByKey、reduceByKey、sortByKey(map、mapToPair)
* shuffle操作,会导致大量的磁盘读写,严重降低性能
* 有shuffle的算子,和没有shuffle的算子,甚至性能,会达到几十分钟,甚至数个小时的差别
* 有shfufle的算子,很容易导致数据倾斜,一旦数据倾斜,简直就是性能杀手(完整的解决方案)
* 4、无论做什么功能,性能第一
* 在传统的J2EE或者.NET后者PHP,软件/系统/网站开发中,我认为是架构和可维护性,可扩展性的重要
* 程度,远远高于了性能,大量的分布式的架构,设计模式,代码的划分,类的划分(高并发网站除外)
*
* 在大数据项目中,比如MapReduce、Hive、Spark、Storm,我认为性能的重要程度,远远大于一些代码
* 的规范,和设计模式,代码的划分,类的划分;大数据,大数据,最重要的,就是性能
* 主要就是因为大数据以及大数据项目的特点,决定了,大数据的程序和项目的速度,都比较慢
* 如果不优先考虑性能的话,会导致一个大数据处理程序运行时间长度数个小时,甚至数十个小时
* 此时,对于用户体验,简直就是一场灾难
*
* 所以,推荐大数据项目,在开发和代码的架构中,优先考虑性能;其次考虑功能代码的划分、解耦合
*
* 我们如果采用第一种实现方案,那么其实就是代码划分(解耦合、可维护)优先,设计优先
* 如果采用第二种方案,那么其实就是性能优先
*
* 讲了这么多,其实大家不要以为我是在岔开话题,大家不要觉得项目的课程,就是单纯的项目本身以及
* 代码coding最重要,其实项目,我觉得,最重要的,除了技术本身和项目经验以外;非常重要的一点,就是
* 积累了,处理各种问题的经验
*
*/
// 关闭Spark上下文
sc.close();
}
/**
* 获取SQLContext
* 如果是在本地测试环境的话,那么就生成SQLContext对象
* 如果是在生产环境运行的话,那么就生成HiveContext对象
* @param sc SparkContext
* @return SQLContext
*/
private static SQLContext getSQLContext(SparkContext sc) {
boolean local = ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
return new SQLContext(sc);
} else {
return new HiveContext(sc);
}
}
/**
* 生成模拟数据(只有本地模式,才会去生成模拟数据)
* @param sc
* @param sqlContext
*/
private static void mockData(JavaSparkContext sc, SQLContext sqlContext) {
boolean local = ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
MockData.mock(sc, sqlContext);
}
}
/**
* 获取指定日期范围内的用户访问行为数据
* @param sqlContext SQLContext
* @param taskParam 任务参数
* @return 行为数据RDD
*/
private static JavaRDD<Row> getActionRDDByDateRange(
SQLContext sqlContext, JSONObject taskParam) {
String startDate = ParamUtils.getParam(taskParam, Constants.PARAM_START_DATE);
String endDate = ParamUtils.getParam(taskParam, Constants.PARAM_END_DATE);
String sql =
"select * "
+ "from user_visit_action "
+ "where date>='" + startDate + "' "
+ "and date<='" + endDate + "'";
DataFrame actionDF = sqlContext.sql(sql);
return actionDF.javaRDD();
}
/**
* 对行为数据按session粒度进行聚合
* @param actionRDD 行为数据RDD
* @return session粒度聚合数据
*/
private static JavaPairRDD<String, String> aggregateBySession(
SQLContext sqlContext, JavaRDD<Row> actionRDD) {
// 现在actionRDD中的元素是Row,一个Row就是一行用户访问行为记录,比如一次点击或者搜索
// 我们现在需要将这个Row映射成<sessionid,Row>的格式
JavaPairRDD<String, Row> sessionid2ActionRDD = actionRDD.mapToPair(
/**
* PairFunction
* 第一个参数,相当于是函数的输入
* 第二个参数和第三个参数,相当于是函数的输出(Tuple),分别是Tuple第一个和第二个值
*/
new PairFunction<Row, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(Row row) throws Exception {
return new Tuple2<String, Row>(row.getString(2), row);
}
});
// 对行为数据按session粒度进行分组
JavaPairRDD<String, Iterable<Row>> sessionid2ActionsRDD =
sessionid2ActionRDD.groupByKey();
// 对每一个session分组进行聚合,将session中所有的搜索词和点击品类都聚合起来
// 到此为止,获取的数据格式,如下:<userid,partAggrInfo(sessionid,searchKeywords,clickCategoryIds)>
JavaPairRDD<Long, String> userid2PartAggrInfoRDD = sessionid2ActionsRDD.mapToPair(
new PairFunction<Tuple2<String,Iterable<Row>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(Tuple2<String, Iterable<Row>> tuple)
throws Exception {
String sessionid = tuple._1;
Iterator<Row> iterator = tuple._2.iterator();
StringBuffer searchKeywordsBuffer = new StringBuffer("");
StringBuffer clickCategoryIdsBuffer = new StringBuffer("");
Long userid = null;
// session的起始和结束时间
Date startTime = null;
Date endTime = null;
// session的访问步长
int stepLength = 0;
// 遍历session所有的访问行为
while(iterator.hasNext()) {
// 提取每个访问行为的搜索词字段和点击品类字段
Row row = iterator.next();
if(userid == null) {
userid = row.getLong(1);
}
String searchKeyword = row.getString(5);
Long clickCategoryId = row.getLong(6);
// 实际上这里要对数据说明一下
// 并不是每一行访问行为都有searchKeyword何clickCategoryId两个字段的
// 其实,只有搜索行为,是有searchKeyword字段的
// 只有点击品类的行为,是有clickCategoryId字段的
// 所以,任何一行行为数据,都不可能两个字段都有,所以数据是可能出现null值的
// 我们决定是否将搜索词或点击品类id拼接到字符串中去
// 首先要满足:不能是null值
// 其次,之前的字符串中还没有搜索词或者点击品类id
if(StringUtils.isNotEmpty(searchKeyword)) {
if(!searchKeywordsBuffer.toString().contains(searchKeyword)) {
searchKeywordsBuffer.append(searchKeyword + ",");
}
}
if(clickCategoryId != null) {
if(!clickCategoryIdsBuffer.toString().contains(
String.valueOf(clickCategoryId))) {
clickCategoryIdsBuffer.append(clickCategoryId + ",");
}
}
// 计算session开始和结束时间
Date actionTime = DateUtils.parseTime(row.getString(4));
if(startTime == null) {
startTime = actionTime;
}
if(endTime == null) {
endTime = actionTime;
}
if(actionTime.before(startTime)) {
startTime = actionTime;
}
if(actionTime.after(endTime)) {
endTime = actionTime;
}
// 计算session访问步长
stepLength++;
}
String searchKeywords = StringUtils.trimComma(searchKeywordsBuffer.toString());
String clickCategoryIds = StringUtils.trimComma(clickCategoryIdsBuffer.toString());
// 计算session访问时长(秒)
long visitLength = (endTime.getTime() - startTime.getTime()) / 1000;
// 大家思考一下
// 我们返回的数据格式,即使<sessionid,partAggrInfo>
// 但是,这一步聚合完了以后,其实,我们是还需要将每一行数据,跟对应的用户信息进行聚合
// 问题就来了,如果是跟用户信息进行聚合的话,那么key,就不应该是sessionid
// 就应该是userid,才能够跟<userid,Row>格式的用户信息进行聚合
// 如果我们这里直接返回<sessionid,partAggrInfo>,还得再做一次mapToPair算子
// 将RDD映射成<userid,partAggrInfo>的格式,那么就多此一举
// 所以,我们这里其实可以直接,返回的数据格式,就是<userid,partAggrInfo>
// 然后跟用户信息join的时候,将partAggrInfo关联上userInfo
// 然后再直接将返回的Tuple的key设置成sessionid
// 最后的数据格式,还是<sessionid,fullAggrInfo>
// 聚合数据,用什么样的格式进行拼接?
// 我们这里统一定义,使用key=value|key=value
String partAggrInfo = Constants.FIELD_SESSION_ID + "=" + sessionid + "|"
+ Constants.FIELD_SEARCH_KEYWORDS + "=" + searchKeywords + "|"
+ Constants.FIELD_CLICK_CATEGORY_IDS + "=" + clickCategoryIds + "|"
+ Constants.FIELD_VISIT_LENGTH + "=" + visitLength + "|"
+ Constants.FIELD_STEP_LENGTH + "=" + stepLength;
return new Tuple2<Long, String>(userid, partAggrInfo);
}
});
// 查询所有用户数据,并映射成<userid,Row>的格式
String sql = "select * from user_info";
JavaRDD<Row> userInfoRDD = sqlContext.sql(sql).javaRDD();
JavaPairRDD<Long, Row> userid2InfoRDD = userInfoRDD.mapToPair(
new PairFunction<Row, Long, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, Row> call(Row row) throws Exception {
return new Tuple2<Long, Row>(row.getLong(0), row);
}
});
// 将session粒度聚合数据,与用户信息进行join
JavaPairRDD<Long, Tuple2<String, Row>> userid2FullInfoRDD =
userid2PartAggrInfoRDD.join(userid2InfoRDD);
// 对join起来的数据进行拼接,并且返回<sessionid,fullAggrInfo>格式的数据
JavaPairRDD<String, String> sessionid2FullAggrInfoRDD = userid2FullInfoRDD.mapToPair(
new PairFunction<Tuple2<Long,Tuple2<String,Row>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<Long, Tuple2<String, Row>> tuple)
throws Exception {
String partAggrInfo = tuple._2._1;
Row userInfoRow = tuple._2._2;
String sessionid = StringUtils.getFieldFromConcatString(
partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
int age = userInfoRow.getInt(3);
String professional = userInfoRow.getString(4);
String city = userInfoRow.getString(5);
String sex = userInfoRow.getString(6);
String fullAggrInfo = partAggrInfo + "|"
+ Constants.FIELD_AGE + "=" + age + "|"
+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
+ Constants.FIELD_CITY + "=" + city + "|"
+ Constants.FIELD_SEX + "=" + sex;
return new Tuple2<String, String>(sessionid, fullAggrInfo);
}
});
return sessionid2FullAggrInfoRDD;
}
/**
* 过滤session数据,并进行聚合统计
* @param sessionid2AggrInfoRDD
* @return
*/
private static JavaPairRDD<String, String> filterSessionAndAggrStat(
JavaPairRDD<String, String> sessionid2AggrInfoRDD,
final JSONObject taskParam,
final Accumulator<String> sessionAggrStatAccumulator) {
// 为了使用我们后面的ValieUtils,所以,首先将所有的筛选参数拼接成一个连接串
// 此外,这里其实大家不要觉得是多此一举
// 其实我们是给后面的性能优化埋下了一个伏笔
String startAge = ParamUtils.getParam(taskParam, Constants.PARAM_START_AGE);
String endAge = ParamUtils.getParam(taskParam, Constants.PARAM_END_AGE);
String professionals = ParamUtils.getParam(taskParam, Constants.PARAM_PROFESSIONALS);
String cities = ParamUtils.getParam(taskParam, Constants.PARAM_CITIES);
String sex = ParamUtils.getParam(taskParam, Constants.PARAM_SEX);
String keywords = ParamUtils.getParam(taskParam, Constants.PARAM_KEYWORDS);
String categoryIds = ParamUtils.getParam(taskParam, Constants.PARAM_CATEGORY_IDS);
String _parameter = (startAge != null ? Constants.PARAM_START_AGE + "=" + startAge + "|" : "")
+ (endAge != null ? Constants.PARAM_END_AGE + "=" + endAge + "|" : "")
+ (professionals != null ? Constants.PARAM_PROFESSIONALS + "=" + professionals + "|" : "")
+ (cities != null ? Constants.PARAM_CITIES + "=" + cities + "|" : "")
+ (sex != null ? Constants.PARAM_SEX + "=" + sex + "|" : "")
+ (keywords != null ? Constants.PARAM_KEYWORDS + "=" + keywords + "|" : "")
+ (categoryIds != null ? Constants.PARAM_CATEGORY_IDS + "=" + categoryIds: "");
if(_parameter.endsWith("\\|")) {
_parameter = _parameter.substring(0, _parameter.length() - 1);
}
final String parameter = _parameter;
// 根据筛选参数进行过滤
JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD = sessionid2AggrInfoRDD.filter(
new Function<Tuple2<String,String>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, String> tuple) throws Exception {
// 首先,从tuple中,获取聚合数据
String aggrInfo = tuple._2;
// 接着,依次按照筛选条件进行过滤
// 按照年龄范围进行过滤(startAge、endAge)
if(!ValidUtils.between(aggrInfo, Constants.FIELD_AGE,
parameter, Constants.PARAM_START_AGE, Constants.PARAM_END_AGE)) {
return false;
}
// 按照职业范围进行过滤(professionals)
// 互联网,IT,软件
// 互联网
if(!ValidUtils.in(aggrInfo, Constants.FIELD_PROFESSIONAL,
parameter, Constants.PARAM_PROFESSIONALS)) {
return false;
}
// 按照城市范围进行过滤(cities)
// 北京,上海,广州,深圳
// 成都
if(!ValidUtils.in(aggrInfo, Constants.FIELD_CITY,
parameter, Constants.PARAM_CITIES)) {
return false;
}
// 按照性别进行过滤
// 男/女
// 男,女
if(!ValidUtils.equal(aggrInfo, Constants.FIELD_SEX,
parameter, Constants.PARAM_SEX)) {
return false;
}
// 按照搜索词进行过滤
// 我们的session可能搜索了 火锅,蛋糕,烧烤
// 我们的筛选条件可能是 火锅,串串香,iphone手机
// 那么,in这个校验方法,主要判定session搜索的词中,有任何一个,与筛选条件中
// 任何一个搜索词相当,即通过
if(!ValidUtils.in(aggrInfo, Constants.FIELD_SEARCH_KEYWORDS,
parameter, Constants.PARAM_KEYWORDS)) {
return false;
}
// 按照点击品类id进行过滤
if(!ValidUtils.in(aggrInfo, Constants.FIELD_CLICK_CATEGORY_IDS,
parameter, Constants.PARAM_CATEGORY_IDS)) {
return false;
}
// 如果经过了之前的多个过滤条件之后,程序能够走到这里
// 那么就说明,该session是通过了用户指定的筛选条件的,也就是需要保留的session
// 那么就要对session的访问时长和访问步长,进行统计,根据session对应的范围
// 进行相应的累加计数
// 主要走到这一步,那么就是需要计数的session
sessionAggrStatAccumulator.add(Constants.SESSION_COUNT);
// 计算出session的访问时长和访问步长的范围,并进行相应的累加
long visitLength = Long.valueOf(StringUtils.getFieldFromConcatString(
aggrInfo, "\\|", Constants.FIELD_VISIT_LENGTH));
long stepLength = Long.valueOf(StringUtils.getFieldFromConcatString(
aggrInfo, "\\|", Constants.FIELD_STEP_LENGTH));
calculateVisitLength(visitLength);
calculateStepLength(stepLength);
return true;
}
/**
* 计算访问时长范围
* @param visitLength
*/
private void calculateVisitLength(long visitLength) {
if(visitLength >=1 && visitLength <= 3) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_1s_3s);
} else if(visitLength >=4 && visitLength <= 6) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_4s_6s);
} else if(visitLength >=7 && visitLength <= 9) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_7s_9s);
} else if(visitLength >=10 && visitLength <= 30) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_10s_30s);
} else if(visitLength > 30 && visitLength <= 60) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_30s_60s);
} else if(visitLength > 60 && visitLength <= 180) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_1m_3m);
} else if(visitLength > 180 && visitLength <= 600) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_3m_10m);
} else if(visitLength > 600 && visitLength <= 1800) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_10m_30m);
} else if(visitLength > 1800) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_30m);
}
}
/**
* 计算访问步长范围
* @param stepLength
*/
private void calculateStepLength(long stepLength) {
if(stepLength >= 1 && stepLength <= 3) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_1_3);
} else if(stepLength >= 4 && stepLength <= 6) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_4_6);
} else if(stepLength >= 7 && stepLength <= 9) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_7_9);
} else if(stepLength >= 10 && stepLength <= 30) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_10_30);
} else if(stepLength > 30 && stepLength <= 60) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_30_60);
} else if(stepLength > 60) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_60);
}
}
});
return filteredSessionid2AggrInfoRDD;
}
/**
* 计算各session范围占比,并写入MySQL
* @param value
*/
private static void calculateAndPersistAggrStat(String value, long taskid) {
// 从Accumulator统计串中获取值
long session_count = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.SESSION_COUNT));
long visit_length_1s_3s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_1s_3s));
long visit_length_4s_6s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_4s_6s));
long visit_length_7s_9s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_7s_9s));
long visit_length_10s_30s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_10s_30s));
long visit_length_30s_60s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_30s_60s));
long visit_length_1m_3m = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_1m_3m));
long visit_length_3m_10m = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_3m_10m));
long visit_length_10m_30m = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_10m_30m));
long visit_length_30m = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_30m));
long step_length_1_3 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_1_3));
long step_length_4_6 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_4_6));
long step_length_7_9 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_7_9));
long step_length_10_30 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_10_30));
long step_length_30_60 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_30_60));
long step_length_60 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_60));
// 计算各个访问时长和访问步长的范围
double visit_length_1s_3s_ratio = NumberUtils.formatDouble(
visit_length_1s_3s / session_count, 2);
double visit_length_4s_6s_ratio = NumberUtils.formatDouble(
visit_length_4s_6s / session_count, 2);
double visit_length_7s_9s_ratio = NumberUtils.formatDouble(
visit_length_7s_9s / session_count, 2);
double visit_length_10s_30s_ratio = NumberUtils.formatDouble(
visit_length_10s_30s / session_count, 2);
double visit_length_30s_60s_ratio = NumberUtils.formatDouble(
visit_length_30s_60s / session_count, 2);
double visit_length_1m_3m_ratio = NumberUtils.formatDouble(
visit_length_1m_3m / session_count, 2);
double visit_length_3m_10m_ratio = NumberUtils.formatDouble(
visit_length_3m_10m / session_count, 2);
double visit_length_10m_30m_ratio = NumberUtils.formatDouble(
visit_length_10m_30m / session_count, 2);
double visit_length_30m_ratio = NumberUtils.formatDouble(
visit_length_30m / session_count, 2);
double step_length_1_3_ratio = NumberUtils.formatDouble(
step_length_1_3 / session_count, 2);
double step_length_4_6_ratio = NumberUtils.formatDouble(
step_length_4_6 / session_count, 2);
double step_length_7_9_ratio = NumberUtils.formatDouble(
step_length_7_9 / session_count, 2);
double step_length_10_30_ratio = NumberUtils.formatDouble(
step_length_10_30 / session_count, 2);
double step_length_30_60_ratio = NumberUtils.formatDouble(
step_length_30_60 / session_count, 2);
double step_length_60_ratio = NumberUtils.formatDouble(
step_length_60 / session_count, 2);
// 将统计结果封装为Domain对象
SessionAggrStat sessionAggrStat = new SessionAggrStat();
sessionAggrStat.setTaskid(taskid);
sessionAggrStat.setSession_count(session_count);
sessionAggrStat.setVisit_length_1s_3s_ratio(visit_length_1s_3s_ratio);
sessionAggrStat.setVisit_length_4s_6s_ratio(visit_length_4s_6s_ratio);
sessionAggrStat.setVisit_length_7s_9s_ratio(visit_length_7s_9s_ratio);
sessionAggrStat.setVisit_length_10s_30s_ratio(visit_length_10s_30s_ratio);
sessionAggrStat.setVisit_length_30s_60s_ratio(visit_length_30s_60s_ratio);
sessionAggrStat.setVisit_length_1m_3m_ratio(visit_length_1m_3m_ratio);
sessionAggrStat.setVisit_length_3m_10m_ratio(visit_length_3m_10m_ratio);
sessionAggrStat.setVisit_length_10m_30m_ratio(visit_length_10m_30m_ratio);
sessionAggrStat.setVisit_length_30m_ratio(visit_length_30m_ratio);
sessionAggrStat.setStep_length_1_3_ratio(step_length_1_3_ratio);
sessionAggrStat.setStep_length_4_6_ratio(step_length_4_6_ratio);
sessionAggrStat.setStep_length_7_9_ratio(step_length_7_9_ratio);
sessionAggrStat.setStep_length_10_30_ratio(step_length_10_30_ratio);
sessionAggrStat.setStep_length_30_60_ratio(step_length_30_60_ratio);
sessionAggrStat.setStep_length_60_ratio(step_length_60_ratio);
// 调用对应的DAO插入统计结果
ISessionAggrStatDAO sessionAggrStatDAO = DAOFactory.getSessionAggrStatDAO();
sessionAggrStatDAO.insert(sessionAggrStat);
}
}
5.3 Session随机抽取
每一次执行用户访问session分析模块,要抽取出100个session
session随机抽取:按每天的每个小时的session数量,占当天session总数的比例,乘以每天要抽取的session数量,计算出每个小时要抽取的session数量;然后呢,在每天每小时的session中,随机抽取出之前计算出来的数量的session。
举例:10000个session中抽取100个session;0点~1点之间,有2000个session,占总session的比例就是0.2;按照比例,0点~1点需要抽取出来的session数量是100 * 0.2 = 20个;在0点~点的2000个session中,随机抽取出来20个session。
我们之前有什么数据:session粒度的聚合数据(计算出来session的start_time)
session聚合数据进行映射,将每个session发生的yyyy-MM-dd_HH(start_time)作为key,value就是session_id
对上述数据,使用countByKey算子,就可以获取到每天每小时的session数量
(按时间比例随机抽取算法)每天每小时有多少session,根据这个数量计算出每天每小时的session占比,以及按照占比,需要抽取多少session,可以计算出每个小时内,从0~session数量之间的范围中,获取指定抽取数量个随机数,作为随机抽取的索引
把之前转换后的session数据(以yyyy-MM-dd_HH作为key),执行groupByKey算子;然后可以遍历每天每小时的session,遍历时,遇到之前计算出来的要抽取的索引,即将session抽取出来;抽取出来的session,直接写入MySQL数据库
5.3.1 随机抽取每天每小时的Session数量
/**
* 随机抽取session
* @param sessionid2AggrInfoRDD
*/
private static void randomExtractSession(
JavaPairRDD<String, String> sessionid2AggrInfoRDD) {
// 第一步,计算出每天每小时的session数量,获取<yyyy-MM-dd_HH,sessionid>格式的RDD
JavaPairRDD<String, String> time2sessionidRDD = sessionid2AggrInfoRDD.mapToPair(
new PairFunction<Tuple2<String,String>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<String, String> tuple) throws Exception {
String aggrInfo = tuple._2;
String startTime = StringUtils.getFieldFromConcatString(
aggrInfo, "\\|", Constants.FIELD_START_TIME);
String dateHour = DateUtils.getDateHour(startTime);
return new Tuple2<String, String>(dateHour, aggrInfo);
}
});
/**
* 思考一下:这里我们不要着急写大量的代码,做项目的时候,一定要用脑子多思考
*
* 每天每小时的session数量,然后计算出每天每小时的session抽取索引,遍历每天每小时session
* 首先抽取出的session的聚合数据,写入session_random_extract表
* 所以第一个RDD的value,应该是session聚合数据
*
*/
// 得到每天每小时的session数量
Map<String, Object> countMap = time2sessionidRDD.countByKey();
}
5.3.2 Session随机抽取之按时间比例随机抽取算法实现
/**
* 随机抽取session
* @param sessionid2AggrInfoRDD
*/
private static void randomExtractSession(
JavaPairRDD<String, String> sessionid2AggrInfoRDD) {
// 第一步,计算出每天每小时的session数量,获取<yyyy-MM-dd_HH,sessionid>格式的RDD
JavaPairRDD<String, String> time2sessionidRDD = sessionid2AggrInfoRDD.mapToPair(
new PairFunction<Tuple2<String,String>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<String, String> tuple) throws Exception {
String aggrInfo = tuple._2;
String startTime = StringUtils.getFieldFromConcatString(
aggrInfo, "\\|", Constants.FIELD_START_TIME);
String dateHour = DateUtils.getDateHour(startTime);
return new Tuple2<String, String>(dateHour, aggrInfo);
}
});
/**
* 思考一下:这里我们不要着急写大量的代码,做项目的时候,一定要用脑子多思考
*
* 每天每小时的session数量,然后计算出每天每小时的session抽取索引,遍历每天每小时session
* 首先抽取出的session的聚合数据,写入session_random_extract表
* 所以第一个RDD的value,应该是session聚合数据
*
*/
// 得到每天每小时的session数量
Map<String, Object> countMap = time2sessionidRDD.countByKey();
// 第二步,使用按时间比例随机抽取算法,计算出每天每小时要抽取session的索引
// 将<yyyy-MM-dd_HH,count>格式的map,转换成<yyyy-MM-dd,<HH,count>>的格式
Map<String, Map<String, Long>> dateHourCountMap =
new HashMap<String, Map<String, Long>>();
for(Map.Entry<String, Object> countEntry : countMap.entrySet()) {
String dateHour = countEntry.getKey();
String date = dateHour.split("_")[0];
String hour = dateHour.split("_")[1];
long count = Long.valueOf(String.valueOf(countEntry.getValue()));
Map<String, Long> hourCountMap = dateHourCountMap.get(date);
if(hourCountMap == null) {
hourCountMap = new HashMap<String, Long>();
dateHourCountMap.put(date, hourCountMap);
}
hourCountMap.put(hour, count);
}
// 开始实现我们的按时间比例随机抽取算法
// 总共要抽取100个session,先按照天数,进行平分
int extractNumberPerDay = 100 / dateHourCountMap.size();
// <date,<hour,(3,5,20,102)>>
Map<String, Map<String, List<Integer>>> dateHourExtractMap =
new HashMap<String, Map<String, List<Integer>>>();
Random random = new Random();
for(Map.Entry<String, Map<String, Long>> dateHourCountEntry : dateHourCountMap.entrySet()) {
String date = dateHourCountEntry.getKey();
Map<String, Long> hourCountMap = dateHourCountEntry.getValue();
// 计算出这一天的session总数
long sessionCount = 0L;
for(long hourCount : hourCountMap.values()) {
sessionCount += hourCount;
}
Map<String, List<Integer>> hourExtractMap = dateHourExtractMap.get(date);
if(hourExtractMap == null) {
hourExtractMap = new HashMap<String, List<Integer>>();
dateHourExtractMap.put(date, hourExtractMap);
}
// 遍历每个小时
for(Map.Entry<String, Long> hourCountEntry : hourCountMap.entrySet()) {
String hour = hourCountEntry.getKey();
long count = hourCountEntry.getValue();
// 计算每个小时的session数量,占据当天总session数量的比例,直接乘以每天要抽取的数量
// 就可以计算出,当前小时需要抽取的session数量
int hourExtractNumber = (int)(((double)count / (double)sessionCount)
* extractNumberPerDay);
if(hourExtractNumber > count) {
hourExtractNumber = (int) count;
}
// 先获取当前小时的存放随机数的list
List<Integer> extractIndexList = hourExtractMap.get(hour);
if(extractIndexList == null) {
extractIndexList = new ArrayList<Integer>();
hourExtractMap.put(hour, extractIndexList);
}
// 生成上面计算出来的数量的随机数
for(int i = 0; i < hourExtractNumber; i++) {
int extractIndex = random.nextInt((int) count);
while(extractIndexList.contains(extractIndex)) {
extractIndex = random.nextInt((int) count);
}
extractIndexList.add(extractIndex);
}
}
}
}
5.3.3 Session随机抽取并获取抽取Session的明细数据
/**
* 随机抽取session
* @param sessionid2AggrInfoRDD
*/
private static void randomExtractSession(
final long taskid,
JavaPairRDD<String, String> sessionid2AggrInfoRDD,
JavaPairRDD<String, Row> sessionid2actionRDD) {
/**
* 第一步,计算出每天每小时的session数量
*/
// 获取<yyyy-MM-dd_HH,aggrInfo>格式的RDD
JavaPairRDD<String, String> time2sessionidRDD = sessionid2AggrInfoRDD.mapToPair(
new PairFunction<Tuple2<String,String>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<String, String> tuple) throws Exception {
String aggrInfo = tuple._2;
String startTime = StringUtils.getFieldFromConcatString(
aggrInfo, "\\|", Constants.FIELD_START_TIME);
String dateHour = DateUtils.getDateHour(startTime);
return new Tuple2<String, String>(dateHour, aggrInfo);
}
});
/**
* 思考一下:这里我们不要着急写大量的代码,做项目的时候,一定要用脑子多思考
*
* 每天每小时的session数量,然后计算出每天每小时的session抽取索引,遍历每天每小时session
* 首先抽取出的session的聚合数据,写入session_random_extract表
* 所以第一个RDD的value,应该是session聚合数据
*
*/
// 得到每天每小时的session数量
Map<String, Object> countMap = time2sessionidRDD.countByKey();
/**
* 第二步,使用按时间比例随机抽取算法,计算出每天每小时要抽取session的索引
*/
// 将<yyyy-MM-dd_HH,count>格式的map,转换成<yyyy-MM-dd,<HH,count>>的格式
Map<String, Map<String, Long>> dateHourCountMap =
new HashMap<String, Map<String, Long>>();
for(Map.Entry<String, Object> countEntry : countMap.entrySet()) {
String dateHour = countEntry.getKey();
String date = dateHour.split("_")[0];
String hour = dateHour.split("_")[1];
long count = Long.valueOf(String.valueOf(countEntry.getValue()));
Map<String, Long> hourCountMap = dateHourCountMap.get(date);
if(hourCountMap == null) {
hourCountMap = new HashMap<String, Long>();
dateHourCountMap.put(date, hourCountMap);
}
hourCountMap.put(hour, count);
}
// 开始实现我们的按时间比例随机抽取算法
// 总共要抽取100个session,先按照天数,进行平分
int extractNumberPerDay = 100 / dateHourCountMap.size();
// <date,<hour,(3,5,20,102)>>
final Map<String, Map<String, List<Integer>>> dateHourExtractMap =
new HashMap<String, Map<String, List<Integer>>>();
Random random = new Random();
for(Map.Entry<String, Map<String, Long>> dateHourCountEntry : dateHourCountMap.entrySet()) {
String date = dateHourCountEntry.getKey();
Map<String, Long> hourCountMap = dateHourCountEntry.getValue();
// 计算出这一天的session总数
long sessionCount = 0L;
for(long hourCount : hourCountMap.values()) {
sessionCount += hourCount;
}
Map<String, List<Integer>> hourExtractMap = dateHourExtractMap.get(date);
if(hourExtractMap == null) {
hourExtractMap = new HashMap<String, List<Integer>>();
dateHourExtractMap.put(date, hourExtractMap);
}
// 遍历每个小时
for(Map.Entry<String, Long> hourCountEntry : hourCountMap.entrySet()) {
String hour = hourCountEntry.getKey();
long count = hourCountEntry.getValue();
// 计算每个小时的session数量,占据当天总session数量的比例,直接乘以每天要抽取的数量
// 就可以计算出,当前小时需要抽取的session数量
int hourExtractNumber = (int)(((double)count / (double)sessionCount)
* extractNumberPerDay);
if(hourExtractNumber > count) {
hourExtractNumber = (int) count;
}
// 先获取当前小时的存放随机数的list
List<Integer> extractIndexList = hourExtractMap.get(hour);
if(extractIndexList == null) {
extractIndexList = new ArrayList<Integer>();
hourExtractMap.put(hour, extractIndexList);
}
// 生成上面计算出来的数量的随机数
for(int i = 0; i < hourExtractNumber; i++) {
int extractIndex = random.nextInt((int) count);
while(extractIndexList.contains(extractIndex)) {
extractIndex = random.nextInt((int) count);
}
extractIndexList.add(extractIndex);
}
}
}
/**
* 第三步:遍历每天每小时的session,然后根据随机索引进行抽取
*/
// 执行groupByKey算子,得到<dateHour,(session aggrInfo)>
JavaPairRDD<String, Iterable<String>> time2sessionsRDD = time2sessionidRDD.groupByKey();
// 我们用flatMap算子,遍历所有的<dateHour,(session aggrInfo)>格式的数据
// 然后呢,会遍历每天每小时的session
// 如果发现某个session恰巧在我们指定的这天这小时的随机抽取索引上
// 那么抽取该session,直接写入MySQL的random_extract_session表
// 将抽取出来的session id返回回来,形成一个新的JavaRDD<String>
// 然后最后一步,是用抽取出来的sessionid,去join它们的访问行为明细数据,写入session表
JavaPairRDD<String, String> extractSessionidsRDD = time2sessionsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Iterable<String>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<String, String>> call(
Tuple2<String, Iterable<String>> tuple)
throws Exception {
List<Tuple2<String, String>> extractSessionids =
new ArrayList<Tuple2<String, String>>();
String dateHour = tuple._1;
String date = dateHour.split("_")[0];
String hour = dateHour.split("_")[1];
Iterator<String> iterator = tuple._2.iterator();
List<Integer> extractIndexList = dateHourExtractMap.get(date).get(hour);
ISessionRandomExtractDAO sessionRandomExtractDAO =
DAOFactory.getSessionRandomExtractDAO();
int index = 0;
while(iterator.hasNext()) {
String sessionAggrInfo = iterator.next();
if(extractIndexList.contains(index)) {
String sessionid = StringUtils.getFieldFromConcatString(
sessionAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
// 将数据写入MySQL
SessionRandomExtract sessionRandomExtract = new SessionRandomExtract();
sessionRandomExtract.setTaskid(taskid);
sessionRandomExtract.setSessionid(sessionid);
sessionRandomExtract.setStartTime(StringUtils.getFieldFromConcatString(
sessionAggrInfo, "\\|", Constants.FIELD_START_TIME));
sessionRandomExtract.setSearchKeywords(StringUtils.getFieldFromConcatString(
sessionAggrInfo, "\\|", Constants.FIELD_SEARCH_KEYWORDS));
sessionRandomExtract.setClickCategoryIds(StringUtils.getFieldFromConcatString(
sessionAggrInfo, "\\|", Constants.FIELD_CLICK_CATEGORY_IDS));
sessionRandomExtractDAO.insert(sessionRandomExtract);
// 将sessionid加入list
extractSessionids.add(new Tuple2<String, String>(sessionid, sessionid));
}
index++;
}
return extractSessionids;
}
});
/**
* 第四步:获取抽取出来的session的明细数据
*/
JavaPairRDD<String, Tuple2<String, Row>> extractSessionDetailRDD =
extractSessionidsRDD.join(sessionid2actionRDD);
extractSessionDetailRDD.foreach(new VoidFunction<Tuple2<String,Tuple2<String,Row>>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Tuple2<String, Tuple2<String, Row>> tuple) throws Exception {
Row row = tuple._2._2;
SessionDetail sessionDetail = new SessionDetail();
sessionDetail.setTaskid(taskid);
sessionDetail.setUserid(row.getLong(0));
sessionDetail.setSessionid(row.getString(1));
sessionDetail.setPageid(row.getLong(2));
sessionDetail.setActionTime(row.getString(3));
sessionDetail.setSearchKeyword(row.getString(4));
sessionDetail.setClickCategoryId(row.getLong(5));
sessionDetail.setClickProductId(row.getLong(6));
sessionDetail.setOrderCategoryIds(row.getString(7));
sessionDetail.setOrderProductIds(row.getString(8));
sessionDetail.setPayCategoryIds(row.getString(9));
sessionDetail.setPayProductIds(row.getString(11));
ISessionDetailDAO sessionDetailDAO = DAOFactory.getSessionDetailDAO();
sessionDetailDAO.insert(sessionDetail);
}
});
}
5.4 top10热门品类
计算出来通过筛选条件的那些session,他们访问过的所有品类(点击、下单、支付),按照各个品类的点击、下单和支付次数,降序排序,获取前10个品类,也就是筛选条件下的那一批session的top10热门品类;
点击、下单和支付次数:优先按照点击次数排序、如果点击次数相等,那么按照下单次数排序、如果下单次数相当,那么按照支付次数排序
这个需求是很有意义的,因为这样,就可以让数据分析师、产品经理、公司高层,随时随地都可以看到自己感兴趣的那一批用户,最喜欢的10个品类,从而对自己公司和产品的定位有清晰的了解,并且可以更加深入的了解自己的用户,更好的调整公司战略
二次排序:
如果我们就只是根据某一个字段进行排序,比如点击次数降序排序,那么就不是二次排序;
二次排序,顾名思义,就是说,不只是根据一个字段进行一次排序,可能是要根据多个字段,进行多次排序的
点击、下单和支付次数,依次进行排序,就是二次排序
sortByKey算子,默认情况下,它支持根据int、long等类型来进行排序,但是那样的话,key就只能放一个字段了
所以需要自定义key,作为sortByKey算子的key,自定义key中,封装n个字段,并在key中,自己在指定接口方法中,实现自己的根据多字段的排序算法
然后再使用sortByKey算子进行排序,那么就可以按照我们自己的key,使用多个字段进行排序
本模块中,最最重要和核心的一个Spark技术点
实现思路分析:
1、拿到通过筛选条件的那批session,访问过的所有品类
2、计算出session访问过的所有品类的点击、下单和支付次数,这里可能要跟第一步计算出来的品类进行join
3、自己开发二次排序的key
4、做映射,将品类的点击、下单和支付次数,封装到二次排序key中,作为PairRDD的key
5、使用sortByKey(false),按照自定义key,进行降序二次排序
6、使用take(10)获取,排序后的前10个品类,就是top10热门品类
7、将top10热门品类,以及每个品类的点击、下单和支付次数,写入MySQL数据库
8、本地测试
5.4.1 top10热门品类相关操作
1、获取session访问过的所有品类
2、计算各品类点击、下单和支付次数
3、join品类与点击下单支付次数
/**
* 获取top10热门品类
* @param filteredSessionid2AggrInfoRDD
* @param sessionid2actionRDD
*/
private static void getTop10Category(
JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD,
JavaPairRDD<String, Row> sessionid2actionRDD) {
/**
* 第一步:获取符合条件的session访问过的所有品类
*/
// 获取符合条件的session的访问明细
JavaPairRDD<String, Row> sessionid2detailRDD = filteredSessionid2AggrInfoRDD
.join(sessionid2actionRDD)
.mapToPair(new PairFunction<Tuple2<String,Tuple2<String,Row>>, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(
Tuple2<String, Tuple2<String, Row>> tuple) throws Exception {
return new Tuple2<String, Row>(tuple._1, tuple._2._2);
}
});
// 获取session访问过的所有品类id
// 访问过:指的是,点击过、下单过、支付过的品类
JavaPairRDD<Long, Long> categoryidRDD = sessionid2detailRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, Long>> call(
Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
List<Tuple2<Long, Long>> list = new ArrayList<Tuple2<Long, Long>>();
Long clickCategoryId = row.getLong(6);
if(clickCategoryId != null) {
list.add(new Tuple2<Long, Long>(clickCategoryId, clickCategoryId));
}
String orderCategoryIds = row.getString(8);
if(orderCategoryIds != null) {
String[] orderCategoryIdsSplited = orderCategoryIds.split(",");
for(String orderCategoryId : orderCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(orderCategoryId),
Long.valueOf(orderCategoryId)));
}
}
String payCategoryIds = row.getString(10);
if(payCategoryIds != null) {
String[] payCategoryIdsSplited = payCategoryIds.split(",");
for(String payCategoryId : payCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(payCategoryId),
Long.valueOf(payCategoryId)));
}
}
return list;
}
});
/**
* 第二步:计算各品类的点击、下单和支付的次数
*/
// 访问明细中,其中三种访问行为是:点击、下单和支付
// 分别来计算各品类点击、下单和支付的次数,可以先对访问明细数据进行过滤
// 分别过滤出点击、下单和支付行为,然后通过map、reduceByKey等算子来进行计算
// 计算各个品类的点击次数
JavaPairRDD<Long, Long> clickCategoryId2CountRDD =
getClickCategoryId2CountRDD(sessionid2detailRDD);
// 计算各个品类的下单次数
JavaPairRDD<Long, Long> orderCategoryId2CountRDD =
getOrderCategoryId2CountRDD(sessionid2detailRDD);
// 计算各个品类的支付次数
JavaPairRDD<Long, Long> payCategoryId2CountRDD =
getPayCategoryId2CountRDD(sessionid2detailRDD);
/**
* 第三步:join各品类与它的点击、下单和支付的次数
*
* categoryidRDD中,是包含了所有的符合条件的session,访问过的品类id
*
* 上面分别计算出来的三份,各品类的点击、下单和支付的次数,可能不是包含所有品类的
* 比如,有的品类,就只是被点击过,但是没有人下单和支付
*
* 所以,这里,就不能使用join操作,要使用leftOuterJoin操作,就是说,如果categoryidRDD不能
* join到自己的某个数据,比如点击、或下单、或支付次数,那么该categoryidRDD还是要保留下来的
* 只不过,没有join到的那个数据,就是0了
*
*/
JavaPairRDD<Long, String> categoryid2countRDD = joinCategoryAndData(
categoryidRDD, clickCategoryId2CountRDD, orderCategoryId2CountRDD,
payCategoryId2CountRDD);
}
/**
* 获取各品类点击次数RDD
* @param sessionid2detailRDD
* @return
*/
private static JavaPairRDD<Long, Long> getClickCategoryId2CountRDD(
JavaPairRDD<String, Row> sessionid2detailRDD) {
JavaPairRDD<String, Row> clickActionRDD = sessionid2detailRDD.filter(
new Function<Tuple2<String,Row>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
return Long.valueOf(row.getLong(6)) != null ? true : false;
}
});
JavaPairRDD<Long, Long> clickCategoryIdRDD = clickActionRDD.mapToPair(
new PairFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, Long> call(Tuple2<String, Row> tuple)
throws Exception {
long clickCategoryId = tuple._2.getLong(6);
return new Tuple2<Long, Long>(clickCategoryId, 1L);
}
});
JavaPairRDD<Long, Long> clickCategoryId2CountRDD = clickCategoryIdRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
return clickCategoryId2CountRDD;
}
/**
* 获取各品类的下单次数RDD
* @param sessionid2detailRDD
* @return
*/
private static JavaPairRDD<Long, Long> getOrderCategoryId2CountRDD(
JavaPairRDD<String, Row> sessionid2detailRDD) {
JavaPairRDD<String, Row> orderActionRDD = sessionid2detailRDD.filter(
new Function<Tuple2<String,Row>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
return row.getString(8) != null ? true : false;
}
});
JavaPairRDD<Long, Long> orderCategoryIdRDD = orderActionRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, Long>> call(
Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
String orderCategoryIds = row.getString(8);
String[] orderCategoryIdsSplited = orderCategoryIds.split(",");
List<Tuple2<Long, Long>> list = new ArrayList<Tuple2<Long, Long>>();
for(String orderCategoryId : orderCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(orderCategoryId), 1L));
}
return list;
}
});
JavaPairRDD<Long, Long> orderCategoryId2CountRDD = orderCategoryIdRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
return orderCategoryId2CountRDD;
}
/**
* 获取各个品类的支付次数RDD
* @param sessionid2detailRDD
* @return
*/
private static JavaPairRDD<Long, Long> getPayCategoryId2CountRDD(
JavaPairRDD<String, Row> sessionid2detailRDD) {
JavaPairRDD<String, Row> payActionRDD = sessionid2detailRDD.filter(
new Function<Tuple2<String,Row>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
return row.getString(10) != null ? true : false;
}
});
JavaPairRDD<Long, Long> payCategoryIdRDD = payActionRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, Long>> call(
Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
String payCategoryIds = row.getString(10);
String[] payCategoryIdsSplited = payCategoryIds.split(",");
List<Tuple2<Long, Long>> list = new ArrayList<Tuple2<Long, Long>>();
for(String payCategoryId : payCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(payCategoryId), 1L));
}
return list;
}
});
JavaPairRDD<Long, Long> payCategoryId2CountRDD = payCategoryIdRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
return payCategoryId2CountRDD;
}
/**
* 连接品类RDD与数据RDD
* @param categoryidRDD
* @param clickCategoryId2CountRDD
* @param orderCategoryId2CountRDD
* @param payCategoryId2CountRDD
* @return
*/
private static JavaPairRDD<Long, String> joinCategoryAndData(
JavaPairRDD<Long, Long> categoryidRDD,
JavaPairRDD<Long, Long> clickCategoryId2CountRDD,
JavaPairRDD<Long, Long> orderCategoryId2CountRDD,
JavaPairRDD<Long, Long> payCategoryId2CountRDD) {
// 解释一下,如果用leftOuterJoin,就可能出现,右边那个RDD中,join过来时,没有值
// 所以Tuple中的第二个值用Optional<Long>类型,就代表,可能有值,可能没有值
JavaPairRDD<Long, Tuple2<Long, Optional<Long>>> tmpJoinRDD =
categoryidRDD.leftOuterJoin(clickCategoryId2CountRDD);
JavaPairRDD<Long, String> tmpMapRDD = tmpJoinRDD.mapToPair(
new PairFunction<Tuple2<Long,Tuple2<Long,Optional<Long>>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<Long, Optional<Long>>> tuple)
throws Exception {
long categoryid = tuple._1;
Optional<Long> optional = tuple._2._2;
long clickCount = 0L;
if(optional.isPresent()) {
clickCount = optional.get();
}
String value = Constants.FIELD_CATEGORY_ID + "=" + categoryid + "|" +
Constants.FIELD_CLICK_COUNT + "=" + clickCount;
return new Tuple2<Long, String>(categoryid, value);
}
});
tmpMapRDD = tmpMapRDD.leftOuterJoin(orderCategoryId2CountRDD).mapToPair(
new PairFunction<Tuple2<Long,Tuple2<String,Optional<Long>>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<String, Optional<Long>>> tuple)
throws Exception {
long categoryid = tuple._1;
String value = tuple._2._1;
Optional<Long> optional = tuple._2._2;
long orderCount = 0L;
if(optional.isPresent()) {
orderCount = optional.get();
}
value = value + "|" + Constants.FIELD_ORDER_COUNT + "=" + orderCount;
return new Tuple2<Long, String>(categoryid, value);
}
});
tmpMapRDD = tmpMapRDD.leftOuterJoin(payCategoryId2CountRDD).mapToPair(
new PairFunction<Tuple2<Long,Tuple2<String,Optional<Long>>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<String, Optional<Long>>> tuple)
throws Exception {
long categoryid = tuple._1;
String value = tuple._2._1;
Optional<Long> optional = tuple._2._2;
long payCount = 0L;
if(optional.isPresent()) {
payCount = optional.get();
}
value = value + "|" + Constants.FIELD_PAY_COUNT + "=" + payCount;
return new Tuple2<Long, String>(categoryid, value);
}
});
return tmpMapRDD;
}
5.4.2 获取top10品类并写入MySQL
1、自定义二次排序key
package cn.ctgu.sparkproject.spark;
import scala.math.Ordered;
/**
* 品类二次排序key
*
* 封装你要进行排序算法需要的几个字段:点击次数、下单次数和支付次数
* 实现Ordered接口要求的几个方法
*
* 跟其他key相比,如何来判定大于、大于等于、小于、小于等于
*
* 依次使用三个次数进行比较,如果某一个相等,那么就比较下一个
*
* @author Administrator
*
*/
public class CategorySortKey implements Ordered<CategorySortKey> {
private long clickCount;
private long orderCount;
private long payCount;
@Override
public boolean $greater(CategorySortKey other) {
if(clickCount > other.getClickCount()) {
return true;
} else if(clickCount == other.getClickCount() &&
orderCount > other.getOrderCount()) {
return true;
} else if(clickCount == other.getClickCount() &&
orderCount == other.getOrderCount() &&
payCount > other.getPayCount()) {
return true;
}
return false;
}
@Override
public boolean $greater$eq(CategorySortKey other) {
if($greater(other)) {
return true;
} else if(clickCount == other.getClickCount() &&
orderCount == other.getOrderCount() &&
payCount == other.getPayCount()) {
return true;
}
return false;
}
@Override
public boolean $less(CategorySortKey other) {
if(clickCount < other.getClickCount()) {
return true;
} else if(clickCount == other.getClickCount() &&
orderCount < other.getOrderCount()) {
return true;
} else if(clickCount == other.getClickCount() &&
orderCount == other.getOrderCount() &&
payCount < other.getPayCount()) {
return true;
}
return false;
}
@Override
public boolean $less$eq(CategorySortKey other) {
if($less(other)) {
return true;
} else if(clickCount == other.getClickCount() &&
orderCount == other.getOrderCount() &&
payCount == other.getPayCount()) {
return true;
}
return false;
}
@Override
public int compare(CategorySortKey other) {
if(clickCount - other.getClickCount() != 0) {
return (int) (clickCount - other.getClickCount());
} else if(orderCount - other.getOrderCount() != 0) {
return (int) (orderCount - other.getOrderCount());
} else if(payCount - other.getPayCount() != 0) {
return (int) (payCount - other.getPayCount());
}
return 0;
}
@Override
public int compareTo(CategorySortKey other) {
if(clickCount - other.getClickCount() != 0) {
return (int) (clickCount - other.getClickCount());
} else if(orderCount - other.getOrderCount() != 0) {
return (int) (orderCount - other.getOrderCount());
} else if(payCount - other.getPayCount() != 0) {
return (int) (payCount - other.getPayCount());
}
return 0;
}
public long getClickCount() {
return clickCount;
}
public void setClickCount(long clickCount) {
this.clickCount = clickCount;
}
public long getOrderCount() {
return orderCount;
}
public void setOrderCount(long orderCount) {
this.orderCount = orderCount;
}
public long getPayCount() {
return payCount;
}
public void setPayCount(long payCount) {
this.payCount = payCount;
}
}
2、通过二次排序获取top10品类,并写入MySQL
/**
* 获取top10热门品类
* @param filteredSessionid2AggrInfoRDD
* @param sessionid2actionRDD
*/
private static void getTop10Category(
long taskid,
JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD,
JavaPairRDD<String, Row> sessionid2actionRDD) {
/**
* 第一步:获取符合条件的session访问过的所有品类
*/
// 获取符合条件的session的访问明细
JavaPairRDD<String, Row> sessionid2detailRDD = filteredSessionid2AggrInfoRDD
.join(sessionid2actionRDD)
.mapToPair(new PairFunction<Tuple2<String,Tuple2<String,Row>>, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(
Tuple2<String, Tuple2<String, Row>> tuple) throws Exception {
return new Tuple2<String, Row>(tuple._1, tuple._2._2);
}
});
// 获取session访问过的所有品类id
// 访问过:指的是,点击过、下单过、支付过的品类
JavaPairRDD<Long, Long> categoryidRDD = sessionid2detailRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, Long>> call(
Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
List<Tuple2<Long, Long>> list = new ArrayList<Tuple2<Long, Long>>();
Long clickCategoryId = row.getLong(6);
if(clickCategoryId != null) {
list.add(new Tuple2<Long, Long>(clickCategoryId, clickCategoryId));
}
String orderCategoryIds = row.getString(8);
if(orderCategoryIds != null) {
String[] orderCategoryIdsSplited = orderCategoryIds.split(",");
for(String orderCategoryId : orderCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(orderCategoryId),
Long.valueOf(orderCategoryId)));
}
}
String payCategoryIds = row.getString(10);
if(payCategoryIds != null) {
String[] payCategoryIdsSplited = payCategoryIds.split(",");
for(String payCategoryId : payCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(payCategoryId),
Long.valueOf(payCategoryId)));
}
}
return list;
}
});
/**
* 第二步:计算各品类的点击、下单和支付的次数
*/
// 访问明细中,其中三种访问行为是:点击、下单和支付
// 分别来计算各品类点击、下单和支付的次数,可以先对访问明细数据进行过滤
// 分别过滤出点击、下单和支付行为,然后通过map、reduceByKey等算子来进行计算
// 计算各个品类的点击次数
JavaPairRDD<Long, Long> clickCategoryId2CountRDD =
getClickCategoryId2CountRDD(sessionid2detailRDD);
// 计算各个品类的下单次数
JavaPairRDD<Long, Long> orderCategoryId2CountRDD =
getOrderCategoryId2CountRDD(sessionid2detailRDD);
// 计算各个品类的支付次数
JavaPairRDD<Long, Long> payCategoryId2CountRDD =
getPayCategoryId2CountRDD(sessionid2detailRDD);
/**
* 第三步:join各品类与它的点击、下单和支付的次数
*
* categoryidRDD中,是包含了所有的符合条件的session,访问过的品类id
*
* 上面分别计算出来的三份,各品类的点击、下单和支付的次数,可能不是包含所有品类的
* 比如,有的品类,就只是被点击过,但是没有人下单和支付
*
* 所以,这里,就不能使用join操作,要使用leftOuterJoin操作,就是说,如果categoryidRDD不能
* join到自己的某个数据,比如点击、或下单、或支付次数,那么该categoryidRDD还是要保留下来的
* 只不过,没有join到的那个数据,就是0了
*
*/
JavaPairRDD<Long, String> categoryid2countRDD = joinCategoryAndData(
categoryidRDD, clickCategoryId2CountRDD, orderCategoryId2CountRDD,
payCategoryId2CountRDD);
/**
* 第四步:自定义二次排序key
*/
/**
* 第五步:将数据映射成<CategorySortKey,info>格式的RDD,然后进行二次排序(降序)
*/
JavaPairRDD<CategorySortKey, String> sortKey2countRDD = categoryid2countRDD.mapToPair(
new PairFunction<Tuple2<Long,String>, CategorySortKey, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<CategorySortKey, String> call(
Tuple2<Long, String> tuple) throws Exception {
String countInfo = tuple._2;
long clickCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_CLICK_COUNT));
long orderCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_ORDER_COUNT));
long payCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_PAY_COUNT));
CategorySortKey sortKey = new CategorySortKey(clickCount,
orderCount, payCount);
return new Tuple2<CategorySortKey, String>(sortKey, countInfo);
}
});
JavaPairRDD<CategorySortKey, String> sortedCategoryCountRDD =
sortKey2countRDD.sortByKey(false);
/**
* 第六步:用take(10)取出top10热门品类,并写入MySQL
*/
ITop10CategoryDAO top10CategoryDAO = DAOFactory.getTop10CategoryDAO();
List<Tuple2<CategorySortKey, String>> top10CategoryList =
sortedCategoryCountRDD.take(10);
for(Tuple2<CategorySortKey, String> tuple: top10CategoryList) {
String countInfo = tuple._2;
long categoryid = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_CATEGORY_ID));
long clickCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_CLICK_COUNT));
long orderCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_ORDER_COUNT));
long payCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_PAY_COUNT));
Top10Category category = new Top10Category();
category.setTaskid(taskid);
category.setCategoryid(categoryid);
category.setClickCount(clickCount);
category.setOrderCount(orderCount);
category.setPayCount(payCount);
top10CategoryDAO.insert(category);
}
}
5.5 top10活跃Session
top10热门品类,获取每个品类点击次数最多的10个session,以及其对应的访问明细
实现思路分析:
1、拿到符合筛选条件的session的明细数据
2、按照session粒度进行聚合,获取到session对每个品类的点击次数,用flatMap,算子函数返回的是
/**
* 获取top10活跃session
* @param taskid
* @param sessionid2detailRDD
*/
private static void getTop10Session(
JavaSparkContext sc,
final long taskid,
List<Tuple2<CategorySortKey, String>> top10CategoryList,
JavaPairRDD<String, Row> sessionid2detailRDD) {
/**
* 第一步:将top10热门品类的id,生成一份RDD
*/
List<Tuple2<Long, Long>> top10CategoryIdList =
new ArrayList<Tuple2<Long, Long>>();
for(Tuple2<CategorySortKey, String> category : top10CategoryList) {
long categoryid = Long.valueOf(StringUtils.getFieldFromConcatString(
category._2, "\\|", Constants.FIELD_CATEGORY_ID));
top10CategoryIdList.add(new Tuple2<Long, Long>(categoryid, categoryid));
}
JavaPairRDD<Long, Long> top10CategoryIdRDD =
sc.parallelizePairs(top10CategoryIdList);
/**
* 第二步:计算top10品类被各session点击的次数
*/
JavaPairRDD<String, Iterable<Row>> sessionid2detailsRDD =
sessionid2detailRDD.groupByKey();
JavaPairRDD<Long, String> categoryid2sessionCountRDD = sessionid2detailsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Iterable<Row>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, String>> call(
Tuple2<String, Iterable<Row>> tuple) throws Exception {
String sessionid = tuple._1;
Iterator<Row> iterator = tuple._2.iterator();
Map<Long, Long> categoryCountMap = new HashMap<Long, Long>();
// 计算出该session,对每个品类的点击次数
while(iterator.hasNext()) {
Row row = iterator.next();
if(row.get(6) != null) {
long categoryid = row.getLong(6);
Long count = categoryCountMap.get(categoryid);
if(count == null) {
count = 0L;
}
count++;
categoryCountMap.put(categoryid, count);
}
}
// 返回结果,<categoryid,sessionid,count>格式
List<Tuple2<Long, String>> list = new ArrayList<Tuple2<Long, String>>();
for(Map.Entry<Long, Long> categoryCountEntry : categoryCountMap.entrySet()) {
long categoryid = categoryCountEntry.getKey();
long count = categoryCountEntry.getValue();
String value = sessionid + "," + count;
list.add(new Tuple2<Long, String>(categoryid, value));
}
return list;
}
}) ;
// 获取到to10热门品类,被各个session点击的次数
JavaPairRDD<Long, String> top10CategorySessionCountRDD = top10CategoryIdRDD
.join(categoryid2sessionCountRDD)
.mapToPair(new PairFunction<Tuple2<Long,Tuple2<Long,String>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<Long, String>> tuple)
throws Exception {
return new Tuple2<Long, String>(tuple._1, tuple._2._2);
}
});
}
5.5.2 分组获取top10活跃session
/**
* 获取top10活跃session
* @param taskid
* @param sessionid2detailRDD
*/
private static void getTop10Session(
JavaSparkContext sc,
final long taskid,
List<Tuple2<CategorySortKey, String>> top10CategoryList,
JavaPairRDD<String, Row> sessionid2detailRDD) {
/**
* 第一步:将top10热门品类的id,生成一份RDD
*/
List<Tuple2<Long, Long>> top10CategoryIdList =
new ArrayList<Tuple2<Long, Long>>();
for(Tuple2<CategorySortKey, String> category : top10CategoryList) {
long categoryid = Long.valueOf(StringUtils.getFieldFromConcatString(
category._2, "\\|", Constants.FIELD_CATEGORY_ID));
top10CategoryIdList.add(new Tuple2<Long, Long>(categoryid, categoryid));
}
JavaPairRDD<Long, Long> top10CategoryIdRDD =
sc.parallelizePairs(top10CategoryIdList);
/**
* 第二步:计算top10品类被各session点击的次数
*/
JavaPairRDD<String, Iterable<Row>> sessionid2detailsRDD =
sessionid2detailRDD.groupByKey();
JavaPairRDD<Long, String> categoryid2sessionCountRDD = sessionid2detailsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Iterable<Row>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, String>> call(
Tuple2<String, Iterable<Row>> tuple) throws Exception {
String sessionid = tuple._1;
Iterator<Row> iterator = tuple._2.iterator();
Map<Long, Long> categoryCountMap = new HashMap<Long, Long>();
// 计算出该session,对每个品类的点击次数
while(iterator.hasNext()) {
Row row = iterator.next();
if(row.get(6) != null) {
long categoryid = row.getLong(6);
Long count = categoryCountMap.get(categoryid);
if(count == null) {
count = 0L;
}
count++;
categoryCountMap.put(categoryid, count);
}
}
// 返回结果,<categoryid,sessionid,count>格式
List<Tuple2<Long, String>> list = new ArrayList<Tuple2<Long, String>>();
for(Map.Entry<Long, Long> categoryCountEntry : categoryCountMap.entrySet()) {
long categoryid = categoryCountEntry.getKey();
long count = categoryCountEntry.getValue();
String value = sessionid + "," + count;
list.add(new Tuple2<Long, String>(categoryid, value));
}
return list;
}
}) ;
// 获取到to10热门品类,被各个session点击的次数
JavaPairRDD<Long, String> top10CategorySessionCountRDD = top10CategoryIdRDD
.join(categoryid2sessionCountRDD)
.mapToPair(new PairFunction<Tuple2<Long,Tuple2<Long,String>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<Long, String>> tuple)
throws Exception {
return new Tuple2<Long, String>(tuple._1, tuple._2._2);
}
});
/**
* 第三步:分组取TopN算法实现,获取每个品类的top10活跃用户
*/
JavaPairRDD<Long, Iterable<String>> top10CategorySessionCountsRDD =
top10CategorySessionCountRDD.groupByKey();
JavaPairRDD<String, String> top10SessionRDD = top10CategorySessionCountsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<Long,Iterable<String>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<String, String>> call(
Tuple2<Long, Iterable<String>> tuple)
throws Exception {
long categoryid = tuple._1;
Iterator<String> iterator = tuple._2.iterator();
// 定义取topn的排序数组
String[] top10Sessions = new String[10];
while(iterator.hasNext()) {
String sessionCount = iterator.next();
long count = Long.valueOf(sessionCount.split(",")[1]);
// 遍历排序数组
for(int i = 0; i < top10Sessions.length; i++) {
// 如果当前i位,没有数据,那么直接将i位数据赋值为当前sessionCount
if(top10Sessions[i] == null) {
top10Sessions[i] = sessionCount;
break;
} else {
long _count = Long.valueOf(top10Sessions[i].split(",")[1]);
// 如果sessionCount比i位的sessionCount要大
if(count > _count) {
// 从排序数组最后一位开始,到i位,所有数据往后挪一位
for(int j = 9; j > i; j--) {
top10Sessions[j] = top10Sessions[j - 1];
}
// 将i位赋值为sessionCount
top10Sessions[i] = sessionCount;
break;
}
// 比较小,继续外层for循环
}
}
}
// 将数据写入MySQL表
List<Tuple2<String, String>> list = new ArrayList<Tuple2<String, String>>();
for(String sessionCount : top10Sessions) {
String sessionid = sessionCount.split(",")[0];
long count = Long.valueOf(sessionCount.split(",")[1]);
// 将top10 session插入MySQL表
Top10Session top10Session = new Top10Session();
top10Session.setTaskid(taskid);
top10Session.setCategoryid(categoryid);
top10Session.setSessionid(sessionid);
top10Session.setClickCount(count);
ITop10SessionDAO top10SessionDAO = DAOFactory.getTop10SessionDAO();
top10SessionDAO.insert(top10Session);
// 放入list
list.add(new Tuple2<String, String>(sessionid, sessionid));
}
return list;
}
});
/**
* 第四步:获取top10活跃session的明细数据,并写入MySQL
*/
JavaPairRDD<String, Tuple2<String, Row>> sessionDetailRDD =
top10SessionRDD.join(sessionid2detailRDD);
sessionDetailRDD.foreach(new VoidFunction<Tuple2<String,Tuple2<String,Row>>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Tuple2<String, Tuple2<String, Row>> tuple) throws Exception {
Row row = tuple._2._2;
SessionDetail sessionDetail = new SessionDetail();
sessionDetail.setTaskid(taskid);
sessionDetail.setUserid(row.getLong(1));
sessionDetail.setSessionid(row.getString(2));
sessionDetail.setPageid(row.getLong(3));
sessionDetail.setActionTime(row.getString(4));
sessionDetail.setSearchKeyword(row.getString(5));
sessionDetail.setClickCategoryId(row.getLong(6));
sessionDetail.setClickProductId(row.getLong(7));
sessionDetail.setOrderCategoryIds(row.getString(8));
sessionDetail.setOrderProductIds(row.getString(9));
sessionDetail.setPayCategoryIds(row.getString(10));
sessionDetail.setPayProductIds(row.getString(11));
ISessionDetailDAO sessionDetailDAO = DAOFactory.getSessionDetailDAO();
sessionDetailDAO.insert(sessionDetail);
}
});
}
6、第一个模块完整的代码
package cn.ctgu.sparkproject.spark.session;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.apache.spark.Accumulator;
import org.apache.spark.SparkConf;
import org.apache.spark.SparkContext;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFlatMapFunction;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.sql.DataFrame;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SQLContext;
import org.apache.spark.sql.hive.HiveContext;
import scala.Tuple2;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Optional;
import cn.ctgu.sparkproject.conf.ConfigurationManager;
import cn.ctgu.sparkproject.constant.Constants;
import cn.ctgu.sparkproject.dao.ISessionAggrStatDAO;
import cn.ctgu.sparkproject.dao.ISessionDetailDAO;
import cn.ctgu.sparkproject.dao.ISessionRandomExtractDAO;
import cn.ctgu.sparkproject.dao.ITaskDAO;
import cn.ctgu.sparkproject.dao.ITop10CategoryDAO;
import cn.ctgu.sparkproject.dao.ITop10SessionDAO;
import cn.ctgu.sparkproject.dao.factory.DAOFactory;
import cn.ctgu.sparkproject.domain.SessionAggrStat;
import cn.ctgu.sparkproject.domain.SessionDetail;
import cn.ctgu.sparkproject.domain.SessionRandomExtract;
import cn.ctgu.sparkproject.domain.Task;
import cn.ctgu.sparkproject.domain.Top10Category;
import cn.ctgu.sparkproject.domain.Top10Session;
import cn.ctgu.sparkproject.test.MockData;
import cn.ctgu.sparkproject.util.DateUtils;
import cn.ctgu.sparkproject.util.NumberUtils;
import cn.ctgu.sparkproject.util.ParamUtils;
import cn.ctgu.sparkproject.util.StringUtils;
import cn.ctgu.sparkproject.util.ValidUtils;
/**
* 用户访问session分析Spark作业
*
* 接收用户创建的分析任务,用户可能指定的条件如下:
*
* 1、时间范围:起始日期~结束日期
* 2、性别:男或女
* 3、年龄范围
* 4、职业:多选
* 5、城市:多选
* 6、搜索词:多个搜索词,只要某个session中的任何一个action搜索过指定的关键词,那么session就符合条件
* 7、点击品类:多个品类,只要某个session中的任何一个action点击过某个品类,那么session就符合条件
*
* 我们的spark作业如何接受用户创建的任务?
*
* J2EE平台在接收用户创建任务的请求之后,会将任务信息插入MySQL的task表中,任务参数以JSON格式封装在task_param
* 字段中
*
* 接着J2EE平台会执行我们的spark-submit shell脚本,并将taskid作为参数传递给spark-submit shell脚本
* spark-submit shell脚本,在执行时,是可以接收参数的,并且会将接收的参数,传递给Spark作业的main函数
* 参数就封装在main函数的args数组中
*
* 这是spark本身提供的特性
*
* @author Administrator
*
*/
public class UserVisitSessionAnalyzeSpark {
public static void main(String[] args) {
args = new String[]{"2"};
// 构建Spark上下文
SparkConf conf = new SparkConf()
.setAppName(Constants.SPARK_APP_NAME_SESSION)
.setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
SQLContext sqlContext = getSQLContext(sc.sc());
// 生成模拟测试数据
mockData(sc, sqlContext);
// 创建需要使用的DAO组件
ITaskDAO taskDAO = DAOFactory.getTaskDAO();
// 首先得查询出来指定的任务,并获取任务的查询参数
long taskid = ParamUtils.getTaskIdFromArgs(args);
Task task = taskDAO.findById(taskid);
JSONObject taskParam = JSONObject.parseObject(task.getTaskParam());
// 如果要进行session粒度的数据聚合
// 首先要从user_visit_action表中,查询出来指定日期范围内的行为数据
JavaRDD<Row> actionRDD = getActionRDDByDateRange(sqlContext, taskParam);
JavaPairRDD<String, Row> sessionid2actionRDD = getSessionid2ActionRDD(actionRDD);
// 首先,可以将行为数据,按照session_id进行groupByKey分组
// 此时的数据的粒度就是session粒度了,然后呢,可以将session粒度的数据
// 与用户信息数据,进行join
// 然后就可以获取到session粒度的数据,同时呢,数据里面还包含了session对应的user的信息
// 到这里为止,获取的数据是<sessionid,(sessionid,searchKeywords,clickCategoryIds,age,professional,city,sex)>
JavaPairRDD<String, String> sessionid2AggrInfoRDD =
aggregateBySession(sqlContext, actionRDD);
// 接着,就要针对session粒度的聚合数据,按照使用者指定的筛选参数进行数据过滤
// 相当于我们自己编写的算子,是要访问外面的任务参数对象的
// 所以,大家记得我们之前说的,匿名内部类(算子函数),访问外部对象,是要给外部对象使用final修饰的
// 重构,同时进行过滤和统计
Accumulator<String> sessionAggrStatAccumulator = sc.accumulator(
"", new SessionAggrStatAccumulator());
JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD = filterSessionAndAggrStat(
sessionid2AggrInfoRDD, taskParam, sessionAggrStatAccumulator);
// 生成公共的RDD:通过筛选条件的session的访问明细数据
JavaPairRDD<String, Row> sessionid2detailRDD = getSessionid2detailRDD(
filteredSessionid2AggrInfoRDD, sessionid2actionRDD);
/**
* 对于Accumulator这种分布式累加计算的变量的使用,有一个重要说明
*
* 从Accumulator中,获取数据,插入数据库的时候,一定要,一定要,是在有某一个action操作以后
* 再进行。。。
*
* 如果没有action的话,那么整个程序根本不会运行。。。
*
* 是不是在calculateAndPersisitAggrStat方法之后,运行一个action操作,比如count、take
* 不对!!!
*
* 必须把能够触发job执行的操作,放在最终写入MySQL方法之前
*
* 计算出来的结果,在J2EE中,是怎么显示的,是用两张柱状图显示
*/
randomExtractSession(task.getTaskid(),
filteredSessionid2AggrInfoRDD, sessionid2actionRDD);
/**
* 特别说明
* 我们知道,要将上一个功能的session聚合统计数据获取到,就必须是在一个action操作触发job之后
* 才能从Accumulator中获取数据,否则是获取不到数据的,因为没有job执行,Accumulator的值为空
* 所以,我们在这里,将随机抽取的功能的实现代码,放在session聚合统计功能的最终计算和写库之前
* 因为随机抽取功能中,有一个countByKey算子,是action操作,会触发job
*/
// 计算出各个范围的session占比,并写入MySQL
calculateAndPersistAggrStat(sessionAggrStatAccumulator.value(),
task.getTaskid());
/**
* session聚合统计(统计出访问时长和访问步长,各个区间的session数量占总session数量的比例)
*
* 如果不进行重构,直接来实现,思路:
* 1、actionRDD,映射成<sessionid,Row>的格式
* 2、按sessionid聚合,计算出每个session的访问时长和访问步长,生成一个新的RDD
* 3、遍历新生成的RDD,将每个session的访问时长和访问步长,去更新自定义Accumulator中的对应的值
* 4、使用自定义Accumulator中的统计值,去计算各个区间的比例
* 5、将最后计算出来的结果,写入MySQL对应的表中
*
* 普通实现思路的问题:
* 1、为什么还要用actionRDD,去映射?其实我们之前在session聚合的时候,映射已经做过了。多此一举
* 2、是不是一定要,为了session的聚合这个功能,单独去遍历一遍session?其实没有必要,已经有session数据
* 之前过滤session的时候,其实,就相当于,是在遍历session,那么这里就没有必要再过滤一遍了
*
* 重构实现思路:
* 1、不要去生成任何新的RDD(处理上亿的数据)
* 2、不要去单独遍历一遍session的数据(处理上千万的数据)
* 3、可以在进行session聚合的时候,就直接计算出来每个session的访问时长和访问步长
* 4、在进行过滤的时候,本来就要遍历所有的聚合session信息,此时,就可以在某个session通过筛选条件后
* 将其访问时长和访问步长,累加到自定义的Accumulator上面去
* 5、就是两种截然不同的思考方式,和实现方式,在面对上亿,上千万数据的时候,甚至可以节省时间长达
* 半个小时,或者数个小时
*
* 开发Spark大型复杂项目的一些经验准则:
* 1、尽量少生成RDD
* 2、尽量少对RDD进行算子操作,如果有可能,尽量在一个算子里面,实现多个需要做的功能
* 3、尽量少对RDD进行shuffle算子操作,比如groupByKey、reduceByKey、sortByKey(map、mapToPair)
* shuffle操作,会导致大量的磁盘读写,严重降低性能
* 有shuffle的算子,和没有shuffle的算子,甚至性能,会达到几十分钟,甚至数个小时的差别
* 有shfufle的算子,很容易导致数据倾斜,一旦数据倾斜,简直就是性能杀手(完整的解决方案)
* 4、无论做什么功能,性能第一
* 在传统的J2EE或者.NET后者PHP,软件/系统/网站开发中,我认为是架构和可维护性,可扩展性的重要
* 程度,远远高于了性能,大量的分布式的架构,设计模式,代码的划分,类的划分(高并发网站除外)
*
* 在大数据项目中,比如MapReduce、Hive、Spark、Storm,我认为性能的重要程度,远远大于一些代码
* 的规范,和设计模式,代码的划分,类的划分;大数据,大数据,最重要的,就是性能
* 主要就是因为大数据以及大数据项目的特点,决定了,大数据的程序和项目的速度,都比较慢
* 如果不优先考虑性能的话,会导致一个大数据处理程序运行时间长度数个小时,甚至数十个小时
* 此时,对于用户体验,简直就是一场灾难
*
* 所以,推荐大数据项目,在开发和代码的架构中,优先考虑性能;其次考虑功能代码的划分、解耦合
*
* 我们如果采用第一种实现方案,那么其实就是代码划分(解耦合、可维护)优先,设计优先
* 如果采用第二种方案,那么其实就是性能优先
*
* 讲了这么多,其实大家不要以为我是在岔开话题,大家不要觉得项目的课程,就是单纯的项目本身以及
* 代码coding最重要,其实项目,我觉得,最重要的,除了技术本身和项目经验以外;非常重要的一点,就是
* 积累了,处理各种问题的经验
*
*/
// 获取top10热门品类
List<Tuple2<CategorySortKey, String>> top10CategoryList =
getTop10Category(task.getTaskid(), sessionid2detailRDD);
// 获取top10活跃session
getTop10Session(sc, task.getTaskid(),
top10CategoryList, sessionid2detailRDD);
// 关闭Spark上下文
sc.close();
}
/**
* 获取SQLContext
* 如果是在本地测试环境的话,那么就生成SQLContext对象
* 如果是在生产环境运行的话,那么就生成HiveContext对象
* @param sc SparkContext
* @return SQLContext
*/
private static SQLContext getSQLContext(SparkContext sc) {
boolean local = ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
return new SQLContext(sc);
} else {
return new HiveContext(sc);
}
}
/**
* 生成模拟数据(只有本地模式,才会去生成模拟数据)
* @param sc
* @param sqlContext
*/
private static void mockData(JavaSparkContext sc, SQLContext sqlContext) {
boolean local = ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
MockData.mock(sc, sqlContext);
}
}
/**
* 获取指定日期范围内的用户访问行为数据
* @param sqlContext SQLContext
* @param taskParam 任务参数
* @return 行为数据RDD
*/
private static JavaRDD<Row> getActionRDDByDateRange(
SQLContext sqlContext, JSONObject taskParam) {
String startDate = ParamUtils.getParam(taskParam, Constants.PARAM_START_DATE);
String endDate = ParamUtils.getParam(taskParam, Constants.PARAM_END_DATE);
String sql =
"select * "
+ "from user_visit_action "
+ "where date>='" + startDate + "' "
+ "and date<='" + endDate + "'";
DataFrame actionDF = sqlContext.sql(sql);
return actionDF.javaRDD();
}
/**
* 获取sessionid2到访问行为数据的映射的RDD
* @param actionRDD
* @return
*/
public static JavaPairRDD<String, Row> getSessionid2ActionRDD(JavaRDD<Row> actionRDD) {
return actionRDD.mapToPair(new PairFunction<Row, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(Row row) throws Exception {
return new Tuple2<String, Row>(row.getString(2), row);
}
});
}
/**
* 对行为数据按session粒度进行聚合
* @param actionRDD 行为数据RDD
* @return session粒度聚合数据
*/
private static JavaPairRDD<String, String> aggregateBySession(
SQLContext sqlContext, JavaRDD<Row> actionRDD) {
// 现在actionRDD中的元素是Row,一个Row就是一行用户访问行为记录,比如一次点击或者搜索
// 我们现在需要将这个Row映射成<sessionid,Row>的格式
JavaPairRDD<String, Row> sessionid2ActionRDD = actionRDD.mapToPair(
/**
* PairFunction
* 第一个参数,相当于是函数的输入
* 第二个参数和第三个参数,相当于是函数的输出(Tuple),分别是Tuple第一个和第二个值
*/
new PairFunction<Row, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(Row row) throws Exception {
return new Tuple2<String, Row>(row.getString(2), row);
}
});
// 对行为数据按session粒度进行分组
JavaPairRDD<String, Iterable<Row>> sessionid2ActionsRDD =
sessionid2ActionRDD.groupByKey();
// 对每一个session分组进行聚合,将session中所有的搜索词和点击品类都聚合起来
// 到此为止,获取的数据格式,如下:<userid,partAggrInfo(sessionid,searchKeywords,clickCategoryIds)>
JavaPairRDD<Long, String> userid2PartAggrInfoRDD = sessionid2ActionsRDD.mapToPair(
new PairFunction<Tuple2<String,Iterable<Row>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(Tuple2<String, Iterable<Row>> tuple)
throws Exception {
String sessionid = tuple._1;
Iterator<Row> iterator = tuple._2.iterator();
StringBuffer searchKeywordsBuffer = new StringBuffer("");
StringBuffer clickCategoryIdsBuffer = new StringBuffer("");
Long userid = null;
// session的起始和结束时间
Date startTime = null;
Date endTime = null;
// session的访问步长
int stepLength = 0;
// 遍历session所有的访问行为
while(iterator.hasNext()) {
// 提取每个访问行为的搜索词字段和点击品类字段
Row row = iterator.next();
if(userid == null) {
userid = row.getLong(1);
}
String searchKeyword = row.getString(5);
Long clickCategoryId = row.getLong(6);
// 实际上这里要对数据说明一下
// 并不是每一行访问行为都有searchKeyword何clickCategoryId两个字段的
// 其实,只有搜索行为,是有searchKeyword字段的
// 只有点击品类的行为,是有clickCategoryId字段的
// 所以,任何一行行为数据,都不可能两个字段都有,所以数据是可能出现null值的
// 我们决定是否将搜索词或点击品类id拼接到字符串中去
// 首先要满足:不能是null值
// 其次,之前的字符串中还没有搜索词或者点击品类id
if(StringUtils.isNotEmpty(searchKeyword)) {
if(!searchKeywordsBuffer.toString().contains(searchKeyword)) {
searchKeywordsBuffer.append(searchKeyword + ",");
}
}
if(clickCategoryId != null) {
if(!clickCategoryIdsBuffer.toString().contains(
String.valueOf(clickCategoryId))) {
clickCategoryIdsBuffer.append(clickCategoryId + ",");
}
}
// 计算session开始和结束时间
Date actionTime = DateUtils.parseTime(row.getString(4));
if(startTime == null) {
startTime = actionTime;
}
if(endTime == null) {
endTime = actionTime;
}
if(actionTime.before(startTime)) {
startTime = actionTime;
}
if(actionTime.after(endTime)) {
endTime = actionTime;
}
// 计算session访问步长
stepLength++;
}
String searchKeywords = StringUtils.trimComma(searchKeywordsBuffer.toString());
String clickCategoryIds = StringUtils.trimComma(clickCategoryIdsBuffer.toString());
// 计算session访问时长(秒)
long visitLength = (endTime.getTime() - startTime.getTime()) / 1000;
// 大家思考一下
// 我们返回的数据格式,即使<sessionid,partAggrInfo>
// 但是,这一步聚合完了以后,其实,我们是还需要将每一行数据,跟对应的用户信息进行聚合
// 问题就来了,如果是跟用户信息进行聚合的话,那么key,就不应该是sessionid
// 就应该是userid,才能够跟<userid,Row>格式的用户信息进行聚合
// 如果我们这里直接返回<sessionid,partAggrInfo>,还得再做一次mapToPair算子
// 将RDD映射成<userid,partAggrInfo>的格式,那么就多此一举
// 所以,我们这里其实可以直接,返回的数据格式,就是<userid,partAggrInfo>
// 然后跟用户信息join的时候,将partAggrInfo关联上userInfo
// 然后再直接将返回的Tuple的key设置成sessionid
// 最后的数据格式,还是<sessionid,fullAggrInfo>
// 聚合数据,用什么样的格式进行拼接?
// 我们这里统一定义,使用key=value|key=value
String partAggrInfo = Constants.FIELD_SESSION_ID + "=" + sessionid + "|"
+ Constants.FIELD_SEARCH_KEYWORDS + "=" + searchKeywords + "|"
+ Constants.FIELD_CLICK_CATEGORY_IDS + "=" + clickCategoryIds + "|"
+ Constants.FIELD_VISIT_LENGTH + "=" + visitLength + "|"
+ Constants.FIELD_STEP_LENGTH + "=" + stepLength + "|"
+ Constants.FIELD_START_TIME + "=" + DateUtils.formatTime(startTime);
return new Tuple2<Long, String>(userid, partAggrInfo);
}
});
// 查询所有用户数据,并映射成<userid,Row>的格式
String sql = "select * from user_info";
JavaRDD<Row> userInfoRDD = sqlContext.sql(sql).javaRDD();
JavaPairRDD<Long, Row> userid2InfoRDD = userInfoRDD.mapToPair(
new PairFunction<Row, Long, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, Row> call(Row row) throws Exception {
return new Tuple2<Long, Row>(row.getLong(0), row);
}
});
// 将session粒度聚合数据,与用户信息进行join
JavaPairRDD<Long, Tuple2<String, Row>> userid2FullInfoRDD =
userid2PartAggrInfoRDD.join(userid2InfoRDD);
// 对join起来的数据进行拼接,并且返回<sessionid,fullAggrInfo>格式的数据
JavaPairRDD<String, String> sessionid2FullAggrInfoRDD = userid2FullInfoRDD.mapToPair(
new PairFunction<Tuple2<Long,Tuple2<String,Row>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<Long, Tuple2<String, Row>> tuple)
throws Exception {
String partAggrInfo = tuple._2._1;
Row userInfoRow = tuple._2._2;
String sessionid = StringUtils.getFieldFromConcatString(
partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
int age = userInfoRow.getInt(3);
String professional = userInfoRow.getString(4);
String city = userInfoRow.getString(5);
String sex = userInfoRow.getString(6);
String fullAggrInfo = partAggrInfo + "|"
+ Constants.FIELD_AGE + "=" + age + "|"
+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
+ Constants.FIELD_CITY + "=" + city + "|"
+ Constants.FIELD_SEX + "=" + sex;
return new Tuple2<String, String>(sessionid, fullAggrInfo);
}
});
return sessionid2FullAggrInfoRDD;
}
/**
* 过滤session数据,并进行聚合统计
* @param sessionid2AggrInfoRDD
* @return
*/
private static JavaPairRDD<String, String> filterSessionAndAggrStat(
JavaPairRDD<String, String> sessionid2AggrInfoRDD,
final JSONObject taskParam,
final Accumulator<String> sessionAggrStatAccumulator) {
// 为了使用我们后面的ValieUtils,所以,首先将所有的筛选参数拼接成一个连接串
// 此外,这里其实大家不要觉得是多此一举
// 其实我们是给后面的性能优化埋下了一个伏笔
String startAge = ParamUtils.getParam(taskParam, Constants.PARAM_START_AGE);
String endAge = ParamUtils.getParam(taskParam, Constants.PARAM_END_AGE);
String professionals = ParamUtils.getParam(taskParam, Constants.PARAM_PROFESSIONALS);
String cities = ParamUtils.getParam(taskParam, Constants.PARAM_CITIES);
String sex = ParamUtils.getParam(taskParam, Constants.PARAM_SEX);
String keywords = ParamUtils.getParam(taskParam, Constants.PARAM_KEYWORDS);
String categoryIds = ParamUtils.getParam(taskParam, Constants.PARAM_CATEGORY_IDS);
String _parameter = (startAge != null ? Constants.PARAM_START_AGE + "=" + startAge + "|" : "")
+ (endAge != null ? Constants.PARAM_END_AGE + "=" + endAge + "|" : "")
+ (professionals != null ? Constants.PARAM_PROFESSIONALS + "=" + professionals + "|" : "")
+ (cities != null ? Constants.PARAM_CITIES + "=" + cities + "|" : "")
+ (sex != null ? Constants.PARAM_SEX + "=" + sex + "|" : "")
+ (keywords != null ? Constants.PARAM_KEYWORDS + "=" + keywords + "|" : "")
+ (categoryIds != null ? Constants.PARAM_CATEGORY_IDS + "=" + categoryIds: "");
if(_parameter.endsWith("\\|")) {
_parameter = _parameter.substring(0, _parameter.length() - 1);
}
final String parameter = _parameter;
// 根据筛选参数进行过滤
JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD = sessionid2AggrInfoRDD.filter(
new Function<Tuple2<String,String>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, String> tuple) throws Exception {
// 首先,从tuple中,获取聚合数据
String aggrInfo = tuple._2;
// 接着,依次按照筛选条件进行过滤
// 按照年龄范围进行过滤(startAge、endAge)
if(!ValidUtils.between(aggrInfo, Constants.FIELD_AGE,
parameter, Constants.PARAM_START_AGE, Constants.PARAM_END_AGE)) {
return false;
}
// 按照职业范围进行过滤(professionals)
// 互联网,IT,软件
// 互联网
if(!ValidUtils.in(aggrInfo, Constants.FIELD_PROFESSIONAL,
parameter, Constants.PARAM_PROFESSIONALS)) {
return false;
}
// 按照城市范围进行过滤(cities)
// 北京,上海,广州,深圳
// 成都
if(!ValidUtils.in(aggrInfo, Constants.FIELD_CITY,
parameter, Constants.PARAM_CITIES)) {
return false;
}
// 按照性别进行过滤
// 男/女
// 男,女
if(!ValidUtils.equal(aggrInfo, Constants.FIELD_SEX,
parameter, Constants.PARAM_SEX)) {
return false;
}
// 按照搜索词进行过滤
// 我们的session可能搜索了 火锅,蛋糕,烧烤
// 我们的筛选条件可能是 火锅,串串香,iphone手机
// 那么,in这个校验方法,主要判定session搜索的词中,有任何一个,与筛选条件中
// 任何一个搜索词相当,即通过
if(!ValidUtils.in(aggrInfo, Constants.FIELD_SEARCH_KEYWORDS,
parameter, Constants.PARAM_KEYWORDS)) {
return false;
}
// 按照点击品类id进行过滤
if(!ValidUtils.in(aggrInfo, Constants.FIELD_CLICK_CATEGORY_IDS,
parameter, Constants.PARAM_CATEGORY_IDS)) {
return false;
}
// 如果经过了之前的多个过滤条件之后,程序能够走到这里
// 那么就说明,该session是通过了用户指定的筛选条件的,也就是需要保留的session
// 那么就要对session的访问时长和访问步长,进行统计,根据session对应的范围
// 进行相应的累加计数
// 主要走到这一步,那么就是需要计数的session
sessionAggrStatAccumulator.add(Constants.SESSION_COUNT);
// 计算出session的访问时长和访问步长的范围,并进行相应的累加
long visitLength = Long.valueOf(StringUtils.getFieldFromConcatString(
aggrInfo, "\\|", Constants.FIELD_VISIT_LENGTH));
long stepLength = Long.valueOf(StringUtils.getFieldFromConcatString(
aggrInfo, "\\|", Constants.FIELD_STEP_LENGTH));
calculateVisitLength(visitLength);
calculateStepLength(stepLength);
return true;
}
/**
* 计算访问时长范围
* @param visitLength
*/
private void calculateVisitLength(long visitLength) {
if(visitLength >=1 && visitLength <= 3) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_1s_3s);
} else if(visitLength >=4 && visitLength <= 6) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_4s_6s);
} else if(visitLength >=7 && visitLength <= 9) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_7s_9s);
} else if(visitLength >=10 && visitLength <= 30) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_10s_30s);
} else if(visitLength > 30 && visitLength <= 60) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_30s_60s);
} else if(visitLength > 60 && visitLength <= 180) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_1m_3m);
} else if(visitLength > 180 && visitLength <= 600) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_3m_10m);
} else if(visitLength > 600 && visitLength <= 1800) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_10m_30m);
} else if(visitLength > 1800) {
sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_30m);
}
}
/**
* 计算访问步长范围
* @param stepLength
*/
private void calculateStepLength(long stepLength) {
if(stepLength >= 1 && stepLength <= 3) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_1_3);
} else if(stepLength >= 4 && stepLength <= 6) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_4_6);
} else if(stepLength >= 7 && stepLength <= 9) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_7_9);
} else if(stepLength >= 10 && stepLength <= 30) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_10_30);
} else if(stepLength > 30 && stepLength <= 60) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_30_60);
} else if(stepLength > 60) {
sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_60);
}
}
});
return filteredSessionid2AggrInfoRDD;
}
/**
* 获取通过筛选条件的session的访问明细数据RDD
* @param sessionid2aggrInfoRDD
* @param sessionid2actionRDD
* @return
*/
private static JavaPairRDD<String, Row> getSessionid2detailRDD(
JavaPairRDD<String, String> sessionid2aggrInfoRDD,
JavaPairRDD<String, Row> sessionid2actionRDD) {
JavaPairRDD<String, Row> sessionid2detailRDD = sessionid2aggrInfoRDD
.join(sessionid2actionRDD)
.mapToPair(new PairFunction<Tuple2<String,Tuple2<String,Row>>, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(
Tuple2<String, Tuple2<String, Row>> tuple) throws Exception {
return new Tuple2<String, Row>(tuple._1, tuple._2._2);
}
});
return sessionid2detailRDD;
}
/**
* 随机抽取session
* @param sessionid2AggrInfoRDD
*/
private static void randomExtractSession(
final long taskid,
JavaPairRDD<String, String> sessionid2AggrInfoRDD,
JavaPairRDD<String, Row> sessionid2actionRDD) {
/**
* 第一步,计算出每天每小时的session数量
*/
// 获取<yyyy-MM-dd_HH,aggrInfo>格式的RDD
JavaPairRDD<String, String> time2sessionidRDD = sessionid2AggrInfoRDD.mapToPair(
new PairFunction<Tuple2<String,String>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<String, String> tuple) throws Exception {
String aggrInfo = tuple._2;
String startTime = StringUtils.getFieldFromConcatString(
aggrInfo, "\\|", Constants.FIELD_START_TIME);
String dateHour = DateUtils.getDateHour(startTime);
return new Tuple2<String, String>(dateHour, aggrInfo);
}
});
/**
* 思考一下:这里我们不要着急写大量的代码,做项目的时候,一定要用脑子多思考
*
* 每天每小时的session数量,然后计算出每天每小时的session抽取索引,遍历每天每小时session
* 首先抽取出的session的聚合数据,写入session_random_extract表
* 所以第一个RDD的value,应该是session聚合数据
*
*/
// 得到每天每小时的session数量
Map<String, Object> countMap = time2sessionidRDD.countByKey();
/**
* 第二步,使用按时间比例随机抽取算法,计算出每天每小时要抽取session的索引
*/
// 将<yyyy-MM-dd_HH,count>格式的map,转换成<yyyy-MM-dd,<HH,count>>的格式
Map<String, Map<String, Long>> dateHourCountMap =
new HashMap<String, Map<String, Long>>();
for(Map.Entry<String, Object> countEntry : countMap.entrySet()) {
String dateHour = countEntry.getKey();
String date = dateHour.split("_")[0];
String hour = dateHour.split("_")[1];
long count = Long.valueOf(String.valueOf(countEntry.getValue()));
Map<String, Long> hourCountMap = dateHourCountMap.get(date);
if(hourCountMap == null) {
hourCountMap = new HashMap<String, Long>();
dateHourCountMap.put(date, hourCountMap);
}
hourCountMap.put(hour, count);
}
// 开始实现我们的按时间比例随机抽取算法
// 总共要抽取100个session,先按照天数,进行平分
int extractNumberPerDay = 100 / dateHourCountMap.size();
// <date,<hour,(3,5,20,102)>>
final Map<String, Map<String, List<Integer>>> dateHourExtractMap =
new HashMap<String, Map<String, List<Integer>>>();
Random random = new Random();
for(Map.Entry<String, Map<String, Long>> dateHourCountEntry : dateHourCountMap.entrySet()) {
String date = dateHourCountEntry.getKey();
Map<String, Long> hourCountMap = dateHourCountEntry.getValue();
// 计算出这一天的session总数
long sessionCount = 0L;
for(long hourCount : hourCountMap.values()) {
sessionCount += hourCount;
}
Map<String, List<Integer>> hourExtractMap = dateHourExtractMap.get(date);
if(hourExtractMap == null) {
hourExtractMap = new HashMap<String, List<Integer>>();
dateHourExtractMap.put(date, hourExtractMap);
}
// 遍历每个小时
for(Map.Entry<String, Long> hourCountEntry : hourCountMap.entrySet()) {
String hour = hourCountEntry.getKey();
long count = hourCountEntry.getValue();
// 计算每个小时的session数量,占据当天总session数量的比例,直接乘以每天要抽取的数量
// 就可以计算出,当前小时需要抽取的session数量
int hourExtractNumber = (int)(((double)count / (double)sessionCount)
* extractNumberPerDay);
if(hourExtractNumber > count) {
hourExtractNumber = (int) count;
}
// 先获取当前小时的存放随机数的list
List<Integer> extractIndexList = hourExtractMap.get(hour);
if(extractIndexList == null) {
extractIndexList = new ArrayList<Integer>();
hourExtractMap.put(hour, extractIndexList);
}
// 生成上面计算出来的数量的随机数
for(int i = 0; i < hourExtractNumber; i++) {
int extractIndex = random.nextInt((int) count);
while(extractIndexList.contains(extractIndex)) {
extractIndex = random.nextInt((int) count);
}
extractIndexList.add(extractIndex);
}
}
}
/**
* 第三步:遍历每天每小时的session,然后根据随机索引进行抽取
*/
// 执行groupByKey算子,得到<dateHour,(session aggrInfo)>
JavaPairRDD<String, Iterable<String>> time2sessionsRDD = time2sessionidRDD.groupByKey();
// 我们用flatMap算子,遍历所有的<dateHour,(session aggrInfo)>格式的数据
// 然后呢,会遍历每天每小时的session
// 如果发现某个session恰巧在我们指定的这天这小时的随机抽取索引上
// 那么抽取该session,直接写入MySQL的random_extract_session表
// 将抽取出来的session id返回回来,形成一个新的JavaRDD<String>
// 然后最后一步,是用抽取出来的sessionid,去join它们的访问行为明细数据,写入session表
JavaPairRDD<String, String> extractSessionidsRDD = time2sessionsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Iterable<String>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<String, String>> call(
Tuple2<String, Iterable<String>> tuple)
throws Exception {
List<Tuple2<String, String>> extractSessionids =
new ArrayList<Tuple2<String, String>>();
String dateHour = tuple._1;
String date = dateHour.split("_")[0];
String hour = dateHour.split("_")[1];
Iterator<String> iterator = tuple._2.iterator();
List<Integer> extractIndexList = dateHourExtractMap.get(date).get(hour);
ISessionRandomExtractDAO sessionRandomExtractDAO =
DAOFactory.getSessionRandomExtractDAO();
int index = 0;
while(iterator.hasNext()) {
String sessionAggrInfo = iterator.next();
if(extractIndexList.contains(index)) {
String sessionid = StringUtils.getFieldFromConcatString(
sessionAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
// 将数据写入MySQL
SessionRandomExtract sessionRandomExtract = new SessionRandomExtract();
sessionRandomExtract.setTaskid(taskid);
sessionRandomExtract.setSessionid(sessionid);
sessionRandomExtract.setStartTime(StringUtils.getFieldFromConcatString(
sessionAggrInfo, "\\|", Constants.FIELD_START_TIME));
sessionRandomExtract.setSearchKeywords(StringUtils.getFieldFromConcatString(
sessionAggrInfo, "\\|", Constants.FIELD_SEARCH_KEYWORDS));
sessionRandomExtract.setClickCategoryIds(StringUtils.getFieldFromConcatString(
sessionAggrInfo, "\\|", Constants.FIELD_CLICK_CATEGORY_IDS));
sessionRandomExtractDAO.insert(sessionRandomExtract);
// 将sessionid加入list
extractSessionids.add(new Tuple2<String, String>(sessionid, sessionid));
}
index++;
}
return extractSessionids;
}
});
/**
* 第四步:获取抽取出来的session的明细数据
*/
JavaPairRDD<String, Tuple2<String, Row>> extractSessionDetailRDD =
extractSessionidsRDD.join(sessionid2actionRDD);
extractSessionDetailRDD.foreach(new VoidFunction<Tuple2<String,Tuple2<String,Row>>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Tuple2<String, Tuple2<String, Row>> tuple) throws Exception {
Row row = tuple._2._2;
SessionDetail sessionDetail = new SessionDetail();
sessionDetail.setTaskid(taskid);
sessionDetail.setUserid(row.getLong(1));
sessionDetail.setSessionid(row.getString(2));
sessionDetail.setPageid(row.getLong(3));
sessionDetail.setActionTime(row.getString(4));
sessionDetail.setSearchKeyword(row.getString(5));
sessionDetail.setClickCategoryId(row.getLong(6));
sessionDetail.setClickProductId(row.getLong(7));
sessionDetail.setOrderCategoryIds(row.getString(8));
sessionDetail.setOrderProductIds(row.getString(9));
sessionDetail.setPayCategoryIds(row.getString(10));
sessionDetail.setPayProductIds(row.getString(11));
ISessionDetailDAO sessionDetailDAO = DAOFactory.getSessionDetailDAO();
sessionDetailDAO.insert(sessionDetail);
}
});
}
/**
* 计算各session范围占比,并写入MySQL
* @param value
*/
private static void calculateAndPersistAggrStat(String value, long taskid) {
// 从Accumulator统计串中获取值
long session_count = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.SESSION_COUNT));
long visit_length_1s_3s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_1s_3s));
long visit_length_4s_6s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_4s_6s));
long visit_length_7s_9s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_7s_9s));
long visit_length_10s_30s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_10s_30s));
long visit_length_30s_60s = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_30s_60s));
long visit_length_1m_3m = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_1m_3m));
long visit_length_3m_10m = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_3m_10m));
long visit_length_10m_30m = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_10m_30m));
long visit_length_30m = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.TIME_PERIOD_30m));
long step_length_1_3 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_1_3));
long step_length_4_6 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_4_6));
long step_length_7_9 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_7_9));
long step_length_10_30 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_10_30));
long step_length_30_60 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_30_60));
long step_length_60 = Long.valueOf(StringUtils.getFieldFromConcatString(
value, "\\|", Constants.STEP_PERIOD_60));
// 计算各个访问时长和访问步长的范围
double visit_length_1s_3s_ratio = NumberUtils.formatDouble(
(double)visit_length_1s_3s / (double)session_count, 2);
double visit_length_4s_6s_ratio = NumberUtils.formatDouble(
(double)visit_length_4s_6s / (double)session_count, 2);
double visit_length_7s_9s_ratio = NumberUtils.formatDouble(
(double)visit_length_7s_9s / (double)session_count, 2);
double visit_length_10s_30s_ratio = NumberUtils.formatDouble(
(double)visit_length_10s_30s / (double)session_count, 2);
double visit_length_30s_60s_ratio = NumberUtils.formatDouble(
(double)visit_length_30s_60s / (double)session_count, 2);
double visit_length_1m_3m_ratio = NumberUtils.formatDouble(
(double)visit_length_1m_3m / (double)session_count, 2);
double visit_length_3m_10m_ratio = NumberUtils.formatDouble(
(double)visit_length_3m_10m / (double)session_count, 2);
double visit_length_10m_30m_ratio = NumberUtils.formatDouble(
(double)visit_length_10m_30m / (double)session_count, 2);
double visit_length_30m_ratio = NumberUtils.formatDouble(
(double)visit_length_30m / (double)session_count, 2);
double step_length_1_3_ratio = NumberUtils.formatDouble(
(double)step_length_1_3 / (double)session_count, 2);
double step_length_4_6_ratio = NumberUtils.formatDouble(
(double)step_length_4_6 / (double)session_count, 2);
double step_length_7_9_ratio = NumberUtils.formatDouble(
(double)step_length_7_9 / (double)session_count, 2);
double step_length_10_30_ratio = NumberUtils.formatDouble(
(double)step_length_10_30 / (double)session_count, 2);
double step_length_30_60_ratio = NumberUtils.formatDouble(
(double)step_length_30_60 / (double)session_count, 2);
double step_length_60_ratio = NumberUtils.formatDouble(
(double)step_length_60 / (double)session_count, 2);
// 将统计结果封装为Domain对象
SessionAggrStat sessionAggrStat = new SessionAggrStat();
sessionAggrStat.setTaskid(taskid);
sessionAggrStat.setSession_count(session_count);
sessionAggrStat.setVisit_length_1s_3s_ratio(visit_length_1s_3s_ratio);
sessionAggrStat.setVisit_length_4s_6s_ratio(visit_length_4s_6s_ratio);
sessionAggrStat.setVisit_length_7s_9s_ratio(visit_length_7s_9s_ratio);
sessionAggrStat.setVisit_length_10s_30s_ratio(visit_length_10s_30s_ratio);
sessionAggrStat.setVisit_length_30s_60s_ratio(visit_length_30s_60s_ratio);
sessionAggrStat.setVisit_length_1m_3m_ratio(visit_length_1m_3m_ratio);
sessionAggrStat.setVisit_length_3m_10m_ratio(visit_length_3m_10m_ratio);
sessionAggrStat.setVisit_length_10m_30m_ratio(visit_length_10m_30m_ratio);
sessionAggrStat.setVisit_length_30m_ratio(visit_length_30m_ratio);
sessionAggrStat.setStep_length_1_3_ratio(step_length_1_3_ratio);
sessionAggrStat.setStep_length_4_6_ratio(step_length_4_6_ratio);
sessionAggrStat.setStep_length_7_9_ratio(step_length_7_9_ratio);
sessionAggrStat.setStep_length_10_30_ratio(step_length_10_30_ratio);
sessionAggrStat.setStep_length_30_60_ratio(step_length_30_60_ratio);
sessionAggrStat.setStep_length_60_ratio(step_length_60_ratio);
// 调用对应的DAO插入统计结果
ISessionAggrStatDAO sessionAggrStatDAO = DAOFactory.getSessionAggrStatDAO();
sessionAggrStatDAO.insert(sessionAggrStat);
}
/**
* 获取top10热门品类
* @param filteredSessionid2AggrInfoRDD
* @param sessionid2actionRDD
*/
private static List<Tuple2<CategorySortKey, String>> getTop10Category(
long taskid,
JavaPairRDD<String, Row> sessionid2detailRDD) {
/**
* 第一步:获取符合条件的session访问过的所有品类
*/
// 获取session访问过的所有品类id
// 访问过:指的是,点击过、下单过、支付过的品类
JavaPairRDD<Long, Long> categoryidRDD = sessionid2detailRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, Long>> call(
Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
List<Tuple2<Long, Long>> list = new ArrayList<Tuple2<Long, Long>>();
Long clickCategoryId = row.getLong(6);
if(clickCategoryId != null) {
list.add(new Tuple2<Long, Long>(clickCategoryId, clickCategoryId));
}
String orderCategoryIds = row.getString(8);
if(orderCategoryIds != null) {
String[] orderCategoryIdsSplited = orderCategoryIds.split(",");
for(String orderCategoryId : orderCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(orderCategoryId),
Long.valueOf(orderCategoryId)));
}
}
String payCategoryIds = row.getString(10);
if(payCategoryIds != null) {
String[] payCategoryIdsSplited = payCategoryIds.split(",");
for(String payCategoryId : payCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(payCategoryId),
Long.valueOf(payCategoryId)));
}
}
return list;
}
});
/**
* 必须要进行去重
* 如果不去重的话,会出现重复的categoryid,排序会对重复的categoryid已经countInfo进行排序
* 最后很可能会拿到重复的数据
*/
categoryidRDD = categoryidRDD.distinct();
/**
* 第二步:计算各品类的点击、下单和支付的次数
*/
// 访问明细中,其中三种访问行为是:点击、下单和支付
// 分别来计算各品类点击、下单和支付的次数,可以先对访问明细数据进行过滤
// 分别过滤出点击、下单和支付行为,然后通过map、reduceByKey等算子来进行计算
// 计算各个品类的点击次数
JavaPairRDD<Long, Long> clickCategoryId2CountRDD =
getClickCategoryId2CountRDD(sessionid2detailRDD);
// 计算各个品类的下单次数
JavaPairRDD<Long, Long> orderCategoryId2CountRDD =
getOrderCategoryId2CountRDD(sessionid2detailRDD);
// 计算各个品类的支付次数
JavaPairRDD<Long, Long> payCategoryId2CountRDD =
getPayCategoryId2CountRDD(sessionid2detailRDD);
/**
* 第三步:join各品类与它的点击、下单和支付的次数
*
* categoryidRDD中,是包含了所有的符合条件的session,访问过的品类id
*
* 上面分别计算出来的三份,各品类的点击、下单和支付的次数,可能不是包含所有品类的
* 比如,有的品类,就只是被点击过,但是没有人下单和支付
*
* 所以,这里,就不能使用join操作,要使用leftOuterJoin操作,就是说,如果categoryidRDD不能
* join到自己的某个数据,比如点击、或下单、或支付次数,那么该categoryidRDD还是要保留下来的
* 只不过,没有join到的那个数据,就是0了
*
*/
JavaPairRDD<Long, String> categoryid2countRDD = joinCategoryAndData(
categoryidRDD, clickCategoryId2CountRDD, orderCategoryId2CountRDD,
payCategoryId2CountRDD);
/**
* 第四步:自定义二次排序key
*/
/**
* 第五步:将数据映射成<CategorySortKey,info>格式的RDD,然后进行二次排序(降序)
*/
JavaPairRDD<CategorySortKey, String> sortKey2countRDD = categoryid2countRDD.mapToPair(
new PairFunction<Tuple2<Long,String>, CategorySortKey, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<CategorySortKey, String> call(
Tuple2<Long, String> tuple) throws Exception {
String countInfo = tuple._2;
long clickCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_CLICK_COUNT));
long orderCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_ORDER_COUNT));
long payCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_PAY_COUNT));
CategorySortKey sortKey = new CategorySortKey(clickCount,
orderCount, payCount);
return new Tuple2<CategorySortKey, String>(sortKey, countInfo);
}
});
JavaPairRDD<CategorySortKey, String> sortedCategoryCountRDD =
sortKey2countRDD.sortByKey(false);
/**
* 第六步:用take(10)取出top10热门品类,并写入MySQL
*/
ITop10CategoryDAO top10CategoryDAO = DAOFactory.getTop10CategoryDAO();
List<Tuple2<CategorySortKey, String>> top10CategoryList =
sortedCategoryCountRDD.take(10);
for(Tuple2<CategorySortKey, String> tuple: top10CategoryList) {
String countInfo = tuple._2;
long categoryid = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_CATEGORY_ID));
long clickCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_CLICK_COUNT));
long orderCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_ORDER_COUNT));
long payCount = Long.valueOf(StringUtils.getFieldFromConcatString(
countInfo, "\\|", Constants.FIELD_PAY_COUNT));
Top10Category category = new Top10Category();
category.setTaskid(taskid);
category.setCategoryid(categoryid);
category.setClickCount(clickCount);
category.setOrderCount(orderCount);
category.setPayCount(payCount);
top10CategoryDAO.insert(category);
}
return top10CategoryList;
}
/**
* 获取各品类点击次数RDD
* @param sessionid2detailRDD
* @return
*/
private static JavaPairRDD<Long, Long> getClickCategoryId2CountRDD(
JavaPairRDD<String, Row> sessionid2detailRDD) {
JavaPairRDD<String, Row> clickActionRDD = sessionid2detailRDD.filter(
new Function<Tuple2<String,Row>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
return row.get(6) != null ? true : false;
}
});
JavaPairRDD<Long, Long> clickCategoryIdRDD = clickActionRDD.mapToPair(
new PairFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, Long> call(Tuple2<String, Row> tuple)
throws Exception {
long clickCategoryId = tuple._2.getLong(6);
return new Tuple2<Long, Long>(clickCategoryId, 1L);
}
});
JavaPairRDD<Long, Long> clickCategoryId2CountRDD = clickCategoryIdRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
return clickCategoryId2CountRDD;
}
/**
* 获取各品类的下单次数RDD
* @param sessionid2detailRDD
* @return
*/
private static JavaPairRDD<Long, Long> getOrderCategoryId2CountRDD(
JavaPairRDD<String, Row> sessionid2detailRDD) {
JavaPairRDD<String, Row> orderActionRDD = sessionid2detailRDD.filter(
new Function<Tuple2<String,Row>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
return row.getString(8) != null ? true : false;
}
});
JavaPairRDD<Long, Long> orderCategoryIdRDD = orderActionRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, Long>> call(
Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
String orderCategoryIds = row.getString(8);
String[] orderCategoryIdsSplited = orderCategoryIds.split(",");
List<Tuple2<Long, Long>> list = new ArrayList<Tuple2<Long, Long>>();
for(String orderCategoryId : orderCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(orderCategoryId), 1L));
}
return list;
}
});
JavaPairRDD<Long, Long> orderCategoryId2CountRDD = orderCategoryIdRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
return orderCategoryId2CountRDD;
}
/**
* 获取各个品类的支付次数RDD
* @param sessionid2detailRDD
* @return
*/
private static JavaPairRDD<Long, Long> getPayCategoryId2CountRDD(
JavaPairRDD<String, Row> sessionid2detailRDD) {
JavaPairRDD<String, Row> payActionRDD = sessionid2detailRDD.filter(
new Function<Tuple2<String,Row>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
return row.getString(10) != null ? true : false;
}
});
JavaPairRDD<Long, Long> payCategoryIdRDD = payActionRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Row>, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, Long>> call(
Tuple2<String, Row> tuple) throws Exception {
Row row = tuple._2;
String payCategoryIds = row.getString(10);
String[] payCategoryIdsSplited = payCategoryIds.split(",");
List<Tuple2<Long, Long>> list = new ArrayList<Tuple2<Long, Long>>();
for(String payCategoryId : payCategoryIdsSplited) {
list.add(new Tuple2<Long, Long>(Long.valueOf(payCategoryId), 1L));
}
return list;
}
});
JavaPairRDD<Long, Long> payCategoryId2CountRDD = payCategoryIdRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
return payCategoryId2CountRDD;
}
/**
* 连接品类RDD与数据RDD
* @param categoryidRDD
* @param clickCategoryId2CountRDD
* @param orderCategoryId2CountRDD
* @param payCategoryId2CountRDD
* @return
*/
private static JavaPairRDD<Long, String> joinCategoryAndData(
JavaPairRDD<Long, Long> categoryidRDD,
JavaPairRDD<Long, Long> clickCategoryId2CountRDD,
JavaPairRDD<Long, Long> orderCategoryId2CountRDD,
JavaPairRDD<Long, Long> payCategoryId2CountRDD) {
// 解释一下,如果用leftOuterJoin,就可能出现,右边那个RDD中,join过来时,没有值
// 所以Tuple中的第二个值用Optional<Long>类型,就代表,可能有值,可能没有值
JavaPairRDD<Long, Tuple2<Long, Optional<Long>>> tmpJoinRDD =
categoryidRDD.leftOuterJoin(clickCategoryId2CountRDD);
JavaPairRDD<Long, String> tmpMapRDD = tmpJoinRDD.mapToPair(
new PairFunction<Tuple2<Long,Tuple2<Long,Optional<Long>>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<Long, Optional<Long>>> tuple)
throws Exception {
long categoryid = tuple._1;
Optional<Long> optional = tuple._2._2;
long clickCount = 0L;
if(optional.isPresent()) {
clickCount = optional.get();
}
String value = Constants.FIELD_CATEGORY_ID + "=" + categoryid + "|" +
Constants.FIELD_CLICK_COUNT + "=" + clickCount;
return new Tuple2<Long, String>(categoryid, value);
}
});
tmpMapRDD = tmpMapRDD.leftOuterJoin(orderCategoryId2CountRDD).mapToPair(
new PairFunction<Tuple2<Long,Tuple2<String,Optional<Long>>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<String, Optional<Long>>> tuple)
throws Exception {
long categoryid = tuple._1;
String value = tuple._2._1;
Optional<Long> optional = tuple._2._2;
long orderCount = 0L;
if(optional.isPresent()) {
orderCount = optional.get();
}
value = value + "|" + Constants.FIELD_ORDER_COUNT + "=" + orderCount;
return new Tuple2<Long, String>(categoryid, value);
}
});
tmpMapRDD = tmpMapRDD.leftOuterJoin(payCategoryId2CountRDD).mapToPair(
new PairFunction<Tuple2<Long,Tuple2<String,Optional<Long>>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<String, Optional<Long>>> tuple)
throws Exception {
long categoryid = tuple._1;
String value = tuple._2._1;
Optional<Long> optional = tuple._2._2;
long payCount = 0L;
if(optional.isPresent()) {
payCount = optional.get();
}
value = value + "|" + Constants.FIELD_PAY_COUNT + "=" + payCount;
return new Tuple2<Long, String>(categoryid, value);
}
});
return tmpMapRDD;
}
/**
* 获取top10活跃session
* @param taskid
* @param sessionid2detailRDD
*/
private static void getTop10Session(
JavaSparkContext sc,
final long taskid,
List<Tuple2<CategorySortKey, String>> top10CategoryList,
JavaPairRDD<String, Row> sessionid2detailRDD) {
/**
* 第一步:将top10热门品类的id,生成一份RDD
*/
List<Tuple2<Long, Long>> top10CategoryIdList =
new ArrayList<Tuple2<Long, Long>>();
for(Tuple2<CategorySortKey, String> category : top10CategoryList) {
long categoryid = Long.valueOf(StringUtils.getFieldFromConcatString(
category._2, "\\|", Constants.FIELD_CATEGORY_ID));
top10CategoryIdList.add(new Tuple2<Long, Long>(categoryid, categoryid));
}
JavaPairRDD<Long, Long> top10CategoryIdRDD =
sc.parallelizePairs(top10CategoryIdList);
/**
* 第二步:计算top10品类被各session点击的次数
*/
JavaPairRDD<String, Iterable<Row>> sessionid2detailsRDD =
sessionid2detailRDD.groupByKey();
JavaPairRDD<Long, String> categoryid2sessionCountRDD = sessionid2detailsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Iterable<Row>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<Long, String>> call(
Tuple2<String, Iterable<Row>> tuple) throws Exception {
String sessionid = tuple._1;
Iterator<Row> iterator = tuple._2.iterator();
Map<Long, Long> categoryCountMap = new HashMap<Long, Long>();
// 计算出该session,对每个品类的点击次数
while(iterator.hasNext()) {
Row row = iterator.next();
if(row.get(6) != null) {
long categoryid = row.getLong(6);
Long count = categoryCountMap.get(categoryid);
if(count == null) {
count = 0L;
}
count++;
categoryCountMap.put(categoryid, count);
}
}
// 返回结果,<categoryid,sessionid,count>格式
List<Tuple2<Long, String>> list = new ArrayList<Tuple2<Long, String>>();
for(Map.Entry<Long, Long> categoryCountEntry : categoryCountMap.entrySet()) {
long categoryid = categoryCountEntry.getKey();
long count = categoryCountEntry.getValue();
String value = sessionid + "," + count;
list.add(new Tuple2<Long, String>(categoryid, value));
}
return list;
}
}) ;
// 获取到to10热门品类,被各个session点击的次数
JavaPairRDD<Long, String> top10CategorySessionCountRDD = top10CategoryIdRDD
.join(categoryid2sessionCountRDD)
.mapToPair(new PairFunction<Tuple2<Long,Tuple2<Long,String>>, Long, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, String> call(
Tuple2<Long, Tuple2<Long, String>> tuple)
throws Exception {
return new Tuple2<Long, String>(tuple._1, tuple._2._2);
}
});
/**
* 第三步:分组取TopN算法实现,获取每个品类的top10活跃用户
*/
JavaPairRDD<Long, Iterable<String>> top10CategorySessionCountsRDD =
top10CategorySessionCountRDD.groupByKey();
JavaPairRDD<String, String> top10SessionRDD = top10CategorySessionCountsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<Long,Iterable<String>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<String, String>> call(
Tuple2<Long, Iterable<String>> tuple)
throws Exception {
long categoryid = tuple._1;
Iterator<String> iterator = tuple._2.iterator();
// 定义取topn的排序数组
String[] top10Sessions = new String[10];
while(iterator.hasNext()) {
String sessionCount = iterator.next();
long count = Long.valueOf(sessionCount.split(",")[1]);
// 遍历排序数组
for(int i = 0; i < top10Sessions.length; i++) {
// 如果当前i位,没有数据,那么直接将i位数据赋值为当前sessionCount
if(top10Sessions[i] == null) {
top10Sessions[i] = sessionCount;
break;
} else {
long _count = Long.valueOf(top10Sessions[i].split(",")[1]);
// 如果sessionCount比i位的sessionCount要大
if(count > _count) {
// 从排序数组最后一位开始,到i位,所有数据往后挪一位
for(int j = 9; j > i; j--) {
top10Sessions[j] = top10Sessions[j - 1];
}
// 将i位赋值为sessionCount
top10Sessions[i] = sessionCount;
break;
}
// 比较小,继续外层for循环
}
}
}
// 将数据写入MySQL表
List<Tuple2<String, String>> list = new ArrayList<Tuple2<String, String>>();
for(String sessionCount : top10Sessions) {
String sessionid = sessionCount.split(",")[0];
long count = Long.valueOf(sessionCount.split(",")[1]);
// 将top10 session插入MySQL表
Top10Session top10Session = new Top10Session();
top10Session.setTaskid(taskid);
top10Session.setCategoryid(categoryid);
top10Session.setSessionid(sessionid);
top10Session.setClickCount(count);
ITop10SessionDAO top10SessionDAO = DAOFactory.getTop10SessionDAO();
top10SessionDAO.insert(top10Session);
// 放入list
list.add(new Tuple2<String, String>(sessionid, sessionid));
}
return list;
}
});
/**
* 第四步:获取top10活跃session的明细数据,并写入MySQL
*/
JavaPairRDD<String, Tuple2<String, Row>> sessionDetailRDD =
top10SessionRDD.join(sessionid2detailRDD);
sessionDetailRDD.foreach(new VoidFunction<Tuple2<String,Tuple2<String,Row>>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Tuple2<String, Tuple2<String, Row>> tuple) throws Exception {
Row row = tuple._2._2;
SessionDetail sessionDetail = new SessionDetail();
sessionDetail.setTaskid(taskid);
sessionDetail.setUserid(row.getLong(1));
sessionDetail.setSessionid(row.getString(2));
sessionDetail.setPageid(row.getLong(3));
sessionDetail.setActionTime(row.getString(4));
sessionDetail.setSearchKeyword(row.getString(5));
sessionDetail.setClickCategoryId(row.getLong(6));
sessionDetail.setClickProductId(row.getLong(7));
sessionDetail.setOrderCategoryIds(row.getString(8));
sessionDetail.setOrderProductIds(row.getString(9));
sessionDetail.setPayCategoryIds(row.getString(10));
sessionDetail.setPayProductIds(row.getString(11));
ISessionDetailDAO sessionDetailDAO = DAOFactory.getSessionDetailDAO();
sessionDetailDAO.insert(sessionDetail);
}
});
}
}
其他辅助类
cn.ctgu.sparkproject.conf.ConfigurationManager
package cn.ctgu.sparkproject.conf;
import java.io.InputStream;
import java.util.Properties;
/*
* 配置管理组件
* 1、配置管理组件可以复杂,也可以很简单,对于简单的配置管理组件来说,只要开发一个类,可以在第一次访问它的时候
* 就从对应的properties文件中,读取配置项,读取配置项,并提供外界获取某个配置key对应的value的方法
* 2、如果是特别复杂的配置管理组件,那么可能需要使用一些软件设计中的设计模式,比如单例模式、解释器模式
* 可能需要管理多个不同的properties,甚至是xml类型的配置文件
* 3、我们这里开发一个简单的配置管理组件就可以了
*
* */
public class ConfigurationManager {
//properties对象使用private来修饰,就代表了其实是类私有的
//那么外界的代码就不能直接通过ConfigurationManager.prop这种方式获取到Properties对象
//之所以这么做,是为了避免外界的代码不小心错误的更新了Properties中某个key对应的value
//从而导致整个程序的状态错误,乃至崩溃
private static Properties prop=new Properties();
static {
try {
InputStream in=ConfigurationManager.class
.getClassLoader().getResourceAsStream("my.properties");
prop.load(in);
}catch(Exception e){
e.printStackTrace();
}
}
public static String getProperty(String key) {
return prop.getProperty(key);
}
/*
* 获取整数型的配置项
*
* */
public static Integer getInteger(String key) {
String value=getProperty(key);
try {
return Integer.valueOf(value);
}catch(Exception e) {
e.printStackTrace();
}
return 0;
}
/*
* 获取布尔类型的配置项
*
* */
public static Boolean getBoolean(String key) {
String value=getProperty(key);
try {
}catch(Exception e) {
e.printStackTrace();
}
return false;
}
}
cn.ctgu.sparkproject.constant.Constants
package cn.ctgu.sparkproject.constant;
//常量接口
public interface Constants {
/*
* 项目配置相关的常量
* */
String JDBC_DRIVER="jdbc.driver";
String JDBC_DATASOURCE_SIZE="jdbc.datasource.size";
String JDBC_URL="jdbc.url";
String JDBC_USER="jdbc.user";
String JDBC_PASSWORD="jdbc.password";
String SPARK_LOCAL="spark.local";
/*
* spark作业相关的常量
* */
String SPARK_APP_NAME_SESSION="";
String FIELD_SESSION_ID="sessionid";
String FIELD_SEARCH_KEYWORDS="searchKeywords";
String FIELD_CLICK_CATEGORY_IDS="clickCategoryIds";
String FIELD_AGE="age";
String FIELD_PROFESSIONAL="professional";
String FIELD_CITY="city";
String FIELD_SEX="sex";
String FIELD_VISIT_LENGTH="visitLength";
String FIELD_STEP_LENGTH="stepLength";
String FIELD_START_TIME="startTime";
String FIELD_CLICK_COUNT="clickCount";
String FIELD_ORDER_COUNT="orderCount";
String FIELD_PAY_COUNT="payCount";
String FIELD_CATEGORY_ID="categoryid";
String SESSION_COUNT = "session_count";
String TIME_PERIOD_1s_3s = "1s_3s";
String TIME_PERIOD_4s_6s = "4s_6s";
String TIME_PERIOD_7s_9s = "7s_9s";
String TIME_PERIOD_10s_30s = "10s_30s";
String TIME_PERIOD_30s_60s = "30s_60s";
String TIME_PERIOD_1m_3m = "1m_3m";
String TIME_PERIOD_3m_10m = "3m_10m";
String TIME_PERIOD_10m_30m = "10m_30m";
String TIME_PERIOD_30m = "30m";
String STEP_PERIOD_1_3 = "1_3";
String STEP_PERIOD_4_6 = "4_6";
String STEP_PERIOD_7_9 = "7_9";
String STEP_PERIOD_10_30 = "10_30";
String STEP_PERIOD_30_60 = "30_60";
String STEP_PERIOD_60 = "60";
/*
* 任务相关的常量
* */
String PARAM_START_DATE="startDate";
String PARAM_END_DATE="endDate";
String PARAM_START_AGE="startAge";
String PARAM_END_AGE="endAge";
String PARAM_PROFESSIONALS="professionals";
String PARAM_CITIES="cities";
String PARAM_SEX="sex";
String PARAM_KEYWORDS="keywords";
String PARAM_CATEGORY_IDS="categoryIds";
}
cn.ctgu.sparkproject.dao.ISessionAggrStatDAO
package cn.ctgu.sparkproject.dao;
import cn.ctgu.sparkproject.domain.SessionAggrStat;
/*
*
*
* */
public interface ISessionAggrStatDAO {
/*
* 插入session聚合统计结果
* */
void insert(SessionAggrStat sessionAggrStat);
}
cn.ctgu.sparkproject.dao.ISessionDetailDAO
package cn.ctgu.sparkproject.dao;
import cn.ctgu.sparkproject.domain.SessionDetail;
/*
* session明细DAO接口
*
* */
public interface ISessionDetailDAO {
/*
* 插入一条session明细
*
* */
public void insert(SessionDetail sessionDetail);
}
cn.ctgu.sparkproject.dao.ISessionRandomExtractDAO
package cn.ctgu.sparkproject.dao;
import cn.ctgu.sparkproject.domain.SessionAggrStat;
import cn.ctgu.sparkproject.domain.SessionRandomExtract;
/*
* session随机抽取模块接口
*
* */
public interface ISessionRandomExtractDAO {
/*
* 插入session随机抽取
* */
void insert(SessionRandomExtract sessionRandomExtract);
}
cn.ctgu.sparkproject.dao.ITaskDAO
package cn.ctgu.sparkproject.dao;
import cn.ctgu.sparkproject.domain.Task;
/*
* 任务管理DAO接口
*
* */
public interface ITaskDAO {
Task findById(long taskid);
}
cn.ctgu.sparkproject.dao.ITop10CategoryDAO
package cn.ctgu.sparkproject.dao;
import cn.ctgu.sparkproject.domain.Top10Category;
/*
* top10品类DAO接口
* */
public interface ITop10CategoryDAO {
void insert(Top10Category category);
}
cn.ctgu.sparkproject.dao.ITop10SessionDAO
package cn.ctgu.sparkproject.dao;
import cn.ctgu.sparkproject.domain.Top10Session;
/*
* top10活跃session的DAO接口
*
* */
public interface ITop10SessionDAO {
void insert(Top10Session top10Session);
}
cn.ctgu.sparkproject.dao.factory.DAOFactory
package cn.ctgu.sparkproject.dao.factory;
import cn.ctgu.sparkproject.dao.ISessionAggrStatDAO;
import cn.ctgu.sparkproject.dao.ISessionDetailDAO;
import cn.ctgu.sparkproject.dao.ISessionRandomExtractDAO;
import cn.ctgu.sparkproject.dao.ITaskDAO;
import cn.ctgu.sparkproject.dao.ITop10CategoryDAO;
import cn.ctgu.sparkproject.dao.ITop10SessionDAO;
import cn.ctgu.sparkproject.dao.impl.SessionAggrStatDAOImpl;
import cn.ctgu.sparkproject.dao.impl.SessionDetailDAOImpl;
import cn.ctgu.sparkproject.dao.impl.SessionRandomExtractDAOImpl;
import cn.ctgu.sparkproject.dao.impl.TaskDAOImpl;
import cn.ctgu.sparkproject.dao.impl.Top10CategoryDAOImpl;
import cn.ctgu.sparkproject.dao.impl.Top10SessionDAOImpl;
/*
* DAO工厂类
*
* */
public class DAOFactory {
/*
* 获取任务管理器DAO
*
* */
public static ITaskDAO getTaskDAO() {
return new TaskDAOImpl();
}
/*
* 获取统计sessionDAO
*
* */
public static ISessionAggrStatDAO getSessionAggrStatDAO() {
return new SessionAggrStatDAOImpl();
}
public static ISessionRandomExtractDAO getSessionRandomExtractDAO() {
return new SessionRandomExtractDAOImpl();
}
public static ISessionDetailDAO getSessionDetailDAO() {
return new SessionDetailDAOImpl();
}
public static ITop10CategoryDAO getTop10CategoryDAO() {
return new Top10CategoryDAOImpl();
}
public static ITop10SessionDAO getTop10SessionDAO() {
return new Top10SessionDAOImpl();
}
}
cn.ctgu.sparkproject.jdbc.JDBCHelper
package cn.ctgu.sparkproject.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.LinkedList;
import java.util.List;
import cn.ctgu.sparkproject.conf.ConfigurationManager;
import cn.ctgu.sparkproject.constant.Constants;
/*
* JDBC辅助组件
*
*
*
* */
public class JDBCHelper {
/*
* 第一步:在静态代码块中,直接加载数据库的驱动
* 加载驱动,不是直接简单的,使用com.mysql.jdbc.Driver就可以了(属于硬编码)
*
*com.mysql.jdbc.Driver只代表了mysql数据的驱动
*那么,如果有一天,项目的底层的数据要进行迁移,比如迁移到SQLServer、DB2
*那么,就必须很费经的在代码中,找到硬编码了的代码并进行修改成其他数据库的驱动类的类名
*
*所以正规项目是不允许硬编码的存在
*
*通常都是使用以一个常量接口中的某个常量来代表一个值
*然后在这个值改变的时候,只要改变常量接口中的变量
*
*项目要尽量做成可配置的,就是说,我们这个数据库驱动,更进一步,也不只是放在常量接口中就可以了
*最后的方式,是放在外部的配置文件中,跟代码彻底分离
*常量接口中只是包含了这个值对应的key的名字
* */
static {
try {
String driver=ConfigurationManager.getProperty(Constants.JDBC_DRIVER);
Class.forName(driver);
}catch(Exception e) {
e.printStackTrace();
}
}
/*
* 第二步,实现JDBCHelper的单例化
* 为什么要实现单例化呢?因为它的内部要封装一个简单的内部的数据连接池
* 为了保证数据库连接池有且仅有一份,所以就通过单例的方式
* 保证JDBCHelper只有一个实例,实例中只有一根数据连接池
* */
private static JDBCHelper instance=null;
/*
* 获取单例
*
* JDBCHelper在整个程序声明周期中,只会创建一次实例
* 在这一次创建实例的过程中,就会调用JDBCHelper()构造方法
* 此时,就可以在构造方法中,去创建自己唯一的数据库连接池
*
* */
public static JDBCHelper getInstance() {
if(instance==null) {
synchronized(JDBCHelper.class){
if(instance==null) {
instance=new JDBCHelper();
}
}
}
return instance;
}
private LinkedList<Connection>datasource=new LinkedList<Connection>();
/*
* 第三步:实现单例的过程中,实现唯一的数据库连接池
* 私有化构造函数
* */
private JDBCHelper() {
//第一步,获取数据库连接池的大小,就是说,数据库连接池中要放多少个数据库连接
//这个,可以通过在配置文件中配置的方式,来灵活的设定
int datasourceSize=ConfigurationManager.getInteger(
Constants.JDBC_DATASOURCE_SIZE);
//然后创建指定数量的数据库连接,并放入数据库连接池中
for(int i=0;i<datasourceSize;i++) {
String url=ConfigurationManager.getProperty(Constants.JDBC_URL);
String user=ConfigurationManager.getProperty(Constants.JDBC_USER);
String password=ConfigurationManager.getProperty(Constants.JDBC_PASSWORD);
try {
Connection conn=DriverManager.getConnection(url, user, password);
datasource.push(conn);
}catch(Exception e) {
e.printStackTrace();
}
}
}
/*
* 第四步:提供获取数据库连接的方法
* 有可能,你去获取的时候,这个时候连接都被用光了,你暂时获取不到数据库连接
* 所以我们要自己编码实现一个简单的等待机制,去等待获取到数据库连接
*
* 为了防止数据库连接池用完了,其他线程都来判断都来拿数据库连接导致的代码重复判断
* 所以加了一个线程同步只有第一个线程拿到了数据库连接,其他线程才会来判断
* */
public synchronized Connection getConnection() {
while(datasource.size()==0) {
try {
Thread.sleep(10);
}catch(Exception e) {
e.printStackTrace();
}
}
return datasource.poll();
}
/*
* 第五步:开发增删改查的方法
* 1、执行增删改查SQL语句的方法
* 2、执行查询SQL语句的方法
* 3、批量执行SQL语句的方法
*
* */
public int executeUpdate(String sql,Object[]params) {
int rtn=0;
Connection conn=null;
PreparedStatement pstmt=null;
try {
conn=getConnection();
pstmt=conn.prepareStatement(sql);
for(int i=0;i<params.length;i++) {
pstmt.setObject(i+1, params[i]);
}
rtn=pstmt.executeUpdate();
}catch(Exception e) {
e.printStackTrace();
}finally{
if(conn!=null) {
datasource.push(conn);
}
}
return rtn;
}
/*
* 执行查询sql语句
*
* */
public void executeQuery(String sql,Object[]params,
QueryCallback callback) {
Connection conn=null;
PreparedStatement pstmt=null;
ResultSet rs=null;
try {
conn=getConnection();
pstmt=conn.prepareStatement(sql);
for(int i=0;i<params.length;i++) {
pstmt.setObject(i+1, params[i]);
}
rs=pstmt.executeQuery();
callback.process(rs);
}catch(Exception e) {
e.printStackTrace();
}
}
/*
* 批量执行sql语句
* 批量执行SQL语句是JDBC中的一个高级功能
* 默认情况下,每次执行一条SQL语句就会通过网络连接,向MySQL发送一次请求
*
* 但是,如果在短时间内要执行多条结构完全一模一样的SQL,只是参数不同
* 虽然使用PrepareStatement这种方式,可以只编译一次SQL,提高性能,但是,还是对于每次SQL
* 都要向MySQL发送一次网络请求
*
* 可以通过批量执行SQL语句的功能优化这个性能
* 一次性通过PreparedStatement发送多条SQL语句,比如100条、1000条甚至是上万条
* 执行的时候,也仅仅编译一次就可以
* 这种批量执行sql语句的方式,可以大大提升性能
*
* */
public int[]executeBatch(String sql,List<Object[]>paramsList){
int[]rtn=null;
Connection conn=null;
PreparedStatement pstmt=null;
try {
conn=getConnection();
//第一步:使用Connection对象取消自动提交
conn.setAutoCommit(false);
pstmt=conn.prepareStatement(sql);
//第二步:使用PrepareStatement.addBatch()方法加入批量的SQL参数
for(Object[]params:paramsList) {
for(int i=0;i<params.length;i++) {
pstmt.setObject(i+1, params[i]);
}
pstmt.addBatch();
}
//第三步:使用PreparedStatement.executeBatch()方法执行批量的SQL语句
rtn=pstmt.executeBatch();
//最后一步:使用Connection对象,提交批量的SQL语句
conn.commit();
}catch(Exception e) {
e.printStackTrace();
}
return rtn;
}
/*
* 内部类,查询回调接口
*
* */
public static interface QueryCallback{
void process(ResultSet rs) throws Exception;
/*
* 处理查询结果
* */
}
}
cn.ctgu.sparkproject.dao.impl.SessionAggrStatDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import cn.ctgu.sparkproject.dao.ISessionAggrStatDAO;
import cn.ctgu.sparkproject.domain.SessionAggrStat;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/*
* session聚合统计DAO实现类
*
* */
public class SessionAggrStatDAOImpl implements ISessionAggrStatDAO{
/*
* 插入session聚合统计结果
*
* */
public void insert(SessionAggrStat sessionAggrStat) {
String sql="insert into session_aggr_stat values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
Object[]params=new Object[] {sessionAggrStat.getTaskid(),
sessionAggrStat.getVisit_length_1s_3s_ratio(),
sessionAggrStat.getVisit_length_4s_6s_ratio(),
sessionAggrStat.getVisit_length_7s_9s_ratio(),
sessionAggrStat.getVisit_length_10s_30s_ratio(),
sessionAggrStat.getVisit_length_1m_3m_ratio(),
sessionAggrStat.getVisit_length_3m_10m_ratio(),
sessionAggrStat.getVisit_length_10m_30m_ratio(),
sessionAggrStat.getVisit_length_30m_ratio(),
sessionAggrStat.getStep_length_1_3_ratio(),
sessionAggrStat.getStep_length_4_6_ratio(),
sessionAggrStat.getStep_length_7_9_ratio(),
sessionAggrStat.getStep_length_10_30_ratio(),
sessionAggrStat.getStep_length_30_60_ratio(),
sessionAggrStat.getStep_length_60_ratio()};
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeUpdate(sql, params);
}
}
cn.ctgu.sparkproject.dao.impl.SessionDetailDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import cn.ctgu.sparkproject.dao.ISessionDetailDAO;
import cn.ctgu.sparkproject.domain.SessionDetail;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
public class SessionDetailDAOImpl implements ISessionDetailDAO{
@Override
public void insert(SessionDetail sessionDetail) {
String sql="insert into session_detail values(?,?,?,?,?,?,?,?,?,?,?,?)";
Object[] params=new Object[] {sessionDetail.getTaskid(),
sessionDetail.getUserid(),
sessionDetail.getSessionid(),
sessionDetail.getPageid(),
sessionDetail.getActionTime(),
sessionDetail.getSearchKeyWord(),
sessionDetail.getClickCategoryId(),
sessionDetail.getClickProductId(),
sessionDetail.getOrderCategoryIds(),
sessionDetail.getOrderProductIds(),
sessionDetail.getPayCategoryIds(),
sessionDetail.getPayProductIds()};
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeUpdate(sql, params);
}
}
cn.ctgu.sparkproject.dao.impl.SessionRandomExtractDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import cn.ctgu.sparkproject.dao.ISessionRandomExtractDAO;
import cn.ctgu.sparkproject.domain.SessionRandomExtract;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/*
* 随机抽取session的DAO实现
*
* */
public class SessionRandomExtractDAOImpl implements ISessionRandomExtractDAO{
@Override
public void insert(SessionRandomExtract sessionRandomExtract) {
String sql="insert into session_random_extract values(?,?,?,?,?)";
Object[]params=new Object[] {sessionRandomExtract.getTaskid(),
sessionRandomExtract.getSessionid(),
sessionRandomExtract.getStartTime(),
sessionRandomExtract.getSearchKeywords(),
sessionRandomExtract.getClickCategoryIds()};
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeUpdate(sql, params);
}
}
cn.ctgu.sparkproject.dao.impl.TaskDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import java.sql.ResultSet;
import cn.ctgu.sparkproject.dao.ITaskDAO;
import cn.ctgu.sparkproject.domain.Task;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/*
* 任务管理实现DAO实现类
*
* */
public class TaskDAOImpl implements ITaskDAO{
/*
* 根据逐渐查询任务
*
* */
public Task findById(long taskid) {
final Task task=new Task();
String sql="select * from task where task_id=?";
Object[] params=new Object[] {taskid};
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeQuery(sql, params, new JDBCHelper.QueryCallback() {
@Override
public void process(ResultSet rs) throws Exception {
if(rs.next()) {
long taskid=rs.getLong(1);
String taskName=rs.getString(2);
String createTime=rs.getString(3);
String startTime=rs.getString(4);
String finishTime=rs.getString(5);
String taskType=rs.getString(6);
String taskStatus=rs.getString(7);
String taskParam=rs.getString(8);
task.setTaskid(taskid);
task.setCreateTime(createTime);
task.setFinishTime(finishTime);
task.setTaskName(taskName);
task.setTaskStatus(taskStatus);
task.setTaskParam(taskParam);
task.setStartTime(startTime);
task.setTaskType(taskType);
}
}
});
/*
* 用JDBC进行数据库操作最大的问题就是为了查询某些数据,需要自己编写大量的domain对象的封装
* 数据的获取,数据的设置造成大量冗余的代码
*
* 所以不建议用scala来开发大型复杂的spark的工程项目
* 因为大型复杂的工程项目,必定要涉及很多第三方的东西的,mysql只是最基础的,要进行数据库操作
* 可能还会有其他的redis、zookeeper等等
*
* 如果就用scala,那么势必会造成与调用第三方组件的代码,用java就会变成scala+java混编
* 大大降低我们的开发和维护的效率
*
* 此外,即使,你是用了scala+java混编
* 但是,真正最方便的,还是使用一些j2ee的开源框架来进行第三方技术的整合和操作
* 比如mysql可以用mybatis/hibernate,大大减少我们冗余的代码
* 大大提升我们的开发速度和效率
*
* 但是如果用了scala,那么用j2ee开源框架就会造成scala+java+j2ee开源框架的极度混乱
* 后期难以维护和交接
* */
return task;
}
}
cn.ctgu.sparkproject.dao.impl.Top10CategoryDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import cn.ctgu.sparkproject.dao.ITop10CategoryDAO;
import cn.ctgu.sparkproject.domain.Top10Category;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/*
* top10品类DAO实现
*
* */
public class Top10CategoryDAOImpl implements ITop10CategoryDAO{
@Override
public void insert(Top10Category category) {
String sql="insert into top10_category values(?,?,?,?,?)";
Object[] params=new Object[] {category.getTaskid(),
category.getCategoryid(),
category.getClickCount(),
category.getOrderCount(),
category.getPayCount()};
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeUpdate(sql, params);
}
}
cn.ctgu.sparkproject.dao.impl.Top10SessionDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import cn.ctgu.sparkproject.dao.ITop10SessionDAO;
import cn.ctgu.sparkproject.domain.Top10Session;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/*
*
* top10活跃session的DAO实现
*
* */
public class Top10SessionDAOImpl implements ITop10SessionDAO{
@Override
public void insert(Top10Session top10Session) {
String sql="insert into top10_session values(?,?,?,?)";
Object[]params= new Object[]{top10Session.getTaskid(),
top10Session.getCategoryid(),
top10Session.getSessionid(),
top10Session.getClickcount()};
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeUpdate(sql, params);
}
}
工具类
cn.ctgu.sparkproject.util.DateUtils
package cn.ctgu.sparkproject.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* 日期时间工具类
* @author Administrator
*
*/
public class DateUtils {
public static final SimpleDateFormat TIME_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd");
/**
* 判断一个时间是否在另一个时间之前
* @param time1 第一个时间
* @param time2 第二个时间
* @return 判断结果
*/
public static boolean before(String time1, String time2) {
try {
Date dateTime1 = TIME_FORMAT.parse(time1);
Date dateTime2 = TIME_FORMAT.parse(time2);
if(dateTime1.before(dateTime2)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 判断一个时间是否在另一个时间之后
* @param time1 第一个时间
* @param time2 第二个时间
* @return 判断结果
*/
public static boolean after(String time1, String time2) {
try {
Date dateTime1 = TIME_FORMAT.parse(time1);
Date dateTime2 = TIME_FORMAT.parse(time2);
if(dateTime1.after(dateTime2)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 计算时间差值(单位为秒)
* @param time1 时间1
* @param time2 时间2
* @return 差值
*/
public static int minus(String time1, String time2) {
try {
Date datetime1 = TIME_FORMAT.parse(time1);
Date datetime2 = TIME_FORMAT.parse(time2);
long millisecond = datetime1.getTime() - datetime2.getTime();
return Integer.valueOf(String.valueOf(millisecond / 1000));
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
/**
* 获取年月日和小时
* @param datetime 时间(yyyy-MM-dd HH:mm:ss)
* @return 结果
*/
public static String getDateHour(String datetime) {
String date = datetime.split(" ")[0];
String hourMinuteSecond = datetime.split(" ")[1];
String hour = hourMinuteSecond.split(":")[0];
return date + "_" + hour;
}
/**
* 获取当天日期(yyyy-MM-dd)
* @return 当天日期
*/
public static String getTodayDate() {
return DATE_FORMAT.format(new Date());
}
/**
* 获取昨天的日期(yyyy-MM-dd)
* @return 昨天的日期
*/
public static String getYesterdayDate() {
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.DAY_OF_YEAR, -1);
Date date = cal.getTime();
return DATE_FORMAT.format(date);
}
/**
* 格式化日期(yyyy-MM-dd)
* @param date Date对象
* @return 格式化后的日期
*/
public static String formatDate(Date date) {
return DATE_FORMAT.format(date);
}
/**
* 格式化时间(yyyy-MM-dd HH:mm:ss)
* @param date Date对象
* @return 格式化后的时间
*/
public static String formatTime(Date date) {
return TIME_FORMAT.format(date);
}
/*
* 解析时间字符串
*
* */
public static Date parseTime(String time) {
try {
return TIME_FORMAT.parse(time);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
cn.ctgu.sparkproject.util.NumberUtils
package cn.ctgu.sparkproject.util;
import java.math.BigDecimal;
/**
* 数字格工具类
* @author Administrator
*
*/
public class NumberUtils {
/**
* 格式化小数
* @param str 字符串
* @param scale 四舍五入的位数
* @return 格式化小数
*/
public static double formatDouble(double num, int scale) {
BigDecimal bd = new BigDecimal(num);
return bd.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
cn.ctgu.sparkproject.util.ParamUtils
package cn.ctgu.sparkproject.util;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
/**
* 参数工具类
* @author Administrator
*
*/
public class ParamUtils {
/**
* 从命令行参数中提取任务id
* @param args 命令行参数
* @return 任务id
*/
public static Long getTaskIdFromArgs(String[] args) {
try {
if(args != null && args.length > 0) {
return Long.valueOf(args[0]);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 从JSON对象中提取参数
* @param jsonObject JSON对象
* @return 参数
*/
public static String getParam(JSONObject jsonObject, String field) {
JSONArray jsonArray = jsonObject.getJSONArray(field);
if(jsonArray != null && jsonArray.size() > 0) {
return jsonArray.getString(0);
}
return null;
}
}
cn.ctgu.sparkproject.util.StringUtils
package cn.ctgu.sparkproject.util;
/**
* 字符串工具类
* @author Administrator
*
*/
public class StringUtils {
/**
* 判断字符串是否为空
* @param str 字符串
* @return 是否为空
*/
public static boolean isEmpty(String str) {
return str == null || "".equals(str);
}
/**
* 判断字符串是否不为空
* @param str 字符串
* @return 是否不为空
*/
public static boolean isNotEmpty(String str) {
return str != null && !"".equals(str);
}
/**
* 截断字符串两侧的逗号
* @param str 字符串
* @return 字符串
*/
public static String trimComma(String str) {
if(str.startsWith(",")) {
str = str.substring(1);
}
if(str.endsWith(",")) {
str = str.substring(0, str.length() - 1);
}
return str;
}
/**
* 补全两位数字
* @param str
* @return
*/
public static String fulfuill(String str) {
if(str.length() == 2) {
return str;
} else {
return "0" + str;
}
}
/**
* 从拼接的字符串中提取字段
* @param str 字符串
* @param delimiter 分隔符
* @param field 字段
* @return 字段值
*/
public static String getFieldFromConcatString(String str,
String delimiter, String field) {
String[] fields = str.split(delimiter);
for(String concatField : fields) {
if(concatField.split("=").length==2) {
String fieldName = concatField.split("=")[0];
String fieldValue = concatField.split("=")[1];
if(fieldName.equals(field)) {
return fieldValue;
}
}
}
return null;
}
/**
* 从拼接的字符串中给字段设置值
* @param str 字符串
* @param delimiter 分隔符
* @param field 字段名
* @param newFieldValue 新的field值
* @return 字段值
*/
public static String setFieldInConcatString(String str,
String delimiter, String field, String newFieldValue) {
String[] fields = str.split(delimiter);
for(int i = 0; i < fields.length; i++) {
String fieldName = fields[i].split("=")[0];
if(fieldName.equals(field)) {
String concatField = fieldName + "=" + newFieldValue;
fields[i] = concatField;
break;
}
}
StringBuffer buffer = new StringBuffer("");
for(int i = 0; i < fields.length; i++) {
buffer.append(fields[i]);
if(i < fields.length - 1) {
buffer.append("|");
}
}
return buffer.toString();
}
}
cn.ctgu.sparkproject.util.ValidUtils
package cn.ctgu.sparkproject.util;
/**
* 校验工具类
* @author Administrator
*
*/
public class ValidUtils {
/**
* 校验数据中的指定字段,是否在指定范围内
* @param data 数据
* @param dataField 数据字段
* @param parameter 参数
* @param startParamField 起始参数字段
* @param endParamField 结束参数字段
* @return 校验结果
*/
public static boolean between(String data, String dataField,
String parameter, String startParamField, String endParamField) {
String startParamFieldStr = StringUtils.getFieldFromConcatString(
parameter, "\\|", startParamField);
String endParamFieldStr = StringUtils.getFieldFromConcatString(
parameter, "\\|", endParamField);
if(startParamFieldStr == null || endParamFieldStr == null) {
return true;
}
int startParamFieldValue = Integer.valueOf(startParamFieldStr);
int endParamFieldValue = Integer.valueOf(endParamFieldStr);
String dataFieldStr = StringUtils.getFieldFromConcatString(
data, "\\|", dataField);
if(dataFieldStr != null) {
int dataFieldValue = Integer.valueOf(dataFieldStr);
if(dataFieldValue >= startParamFieldValue &&
dataFieldValue <= endParamFieldValue) {
return true;
} else {
return false;
}
}
return false;
}
/**
* 校验数据中的指定字段,是否有值与参数字段的值相同
* @param data 数据
* @param dataField 数据字段
* @param parameter 参数
* @param paramField 参数字段
* @return 校验结果
*/
public static boolean in(String data, String dataField,
String parameter, String paramField) {
String paramFieldValue = StringUtils.getFieldFromConcatString(
parameter, "\\|", paramField);
if(paramFieldValue == null) {
return true;
}
String[] paramFieldValueSplited = paramFieldValue.split(",");
String dataFieldValue = StringUtils.getFieldFromConcatString(
data, "\\|", dataField);
if(dataFieldValue != null) {
String[] dataFieldValueSplited = dataFieldValue.split(",");
for(String singleDataFieldValue : dataFieldValueSplited) {
for(String singleParamFieldValue : paramFieldValueSplited) {
if(singleDataFieldValue.equals(singleParamFieldValue)) {
return true;
}
}
}
}
return false;
}
/**
* 校验数据中的指定字段,是否在指定范围内
* @param data 数据
* @param dataField 数据字段
* @param parameter 参数
* @param paramField 参数字段
* @return 校验结果
*/
public static boolean equal(String data, String dataField,
String parameter, String paramField) {
String paramFieldValue = StringUtils.getFieldFromConcatString(
parameter, "\\|", paramField);
if(paramFieldValue == null) {
return true;
}
String dataFieldValue = StringUtils.getFieldFromConcatString(
data, "\\|", dataField);
if(dataFieldValue != null) {
if(dataFieldValue.equals(paramFieldValue)) {
return true;
}
}
return false;
}
}
domian类这里就不赘述,对照MySQL数据库即可。