原文链接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. jmeter java请求:java.lang.VerifyError: Cannot inherit from final class

    被这个问题block了一天,应该是包冲突的.通过对包删减排查,结果发现是netty-all-xxx.Final.jar包的问题 应该是jmeter版本和netty版本的冲突吧,换成jmeter 3.1 ...

  2. bat 笔记 一

    echo 有两个参数 off 和 on 注意echo前面要加个@才生效 当 @echo off的时候就是将doc命令将前面的路径去掉,默认其实就是@echo on显示路径: 默认的状态: 输入@ech ...

  3. 13 python logging模块

    原文:http://www.cnblogs.com/dahu-daqing/p/7040764.html 1 logging模块简介 logging模块是Python内置的标准模块,主要用于输出运行日 ...

  4. LINQ to SQL语句(1)Select查询的九种形式

    目录 说明 简单形式 匿名类型形式 条件形式 指定类型形式 筛选形式 Shaped形式 嵌套形式 本地调用方法形式 Distinct形式 说明 与SQL命令中的select作用相似但位置不同,查询表达 ...

  5. Git revert merge

    从history1分出来的A, B两个branch A上有一些更改,例如 changeA2 changeA1 history1 B上有一些更改,例如 changeB2 changeB1 history ...

  6. linux 中特殊符号用法详解

    # 井号 (comments)#管理员  $普通用户 脚本中 #!/bin/bash   #!/bin/sh井号也常出现在一行的开头,或者位于完整指令之后,这类情况表示符号后面的是注解文字,不会被执行 ...

  7. select 中添加option的注意

    在平时写JS中经常要给Select添加option,如果我们把option中的数据用一个字符串来表示: eg: var strOption='<option>1</option> ...

  8. Moment-JavaScript 日期处理类库

    来源:http://momentjs.cn/ 日期格式化 moment().format('MMMM Do YYYY, h:mm:ss a'); // 二月 22日 2017, 4:04:26 下午 ...

  9. moco入门

    前提:moco是什么?有什么用 Moco是针对HTTP集成而生的,不过,现在也有人把它用在其它需要一个模拟服务器的场景中.比如,在移动开发中,有人开发一个移动应用,需要有一个远端服务,但在开发时,这个 ...

  10. 几个小模板:topology, dijkstra, spfa, floyd, kruskal, prim

    1.topology: #include <fstream> #include <iostream> #include <algorithm> #include & ...