本文为《Spark大型电商项目实战》 系列文章之一,主要介绍自定义Accumulator原因、方法及代码实现,统计出用户访问时长和访问步长。
之前已经得到通过条件过滤后的session,现在需要统计出来访问时长在0s~3s的session的数量,占总session数量的比例;4s~6s的session的数量,占总session数量的比例;…… 并且统计出访问步长在1~3的session的数量,占总session数量的比例;4~6的session的数量,占总session数量的比例;……
这是就需要Accumulator,传统的Accumulator实现方式为: 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,可以更方便进行中间状态的维护,而且不用担心并发和锁的问题。
在包com.erik.sparkproject.spark中新建聚合统计Accumulator类SessionAggrStatAccumulator.java,具体代码如下:
package com.erik.sparkproject.spark; import org.apache.spark.AccumulatorParam; import com.erik.sparkproject.constant.Constants; import com.erik.sparkproject.util.StringUtils; /** * session聚合统计Accumulator * 使用自己定义的一些数据格式,比如String,甚至说,我们可以自己定义model,自己定义的类(必须可序列化) * 然后可以基于这种特殊的数据格式实现复杂的分布式计算逻辑 * 各个task分布式在运行,可以根据自己的需求,task给Accumulator传入不同的值 * 根据不同的值,去做复杂的逻辑 * * Spark Core里很使用的高端技术 * @author Erik * */ public class SesssionAggrStatAccumulator implements AccumulatorParam<String> { private static final long serialVersionUID = -2113961376143864034L; //zero方法,主要用于数据初始化 //这里就返回一个值,就是初始化中,所有范围区间的数值都是0 //各个范围区间的统计数量的拼接,还是采用key=value|key=value的连接串格式 //在Constants.java中添加 //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"; 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,然后再更新到连接串里面去 public String addInPlace(String v1, String v2) { return null; } public String addAccumulator(String v1, String v2) { return null; } /** * 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; } }《Spark 大型电商项目实战》源码:https://github.com/Erik-ly/SprakProject
本文为《Spark大型电商项目实战》系列文章之一。 更多文章:Spark大型电商项目实战:http://blog.csdn.net/u012318074/article/category/6744423
