电商用户行为分析大数据平台(二)-- 用户session分析模块

一、用户session模块需求分析
-------------------------------------------
    1.按条件筛选session
        a.搜索过某些关键词的用户、访问时间在某个时间段内的用户、年龄在某个范围内的用户、职业在某个范围内的用户、所在某个城市的用户,发起的session
        b.这个功能,可以让使用者,对感兴趣的和关系的用户群体,进行后续各种复杂业务逻辑的统计和分析,
        拿到的结果数据,就是只是针对特殊用户群体的分析结果;而不是对所有用户进行分析的泛泛的分析结果。
        比如说,现在某个企业高层,就是想看到用户群体中,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占比
        a.访问时长,也就是说一个session对应的开始的action,到结束的action,之间的时间范围;
        b.访问步长,指的是,一个session执行期间内,依次点击过多少个页面,比如说,一次session,维持了1分钟,那么访问时长就是1m,
        然后在这1分钟内,点击了10个页面,那么session的访问步长,就是10.
        c.目的是让人从全局的角度看到,符合某些条件的用户群体,使用我们的产品的一些习惯。
        比如大多数人,到底是会在产品中停留多长时间,大多数人,会在一次使用产品的过程中,访问多少个页面。
        那么对于使用者来说,有一个全局和清晰的认识。

    3.在符合条件的session中,按照时间比例随机抽取1000个session

    4.在符合条件的session中,获取点击、下单和支付数量排名前10的品类
        a.可以让我们明白,就是符合条件的用户,他最感兴趣的商品是什么种类。这个可以让公司里的人,清晰地了解到不同层次、不同类型的用户的心理和喜好

    5.对于排名前10的品类,分别获取其点击次数排名前10的session
        a.这个功能,可以让我们看到,各个品类最感兴趣最典型的用户的session的行为


二、技术方案设计
------------------------------------------------
    1.按条件筛选session
        防止数据量过大,先对hive中的表进行session粒度的聚合(group by user_id),然后按照指定的粒度进行筛选工作。

    2.聚合统计占比
        因为spark是分布式的,所以聚合统计的时候,要使用计数器进行累加操作
        自定义Accumulator累加器(通过一个accumulator进行统计)

    3.在符合条件的session中,按照时间比例随机抽取1000个session
        综合运用Spark的countByKey、groupByKey、mapToPair等算子,来开发一个复杂的按时间比例随机均匀采样抽取的算法

    4.在符合条件的session中,获取点击、下单和支付数量排名前10的品类
        使用Spark的自定义Key二次排序算法的技术,来实现所有品类,按照三个字段,点击数量、下单数量、支付数量依次进行排序,
        首先比较点击数量,如果相同的话,那么比较下单数量,如果还是相同,那么比较支付数量。

    5.对于排名前10的品类,分别获取其点击次数排名前10的session
        对排名前10的品类对应的数据,按照品类id进行分组,然后求出每组点击数量排名前10的session


三、表设计(Mysql)
---------------------------------------------------
    1.第一表:session_aggr_stat表,存储第一个功能,session聚合统计的结果
       CREATE TABLE `session_aggr_stat` (
         `task_id` int(11) NOT NULL,
         `session_count` int(11) DEFAULT NULL,
         `1s_3s` double DEFAULT NULL,
         `4s_6s` double DEFAULT NULL,
         `7s_9s` double DEFAULT NULL,
         `10s_30s` double DEFAULT NULL,
         `30s_60s` double DEFAULT NULL,
         `1m_3m` double DEFAULT NULL,
         `3m_10m` double DEFAULT NULL,
         `10m_30m` double DEFAULT NULL,
         `30m` double DEFAULT NULL,
         `1_3` double DEFAULT NULL,
         `4_6` double DEFAULT NULL,
         `7_9` double DEFAULT NULL,
         `10_30` double DEFAULT NULL,
         `30_60` double DEFAULT NULL,
         `60` double DEFAULT NULL,
         PRIMARY KEY (`task_id`)
       ) ENGINE=InnoDB DEFAULT CHARSET=utf8


    2.第二个表:session_random_extract表,存储我们的按时间比例随机抽取功能抽取出来的1000个session
        CREATE TABLE `session_random_extract` (
          `task_id` int(11) NOT NULL,
          `session_id` varchar(255) DEFAULT NULL,
          `start_time` varchar(50) DEFAULT NULL,
          `end_time` varchar(50) DEFAULT NULL,
          `search_keywords` varchar(255) DEFAULT NULL,
          PRIMARY KEY (`task_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8


    c.第三个表:top10_category表,存储按点击、下单和支付排序出来的top10品类数据
        CREATE TABLE `top10_category` (
          `task_id` int(11) NOT NULL,
          `category_id` int(11) DEFAULT NULL,
          `click_count` int(11) DEFAULT NULL,
          `order_count` int(11) DEFAULT NULL,
          `pay_count` int(11) DEFAULT NULL,
          PRIMARY KEY (`task_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8


    d.第四个表:top10_category_session表,存储top10每个品类的点击top10的session
        CREATE TABLE `top10_category_session` (
          `task_id` int(11) NOT NULL,
          `category_id` int(11) DEFAULT NULL,
          `session_id` varchar(255) DEFAULT NULL,
          `click_count` int(11) DEFAULT NULL,
          PRIMARY KEY (`task_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8


    e.最后一张表:session_detail,用来存储随机抽取出来的session的明细数据、top10品类的session的明细数据
        CREATE TABLE `session_detail` (
          `task_id` int(11) NOT NULL,
          `user_id` int(11) DEFAULT NULL,
          `session_id` varchar(255) DEFAULT NULL,
          `page_id` int(11) DEFAULT NULL,
          `action_time` varchar(255) DEFAULT NULL,
          `search_keyword` varchar(255) DEFAULT NULL,
          `click_category_id` int(11) DEFAULT NULL,
          `click_product_id` int(11) DEFAULT NULL,
          `order_category_ids` varchar(255) DEFAULT NULL,
          `order_product_ids` varchar(255) DEFAULT NULL,
          `pay_category_ids` varchar(255) DEFAULT NULL,
          `pay_product_ids` varchar(255) DEFAULT NULL,
          PRIMARY KEY (`task_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8

    f.额外的一张表:task表,用来存储J2EE平台插入其中的任务的信息
        CREATE TABLE `task` (
          `task_id` int(11) NOT NULL AUTO_INCREMENT,
          `task_name` varchar(255) DEFAULT NULL,
          `create_time` varchar(255) DEFAULT NULL,
          `start_time` varchar(255) DEFAULT NULL,
          `finish_time` varchar(255) DEFAULT NULL,
          `task_type` varchar(255) DEFAULT NULL,
          `task_status` varchar(255) DEFAULT NULL,
          `task_param` text,
          PRIMARY KEY (`task_id`)
        ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8


四、核心技术点
------------------------------------------
    1.通过底层数据聚合,来减少spark作业处理数据量,从而提升spark作业的性能(从根本上提升spark性能的技巧)
    2.自定义Accumulator实现复杂分布式计算的技术
    3.Spark按时间比例随机抽取算法
    4.Spark自定义key二次排序技术
    5.Spark分组取TopN算法
    6.通过Spark的各种功能和技术点,进行各种聚合、采样、排序、取TopN业务的实现


五、开发准备
-------------------------------------------
    1.新建maven项目
        <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
            <modelVersion>4.0.0</modelVersion>

            <groupId>com.test</groupId>
            <artifactId>Eshop_Spark</artifactId>
            <version>1.0-SNAPSHOT</version>

            <dependencies>
                <dependency>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                    <version>3.8.1</version>
                    <scope>test</scope>
                </dependency>
                <dependency>
                    <groupId>org.apache.spark</groupId>
                    <artifactId>spark-core_2.10</artifactId>
                    <version>1.5.1</version>
                </dependency>
                <dependency>
                    <groupId>org.apache.spark</groupId>
                    <artifactId>spark-sql_2.10</artifactId>
                    <version>1.5.1</version>
                </dependency>
                <dependency>
                    <groupId>org.apache.spark</groupId>
                    <artifactId>spark-hive_2.10</artifactId>
                    <version>1.5.1</version>
                </dependency>
                <dependency>
                    <groupId>org.apache.spark</groupId>
                    <artifactId>spark-streaming_2.10</artifactId>
                    <version>1.5.1</version>
                </dependency>
                <dependency>
                    <groupId>org.apache.hadoop</groupId>
                    <artifactId>hadoop-client</artifactId>
                    <version>2.4.1</version>
                </dependency>
                <dependency>
                    <groupId>org.apache.spark</groupId>
                    <artifactId>spark-streaming-kafka_2.10</artifactId>
                    <version>1.5.1</version>
                </dependency>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>5.1.6</version>
                </dependency>
                <dependency>
                    <groupId>org.json</groupId>
                    <artifactId>json</artifactId>
                    <version>20090211</version>
                </dependency>
                <dependency>
                    <groupId>com.fasterxml.jackson.core</groupId>
                    <artifactId>jackson-core</artifactId>
                    <version>2.4.3</version>
                </dependency>
                <dependency>
                    <groupId>com.fasterxml.jackson.core</groupId>
                    <artifactId>jackson-databind</artifactId>
                    <version>2.4.3</version>
                </dependency>
                <dependency>
                    <groupId>com.fasterxml.jackson.core</groupId>
                    <artifactId>jackson-annotations</artifactId>
                    <version>2.4.3</version>
                </dependency>
                <dependency>
                    <groupId>com.alibaba</groupId>
                    <artifactId>fastjson</artifactId>
                    <version>1.1.41</version>
                </dependency>
            </dependencies>

        </project>


    2.新建工具包 -- com.test.sparkproject.util包

    3.新建配置文件管理包 -- com.test.sparkproject.conf

    4.JDBC原理介绍以及增删改查示范

    5.数据连接池
        1.数据库连接池,会自己在内部持有一定数量的数据库连接,比如通常可能是100~1000个左右。
        然后每次java程序要通过数据库连接往MySQL发送SQL语句的时候,都会从数据库连接池中获取一个连接,然后通过它发送SQL语句。
        SQL语句执行完之后,不会调用Connection.close(),而是将连接还回数据库连接池里面去。
        下一次,java程序再需要操作数据库的时候,就还是重复以上步骤,获取连接、发送SQL、还回连接。

        2.数据库连接池的好处:
          a.java程序不用自己去管理Connection的创建和销毁,代码上更加方便。
          b.程序中只有固定数量的数据库连接,不会一下子变得很多,而且也不会进行销毁。那么对于短时间频繁进行数据库操作的业务来说。就有很高的意义和价值。也就是说,如果短时间内,频繁操作10000次,不需要对数据库连接创建和销毁10000次。这样的话,可以大幅度节省我们的数据库连接的创建和销毁的资源开销以及时间开销。
          c.最终可以提升整个应用程序的性能。

    6.单例模式

    7.内部类和匿名内部类
        内部类有两种,一种是静态内部类,一种是非静态内部类。
        静态内部类和非静态内部类之间的区别主要如下:
        1、内部原理的区别:
        静态内部类是属于外部类的类成员,是一种静态的成员,是属于类的,就有点类似于private static Singleton instance = null;非静态内部类,是属于外部类的实例对象的一个实例成员,也就是说,每个非静态内部类,不是属于外部类的,是属于外部类的每一个实例的,创建非静态内部类的实例以后,非静态内部类实例,是必须跟一个外部类的实例进行关联和有寄存关系的。
        2、创建方式的区别:
        创建静态内部类的实例的时候,只要直接使用“外部类.内部类()”的方式,就可以,比如new School.Teacher();创建非静态内部类的实例的时候,必须要先创建一个外部类的实例,然后通过外部类的实例,再来创建内部类的实例,new School().Teader()
        匿名内部类的使用场景,通常来说,就是在一个内部类,只要创建一次,使用一次,以后就不再使用的情况下,就可以。那么,此时,通常不会选择在外部创建一个类,而是选择直接创建一个实现了某个接口、或者继承了某个父类的内部类,而且通常是在方法内部,创建一个匿名内部类。

    8.开发jdbc辅助组件

    9.javabean
        首先,它需要有一些field,这些field,都必须用private来修饰,表示所有的field,都是私有化的,不能随意的获取和设置
        其次,需要给所有的field,都提供对应的setter和getter方法,什么叫setter和getter?setter,就是说setX()方法,用于给某个field设置值;getter,就是说getX()方法,用于对某个field获取值

    10.dao模式
        引入了DAO模式以后,就大大降低了业务逻辑层和数据访问层的耦合,大大提升了后期的系统维护的效率,并降低了时间成本。
        我们自己在实现DAO模式的时候,通常来说,会将其分为两部分,一个是DAO接口;一个是DAO实现类。
        我们的业务的代码,通常就是面向接口进行编程;那么当接口的实现需要改变的时候,直接定义一个新的实现即可。
        但是对于我们的业务代码来说,只要面向接口开发就可以了。
        DAO的改动对业务代码应该没有任何的影响。

    11.工厂模式
        如果说,你的TaskDAOImpl这个类,在你的程序中出现了100次,那么你就需要修改100个地方。这对程序的维护是一场灾难。
        对于一些种类的对象,使用一个工厂,来提供这些对象创建的方式,外界要使用某个类型的对象时,就直接通过工厂来获取即可。不用自己手动一个一个地方的去创建对应的对象。
        如果想改变TaskDAOImpl,只需要改变工厂类的内容即可

    12.FastJson


六、按照指定的时间段查询访问表,然后根据sessionid进行聚合,从而得到用户的访问信息
-----------------------------------------------------------------------------
    1.准备数据:测试阶段,在内存中生成user_visit_action表
        mockData(sc, sqlContext)

    2.准备sparkcongf
        SparkConf conf = new SparkConf().setAppName("...").setMaster("local");
        JavaSparkContext sc = new JavaSparkContext(conf);
        //本地测试
        SqlContext sqlContext = new SqlContext(sc.sc());
        //生产环境
        SqlContext hiveContext = new HiveContext(sc.sc());

    3.根据起止时间,加载user_visit_action,生成DataFrame.最终生成user_visit_action所对应的JavaRDD<Row>,其中每一行表数据对应一个Row
        使用ITaskDao,访问Task表的taskparam字段,读取startDate和endDate
        String sql = "select * from user_visit_action where data >= startDate and date <= endDate";
        DataFrame df = sqlContext.sql(sql);
        JavaRdd<Row> actionRdd = df.javaRDD();

    4.变换actionRdd,形成<sessionid, row> 形式的RDD
        JavaRDD<Row>actionRdd.maptoPair() ----> JavaPairRDD<sessionid,Row> actionPairRdd

    5.将actionPairRdd按照key聚合,将同一个session的所有行为聚合在一起
        JavaPairRDD<sessionid,Row> actionPairRdd.groupByKey() ----> JavaPairRDD<Sessionid, Itertor<Row>> partAggrRdd

    6.这些行为有的是点击,有的是下单等,这里需要关心的是用户的行为信息:点击了哪些品类[click_category_id]和搜索了哪些关键词[search_keyword字段]
     并且更改key sessionid to userid
        JavaPairRDD<Sessionid, Itertor<Row>> partAggrRdd.mapToPair() ----> JavaPairRDD<userid, stringInfo> partAggrRdd1
        stringInfo:用字符串去拼接用户点击的品类和搜索的关键字信息   sessionid + "|" + searchKeywords + "|" + clickCategoryIds

     7.使用sqlContext.sql查询UserInfo表,获取用户RDD信息
        String sql = "select * from user_info"
        DateFrame df = sqlContext.sql(sql);
        JavaRDD<Row> userRDD = df.javaRDD();

     8.将userRDD 变成以userid为key的RDD
        JavaRDD<Row> userRDD.mapToPair() ----> JavaRDD<userid, Row> userInfoRDD.mapToPair()

     9.将userInfoRDD 与 partAggrRdd1 进行join,将session信息和用户信息结合起来
        JavaPairRDD<userid, stringInfo> partAggrRdd1. join( JavaRDD<userid, Row> userInfoRDD ) ----> JavaPairRDD<userid,Tuple<stringInfo,Row>> partAggrRdd2

     10.将partAggrRdd2进行map变换,变成JavaPairRDD<sessionid,fullsessionInfo>
        JavaPairRDD<userid,Tuple<stringInfo,Row>> partAggrRdd2.mapToPair() ----> JavaPairRDD<sessionid, fullAggrInfo> fullAggrRDD

     11.最终得到
        <sessionid, sessionid_searchwords_clickitems_username_age_city_...> 的rdd复合,最为后续条件过滤查询的数据源fullAggrRDD


七、对fullAggrRDD进行制定的条件查询
-------------------------------------------------
    JavaPairRDD<sessionid, fullAggrInfo> fullAggrRDD. filter() ---->
        a.filter会遍历每一条<sessionid, fullAggrInfo>
        b.得到每条sessionid的user的age,professional,city,searchword等信息
        c.通过TaskParam得到用户的过滤条件比如startAge,endAge,professionals,cities,男或者女等
        d.按照条件执行过滤,符合返回true,不符合返回false
            按照年龄范围进行过滤(startAge、endAge)
            按照职业范围进行过滤(professionals)
            按照城市范围进行过滤(cities)
            按照性别进行过滤
            按照搜索词进行过滤
            按照点击品类id进行过滤


八、用户session聚合统计之自定义计数器Accumulator
-----------------------------------------------------------
    1.目的
        统计出来之前通过条件过滤的session,访问时长在0s~3s的session的数量,占总session数量的比例;4s~6s。。。。;
        访问步长在1~3的session的数量,占总session数量的比例;4~6。。。;

    2.传统的方法计数,使用的Accumulator太多了,动辄十几几十个,非常不便于维护。所以使用自定义的Accumulator
        使用一个Accumulator类维护所有的计数器
        使用自定义的Accumulator可以更方便的进行中间状态的维护,而且不用担心并发和锁的问题

    3.步骤
        a.实现AccumulatorParam<String>接口。其中的String,是你要进行更新操作的对象。可以是自定义的类
        b.实现四个基本方法
            public String zero(String v) { /*你自己的逻辑*/  }

            public String addInPlace(String v1, String v2) {
                  return add(v1, v2);
               }


            public String addAccumulator(String v1, String v2) {
                return add(v1, v2);
            }

            private String add(String v1, String v2) { /*你自己的逻辑*/}

        c.这里使用拼接字符串的方式,将所有需要计数的字段用 field=count 和 | 连接起来,字符串中的field出现一次,就在后面的count上加一

        d.Accumulator<String> sessionAggrStatAccumulator = sc.accumulator("", new SessionAggrStatAccumulator());

九、重构功能实现
-------------------------------------------
    1.上面已经实现了时间范围限定和指定的条件范围过滤查询。接下来要实现的是聚合统计。统计1-3秒内session的占比等

    2.为什么要重构?
        如果要统计1-3秒内的session占比,基本思路是先映射<sessionid,row>,然后按照sessionid进行聚合,计算出每个session的访问时长和步长
        形成新的RDD,然后遍历新的RDD,对总访问和1-3秒内的访问累加,然后相除。
        这样就出现了新的问题?前面已经对actions数据进行了映射和过滤,为什么不在映射和过滤的同时,顺便处理好session的访问步长和时长
        以及相对应的计数器呢

    3.重构实现思路:
         * 尽量不要去生成任何新的RDD(处理上亿的数据)
         * 尽量不要去单独遍历一遍session的数据(处理上千万的数据)
         * 可以在进行session聚合的时候,就直接计算出来每个session的访问时长和访问步长
         * 在进行过滤的时候,本来就要遍历所有的聚合session信息,此时,就可以在某个session通过筛选条件后
               将其访问时长和访问步长,累加到自定义的Accumulator上面去
         * 就是两种截然不同的思考方式,和实现方式,在面对上亿,上千万数据的时候,甚至可以节省时间长达
              半个小时,或者数个小时


十、开发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,性能的重要程度,远远大于一些代码
            的规范,和设计模式,代码的划分,类的划分;大数据,大数据,最重要的,就是性能
            主要就是因为大数据以及大数据项目的特点,决定了,大数据的程序和项目的速度,都比较慢
            如果不优先考虑性能的话,会导致一个大数据处理程序运行时间长度数个小时,甚至数十个小时
            此时,对于用户体验,简直就是一场灾难

            所以,推荐大数据项目,在开发和代码的架构中,优先考虑性能;其次考虑功能代码的划分、解耦合

            我们如果采用第一种实现方案,那么其实就是代码划分(解耦合、可维护)优先,设计优先
            如果采用第二种方案,那么其实就是性能优先


十一、将上述统计结果导入到mysql
----------------------------------------------------
    1.将结果封装成domian,插入到mysql中

    2.对于Accumulator这种分布式累加计算的变量的使用,有一个重要说明
     * 从Accumulator中,获取数据,插入数据库的时候,一定要,一定要,是在有某一个action操作以后再进行。。。


猜你喜欢

转载自blog.csdn.net/xcvbxv01/article/details/86505756