原文链接https://www.cnblogs.com/stillcoolme/p/10160397.html

1 业务需求

最近做的24小时书店大数据平台中的一个需求:获取一段时间内只在晚上进店,而白天没有进店的顾客。

输入是指定的开始日期、结束日期、夜出开始时间(nightTimeS)、夜出结束时间(nightTimeE)。通过userName可以区分一个顾客。

2 业务实现

2.1 第一版 只统计了晚上出现的顾客

下面代码是最开始的实现,有些问题需要改进:

  1. 只将晚上出现过的客户统计起来,而没考虑该顾客可能白天也出现过,基本不能满足业务需求;
  2. 另外对传入的夜出时间范围的判定也不够严谨。
// 得到<userName_date, count>
JavaPairRDD<String, Integer> pairRdd = df.toJavaRDD().mapToPair(new PairFunction<Row, String, Integer>() {
@Override
public Tuple2<String, Integer> call(Row row) throws Exception {
String userName = row.getString(0);
String[] strAry = Strsplit.splitByWholeSeparator(row.getString(1), " ", -1, true);
if(strAry[1].compareTo(nightTimeS)>0){
return new Tuple2<String, Integer>(String.format("%s,%s",userName, strAry[0]), 1);
} else if(strAry[1].compareTo(nightTimeE)<0){
Date preDate = DateUtil.parse(strAry[0], "yyyy-MM-dd");
String preDate_s = DateUtil.format(DateUtil.addDays(preDate, -1), "yyyy-MM-dd");
return new Tuple2<String, Integer>(String.format("%s,%s",userName, preDate_s), 1);
} else {
return new Tuple2<String, Integer>(String.format("%s,%s",userName, strAry[0]), 0);
}
}
});
// 将userName_date相同的相加
JavaPairRDD<String, Integer> pairRdd2 = pairRdd.reduceByKey(new Function2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer v1, Integer v2) throws Exception {
return v1 + v2;
}
});
// 过滤出现次数小于1的
JavaPairRDD<String, Integer> pairRdd3 = pairRdd2.filter(new Function<Tuple2<String,Integer>, Boolean>() {
@Override
public Boolean call(Tuple2<String, Integer> v1) throws Exception {
return v1._2 > 0;
}
});
// 结果处理
JavaRDD<String> result = pairRdd3.map(new Function<Tuple2<String,Integer>, String>() {
@Override
public String call(Tuple2<String, Integer> tuple) throws Exception {
String[] strAry = Strsplit.splitByWholeSeparator(tuple._1, ",", -1, true);
String dayEndDate = DateUtil.format(DateUtil.addDays(DateUtil.parse(strAry[1], "yyyy-MM-dd"),1), "yyyy-MM-dd");
return String.format("%s,%s,%s,%s", strAry[0],strAry[1],dayEndDate,tuple._2);
}
});

2.2 第二版 对白天进店了的顾客形成列表然后用于后续过滤

该版实现做的工作:

  1. 增加了对夜出范围更完整的逻辑判断。
  2. 使用一个List来保存白天出现过的顾客,然后再通过这个List来把结果中在白天出现过且晚上有出现的顾客过滤掉。

经过线上测试,该版本性能极低。因为List中会存储所有白天出现的顾客导致过滤过程缓慢。由于分布式的原因,List还无法使用移除里面的null值。

// 获取白天出现过的顾客列表
final JavaRDD<String> rdd = df.toJavaRDD().map(new Function<Row, String>() {
@Override
public String call(Row row) throws Exception {
String[] strAry = Strsplit.splitByWholeSeparator(row.getString(1), " ", -1, true);
// 下面第一重if是判断夜晚出现的范围, 第二重if是判断顾客在不在范围内
// nightTimeS < nightTimeE
if(nightTimeS.compareTo(nightTimeE) < 0){
if(strAry[1].compareTo(nightTimeS) > 0 && strAry[1].compareTo(nightTimeE) < 0){
return "0";
}else{
return row.getString(0);
}
}else{ // nightTimeS > nightTimeE (包括三种情况:1,22:00 - 00:00 2,00:00 - 02:00 3. 22:00 - 02:00 其实也可以归为一种情况,前两种都是第三种的特例)
if(strAry[1].compareTo(nightTimeS)> 0 || strAry[1].compareTo(nightTimeE) < 0){
return "0";
} else {
return row.getString(0);
}
}
}
});
final List<String> userNameList = rdd.toArray(); JavaPairRDD<String, Integer> pairRddbefore = df.toJavaRDD().mapToPair(new PairFunction<Row, String, Integer>() {
@Override
public Tuple2<String, Integer> call(Row row) throws Exception {
//row : user1 2018-10-11 21:11:11 然后将时间切分成: strAry[0] 2018-10-11 strAry[1] 21:11:11
String userName = row.getString(0);
String[] strAry = Strsplit.splitByWholeSeparator(row.getString(1), " ", -1, true); if(nightTimeS.compareTo(nightTimeE) < 0){
if(strAry[1].compareTo(nightTimeS) > 0 && strAry[1].compareTo(nightTimeE) < 0){
// 区分在 00:00:00前 与 00:00:00后的
// 00:00:00前
if(strAry[1].compareTo("24:00:00") < 0 && strAry[1].compareTo("12:00:00") > 0){
return new Tuple2<String, Integer>(String.format("%s,%s",userName,strAry[0]), 1);
}else{ // 00:00:00 后
Date preDate = DateUtil.parse(strAry[0], "yyyy-MM-dd");
String preDate_s = DateUtil.format(DateUtil.addDays(preDate, -1), "yyyy-MM-dd");
return new Tuple2<String, Integer>(String.format("%s,%s",userName, preDate_s), 1);
}
}else{
return new Tuple2<String, Integer>(String.format("%s,%s", userName, strAry[0]), 0);
}
}else{ // nightTimeS > nightTimeE
if(strAry[1].compareTo(nightTimeS) > 0){
return new Tuple2<String, Integer>(String.format("%s,%s", userName, strAry[0]), 1);
} else if(strAry[1].compareTo(nightTimeE) < 0){
Date preDate = DateUtil.parse(strAry[0], "yyyy-MM-dd");
String preDate_s = DateUtil.format(DateUtil.addDays(preDate, -1), "yyyy-MM-dd");
return new Tuple2<String, Integer>(String.format("%s,%s", userName, preDate_s), 1);
} else {
return new Tuple2<String, Integer>(String.format("%s,%s", userName, strAry[0]), 0);
}
}
}
});
// 执行过滤,pairRddbefore包含全部夜晚出现过的顾客,
JavaPairRDD<String, Integer> pairRdd = pairRddbefore.filter(new Function<Tuple2<String, Integer>, Boolean>(){
@Override
public Boolean call(Tuple2<String, Integer> tuple) throws Exception {
String userName = tuple._1.split(",")[0];
return !userNameList.contains(userName);
}
});

2.3 第三版 通过求子集过滤掉白天出现过的所有顾客

由于第二版的实现中的List太过冗余,在Spark官网的Tuning Data Structures中就明确说过集合类型和包装类型的java对象占用了太多的额外空间,会降低执行效率,强烈不建议使用,并提出以下建议:

  1. 设计数据结构成数组类型和原始类型,而不是标准的Java或者Sacala集合
  2. 避免在数据结构里面嵌套大量的小对象。
  3. 考虑使用数值型的id或者enumeration对象而不是String类型的key,由于String类型会占用额外的字节。
  4. 如果使用小于32G的内存,可以设置JVM参数:-XX:+UseCompressedOops 使指针占用4bytes而不是8bytes,可以加这些配置在spark-env.sh文件中。

所以第三版的实现在这问题上做了改进。

通过subtractByKey算子过滤掉白天出现过的所有顾客。然后进行后续的处理中,计算的就都是在夜出范围内且白天没出现过的顾客了。

// 获取白天出现过的顾客列表
JavaPairRDD<String, String> rdd = df.toJavaRDD().mapToPair(new PairFunction<Row, String, String>() {
@Override
public Tuple2<String, String> call(Row row) throws Exception {
String[] strAry = Strsplit.splitByWholeSeparator(row.getString(1), " ", -1, true);
// 下面第一重if是判断 昼伏夜出的范围, 第二重if是判断车辆在不在范围内
// nightTimeS < nightTimeE
if(nightTimeS.compareTo(nightTimeE) < 0){
if(strAry[1].compareTo(nightTimeS) > 0 && strAry[1].compareTo(nightTimeE) < 0){
return new Tuple2<>("0", "0");
}else{
return new Tuple2<>(row.getString(0), row.getString(1));
}
}else{ // nightTimeS > nightTimeE (包括三种情况:1,22:00 - 00:00 2,00:00 - 02:00 3. 22:00 - 02:00 其实也可以归为一种情况,前两种都是第三种的特例)
if(strAry[1].compareTo(nightTimeS)> 0 || strAry[1].compareTo(nightTimeE) < 0){
return new Tuple2<>("0", "0");
} else {
return new Tuple2<>(row.getString(0), row.getString(1));
}
}
}
}); // 获取全部顾客列表
JavaPairRDD<String, String> rdd2 = df.toJavaRDD().mapToPair(new PairFunction<Row, String, String>() {
@Override
public Tuple2<String, String> call(Row row) throws Exception {
return new Tuple2<>(row.getString(0), row.getString(1));
}
}); // 做差集,获得只在所求的夜出时间段内的顾客名及时间 <userName, capDate>
JavaPairRDD<String, String> rdd3 = rdd2.subtractByKey(rdd); JavaPairRDD<String, Integer> pairRdd = rdd3.mapToPair(new PairFunction<Tuple2<String, String>, String, Integer>() {
//与版本二的实现相同
});

2.4 第四版 在mapTopair的过程中直接赋极小值

第三版实现性能得到了较大的提高,但是还不够好,因为其执行过程使用的转换过多。

下面的第四版实现为了减少转换的使用,就恢复到第一版的实现中。先对夜出时间的逻辑判断增强;另外,由于想到第一版的实现中最后的filter算子中需要value值为大于0才算夜出顾客,所以在mapToPair的过程中,每当得到白天出现的顾客A时就将它的value值设为一个较小的值,在后面的reduceByKey算子的执行过程中将晚上又再次出现的A的value值给抹平。那么最后的filter算子就能将白天出现过的顾客去除掉了!

下面就是具体实现,只是将版本一中的

return new Tuple2<String, Integer>(String.format("%s,%s",userName, strAry[0]), 0);

修改成

return new Tuple2<String, Integer>(String.format("%s,%s",userName,strAry[0]), -10000);

这样实现有些取巧,但是改动比较小,在性能上表现也可以在10秒内。

JavaPairRDD<String, Integer> pairRdd = df.toJavaRDD().mapToPair(new PairFunction<Row, String, Integer>() {
@Override
public Tuple2<String, Integer> call(Row row) throws Exception {
String userName = row.getString(0);
String[] strAry = Strsplit.splitByWholeSeparator(row.getString(1), " ", -1, true);
if(nightTimeS.compareTo(nightTimeE) < 0){
if(strAry[1].compareTo(nightTimeS) > 0 && strAry[1].compareTo(nightTimeE) < 0){
if(strAry[1].compareTo("24:00:00") < 0 && strAry[1].compareTo("12:00:00") > 0){
return new Tuple2<String, Integer>(String.format("%s,%s",userName,strAry[0]), 1);
}else{
Date preDate = DateUtil.parse(strAry[0], "yyyy-MM-dd");
String preDate_s = DateUtil.format(DateUtil.addDays(preDate, -1), "yyyy-MM-dd");
return new Tuple2<String, Integer>(String.format("%s,%s",userName,preDate_s), 1);
}
}else{
// 白天出现的顾客的value设置为-10000
return new Tuple2<String, Integer>(String.format("%s,%s",userName,strAry[0]), -10000);
}
}else{
if(strAry[1].compareTo(nightTimeS) > 0){
return new Tuple2<String, Integer>(String.format("%s,%s",userName,strAry[0]), 1);
} else if(strAry[1].compareTo(nightTimeE) < 0){
Date preDate = DateUtil.parse(strAry[0], "yyyy-MM-dd");
String preDate_s = DateUtil.format(DateUtil.addDays(preDate, -1), "yyyy-MM-dd");
return new Tuple2<String, Integer>(String.format("%s,%s",userName,preDate_s), 1);
} else {
// 白天出现的顾客的value设置为-10000
return new Tuple2<String, Integer>(String.format("%s,%s",userName,strAry[0]), -10000);
}
}
}
});

3 总结

通过多版本的Spark分析服务的改进,发现对Spark基础算子的正确使用至关重要。选择了合适的算子再配以合适的实现逻辑才能得到性能不错的Spark作业。

4 相关文章

基于spark的车辆分析

Spark实践 -- 夜出顾客服务分析的更多相关文章

  1. 全链路实践Spring Cloud 微服务架构

    Spring Cloud 微服务架构全链路实践Spring Cloud 微服务架构全链路实践 阅读目录: 网关请求流程 Eureka 服务治理 Config 配置中心 Hystrix 监控 服务调用链 ...

  2. TCP\IP协议实践:wireshark抓包分析之链路层与网络层

    目录 TCP\IP协议实践:wireshark抓包分析之链路层与网络层 从ping开始 链路层之以太网封装 ip首部 开启ping程序,开始抓包 由一个ping的结果引出来的两个协议ARP ICMP ...

  3. 个推 Spark实践教你绕过开发那些“坑”

    Spark作为一个开源数据处理框架,它在数据计算过程中把中间数据直接缓存到内存里,能大大提高处理速度,特别是复杂的迭代计算.Spark主要包括SparkSQL,SparkStreaming,Spark ...

  4. spark的存储系统--BlockManager源码分析

    spark的存储系统--BlockManager源码分析 根据之前的一系列分析,我们对spark作业从创建到调度分发,到执行,最后结果回传driver的过程有了一个大概的了解.但是在分析源码的过程中也 ...

  5. Spark Straming,Spark Streaming与Storm的对比分析

    Spark Straming,Spark Streaming与Storm的对比分析 一.大数据实时计算介绍 二.大数据实时计算原理 三.Spark Streaming简介 3.1 SparkStrea ...

  6. 系统设计实践(03)- Instagram社交服务

    前言 系统设计实践篇的文章将会根据<系统设计面试的万金油>为前置模板,讲解数十个常见系统的设计思路. 前置阅读: <系统设计面试的万金油> 系统设计实践(01) - 短链服务 ...

  7. 实践2.4 ELF文件格式分析

    实践2.4 ELF文件格式分析 1.ELF文件头 查看/usr/include/elf.h文件: #define EI_NIDENT (16) typedef struct { unsigned ch ...

  8. hadoop之Spark强有力竞争者Flink,Spark与Flink:对比与分析

    hadoop之Spark强有力竞争者Flink,Spark与Flink:对比与分析 Spark是一种快速.通用的计算集群系统,Spark提出的最主要抽象概念是弹性分布式数据集(RDD),它是一个元素集 ...

  9. OpenStack实践系列⑨云硬盘服务Cinder

    OpenStack实践系列⑨云硬盘服务Cinder八.cinder8.1存储的三大分类 块存储:硬盘,磁盘阵列DAS,SAN存储 文件存储:nfs,GluserFS,Ceph(PB级分布式文件系统), ...

随机推荐

  1. kvm安装及使用

    ****centos7安装及使用kvm: http://blog.csdn.net/github_27924183/article/details/76914322?locationNum=5& ...

  2. 34. CentOS-6.3安装配置Apache2.2.6

    安装说明 安装环境:CentOS-6.3安装方式:源码编译安装 软件:httpd-2.2.6.tar.gz  | pcre-8.32.tar.gz | apr-1.4.6.tar.gz | apr-u ...

  3. IE6部分兼容问题

    border-style:dotted 点线 IE6不兼容 (除了solid以外,其它都有兼容问题,不完全一样) a IE6 不支持a以外的所有标签伪类,IE6以上版本支持所有标签的hover伪类. ...

  4. cnapckSurround c++builder Region 代码折叠快捷键

    C++Builder代码折叠 cnapckSurround c++builder Region 代码折叠快捷键,可以导入导出,IDE code edit,cnpack menu surround wi ...

  5. J2SE 8的编译

    动态加载(修改)服务.高性动态业务逻辑实现(用脚本或模板引擎实现效率满足不了需求) package compile; import java.io.File; import java.io.IOExc ...

  6. Spring boot @ConfigurationProperties 和@Value

      @ConfigurationProperties @Value 功能 批量注入配置文件中的属性 一个个指定 松散绑定(松散语法) 支持 不支持 SpEL 不支持 支持 JSR303数据校验 支持 ...

  7. jenkins 没有maven选项,怎么办

    第一步: 进入jenkins,点系统管理 第二:插件管理 点击“可选插件”  然后在右边的过滤输入框中输入搜索关键字: Maven Integration  或者 Pipeline Maven Int ...

  8. java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderL

    今天学习spring+cxf的时候遇到一个问题:在web.xml中配置了spring的上下文监听器: <listener> <listener-class>org.spring ...

  9. Packed with amazing data about the world in 201

    Only those who have the patience to do simple things,perfectly ever acquire the skill to do difficul ...

  10. Haskell语言学习笔记(46)Parsec(3)

    Applicative Parsing 使用 Applicative 式的 Parser. 包括使用 (<$>), (<*>), (<$), (<*), (*> ...