实时数仓(三)---DWM层

DWM层和DWS层

一、设计

设计思路

​ 我们之前在DWD层通过分流将数据拆分成了相互独立的kafka topic。那么接下来如何处理数据,就需要思考一下我们到底要通过实时计算出哪些指标项。

​ 因为实时计算与离线不同,实时计算的开发和运维成本都是非常高的,要结合实际情况考虑是否有必要象离线数仓一样,建一个大而全的中间层。

​ 如果没有必要大而全,这时候就需要大体规划一下要实时计算出的指标需求了。把这些指标以主题宽表的形式输出就是我们的DWS层

要求梳理

统计主题 需求指标 输出方式 计算来源 来源层级
访客 pv 可视化大屏 page_log直接可求 dwd
uv 可视化大屏 需要用page_log过滤去重 dwm
跳出率 可视化大屏 需要通过page_log行为判断 dwm
进入页面数 可视化大屏 需要识别开始访问标识 dwd
连续访问时长 可视化大屏 page_log直接可求 dwd
商品 点击 多维分析 page_log直接可求 dwd
收藏 多维分析 收藏表 dwd
加入购物车 多维分析 购物车表 dwd
下单 可视化大屏 订单宽表 dwm
支付 多维分析 支付宽表 dwm
退款 多维分析 退款表 dwd
评论 多维分析 评论表 dwd
地区 pv 多维分析 page_log直接可求 dwd
uv 多维分析 需要用page_log过滤去重 dwm
下单 可视化大屏 订单宽表 dwm
关键词 搜索关键词 可视化大屏 页面访问日志 直接可求 dwd
点击商品关键词 可视化大屏 商品主题下单再次聚合 dws
下单商品关键词 可视化大屏 商品主题下单再次聚合 dws

实际需求还会有更多,这里主要以为可视化大屏为目的进行实时计算的处理。

DWM层的定位是什么,DWM层主要服务DWS,因为部分需求直接从DWD层到DWS层中间会有一定的计算量,而且这部分计算的结果很有可能被多个DWS层主题复用,所以部分DWD成会形成一层DWM,我们这里主要涉及业务

  • 访问UV计算
  • 跳出明细计算
  • 订单宽表
  • 支付宽表

二、DWM-访客-UV计算

需求分析与思路

​ UV,全称是Unique Visitor,即独立访客,对于实时计算中,也可以称为DAU(Daily Active User),即每日活跃用户,因为实时计算中的uv通常是指当日的访客数。

那么如何从用户行为日志中识别出当日的访客,那么有两点:

  • 其一,是识别出该访客打开的第一个页面,表示这个访客开始进入我们的应用
  • 其二,由于访客可以在一天中多次进入应用,所以我们要在一天的范围内进行去重

代码

package com.atguigu.app.dwm;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.utils.MyKafkaUtil;
import org.apache.flink.api.common.functions.RichFilterFunction;
import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;

import java.text.SimpleDateFormat;
/*
数据流:
web/app -> nginx -> springboot -> Kafka -> FlinkApp(LogBaseApp) -> Kafka
FlinkApp(DauApp) -> Kafka

服务: Nginx Logger zookeeper Kafka LogbaseApp DauApp 消费者(dwm_unique_visit) MockLog

 */
public class DauApp {
   
    public static void main(String[] args) throws Exception {
   

        //1.获取执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        //        //1.1 设置状态后端
//        env.setStateBackend(new FsStateBackend("hdfs://hadoop102:9000/gmall/dwd_log/ck"));
        //1.2 开启CK
//        env.enableCheckpointing(10000L, CheckpointingMode.EXACTLY_ONCE);
//        env.getCheckpointConfig().setCheckpointTimeout(60000L);
        //2.读取Kafka dwd_page_log主题数据创建流
        String groupId = "unique_visit_app";
        String sourceTopic = "dwd_page_log";
        String sinkTopic = "dwm_unique_visit";
        FlinkKafkaConsumer<String> kafkaSource = MyKafkaUtil.getKafkaSource(sourceTopic, groupId);
        DataStreamSource<String> kafkaDS = env.addSource(kafkaSource);

        //3.将每行数据转换为JSON对象
        SingleOutputStreamOperator<JSONObject> jsonObjDS = kafkaDS.process(new ProcessFunction<String, JSONObject>() {
   
            @Override
            public void processElement(String s, Context context, Collector<JSONObject> collector) throws Exception {
   
                try {
   
                    JSONObject jsonObject = JSON.parseObject(s);
                    collector.collect(jsonObject);
                } catch (Exception e) {
   
                    e.printStackTrace();
                    context.output(new OutputTag<String>("dirty") {
   
                    }, s);

                }
            }
        });


        /*
        kafkaDS.map(new MapFunction<String, JSONObject>() {

            @Override
            public JSONObject map(String s) throws Exception {
                try {
                    return JSON.parseObject(s);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("发现脏数据:"+s);
                    return null;
                }
            }
        });
         */

        //4.按照mid分组
        KeyedStream<JSONObject, String> keyedStream = jsonObjDS.keyBy
                (jsonObject -> jsonObject.getJSONObject("common").getString("mid"));


        //5.过滤掉不是今天第一次访问的数据
        SingleOutputStreamOperator<JSONObject> filterDS = keyedStream.filter(new UvRichFilterFunction());

        //6.写入DWM层Kafka主题中
        filterDS.map(json -> json.toString()).addSink(MyKafkaUtil.getKafkaSink(sinkTopic));
        //7.启动任务
        env.execute();

    }
    public static class UvRichFilterFunction extends RichFilterFunction<JSONObject>{
   
        private ValueState<String> firstVisitState ;
        private SimpleDateFormat simpleDateFormat;
        @Override
        public void open(Configuration parameters) throws Exception {
   

            simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd");


            ValueStateDescriptor<String> stringValueStateDescriptor = new ValueStateDescriptor<>("visit-state", String.class);
            //创建状态TTL配置对象
            StateTtlConfig stateTtlConfig = StateTtlConfig.newBuilder(Time.days(1))
                    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
                    .build();
            //保留24小时


            stringValueStateDescriptor.enableTimeToLive(stateTtlConfig);
            firstVisitState = getRuntimeContext().getState(stringValueStateDescriptor);



        }

        @Override
        public boolean filter(JSONObject jsonObject) throws Exception {
   
            //取出上一次访问页面
            String lastPageId = jsonObject.getJSONObject("page").getString("last_page_id");

            //判断是否存在上一个页面(是不是从上一个页面进来的,而不是新打开的)
            if (lastPageId == null || lastPageId.length()<=0){
   

                //取出状态数据
                String firstVisitData = firstVisitState.value();

                //取出数据时间
                Long ts = jsonObject.getLong("ts");
                String curDate = simpleDateFormat.format(ts);

                if (firstVisitData == null || firstVisitData.equals(curDate)){
   
                    firstVisitState.update(curDate);
                    return true;
                }else {
   
                    return false;
                }


            }else {
   
                return false;
            }

        }
    }


}

三、DWM-访客-跳出明细计算

需求分析与思路

跳出

​ 跳出就是用户成功访问了网站的一个页面后就退出,不在继续访问网站的其它页面。而跳出率就是用跳出次数除以访问次数。

​ 关注跳出率,可以看出引流过来的访客是否能很快的被吸引,渠道引流过来的用户之间的质量对比,对于应用优化前后跳出率的对比也能看出优化改进的成果。

计算跳出行为的思路

首先要识别哪些是跳出行为,要把这些跳出的访客最后一个访问的页面识别出来。那么要抓住几个特征:

  • 该页面是用户近期访问的第一个页面

这个可以通过该页面是否有上一个页面(last_page_id)来判断,如果这个表示为空,就说明这是这个访客这次访问的第一个页面。

  • 首次访问之后很长一段时间(自己设定),用户没继续再有其他页面的访问。

这第一个特征的识别很简单,保留last_page_id为空的就可以了。但是第二个访问的判断,其实有点麻烦,首先这不是用一条数据就能得出结论的,需要组合判断,要用一条存在的数据和不存在的数据进行组合判断。而且要通过一个不存在的数据求得一条存在的数据。更麻烦的他并不是永远不存在,而是在一定时间范围内不存在。那么如何识别有一定失效的组合行为呢?

最简单的办法就是Flink自带的CEP技术。这个CEP非常适合通过多条数据组合来识别某个事件。

用户跳出事件,本质上就是一个条件事件加一个超时事件的组合。

{“common”:{“mid”:“101”},“page”:{“page_id”:“home”},“ts”:1000000000}

{“common”:{“mid”:“102”},“page”:{“page_id”:“home”},“ts”:1000000001}

{“common”:{“mid”:“102”},“page”:{“page_id”:“home”,“last_page_id”:“aa”},“ts”:1000000020}

{“common”:{“mid”:“102”},“page”:{“page_id”:“home”,“last_page_id”:“aa”},“ts”:1000000030}

代码

package com.atguigu.app.dwm;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.utils.MyKafkaUtil;

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternFlatSelectFunction;
import org.apache.flink.cep.PatternFlatTimeoutFunction;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;

import java.util.List;
import java.util.Map;

public class UserJumpDetailApp {
   
    public static void main(String[] args) throws Exception {
   
        //1.获取执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        //        //1.1 设置状态后端
//        env.setStateBackend(new FsStateBackend("hdfs://hadoop102:9000/gmall/dwd_log/ck"));
        //1.2 开启CK
//        env.enableCheckpointing(10000L, CheckpointingMode.EXACTLY_ONCE);
//        env.getCheckpointConfig().setCheckpointTimeout(60000L);
        //2.读取Kafka dwd_page_log主题数据创建流
        String sourceTopic = "dwd_page_log";
        String groupId = "userJumpDetailApp";
        String sinkTopic = "dwm_user_jump_detail";

        FlinkKafkaConsumer<String> kafkaSource = MyKafkaUtil.getKafkaSource(sourceTopic, groupId);
        DataStreamSource<String> kafkaDS = env.addSource(kafkaSource);
//        DataStream<String> kafkaDS = env
//                .fromElements(
//                        "{\"common\":{\"mid\":\"101\"},\"page\":{\"page_id\":\"home\"},\"ts\":1000000000} ",
//                        "{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"home\"},\"ts\":1000000001}",
//                        "{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"good_list\",\"last_page_id\":" +
//                                "\"home\"},\"ts\":1000000020} ",
//                        "{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"good_list\",\"last_page_id\":" +
//                                "\"detail\"},\"ts\":1000000025} "
//                );
//
        //DataStream<String> kafkaDS = env.socketTextStream("hadoop103",9999);
        //提取数据中的时间戳,生成watermark
        WatermarkStrategy<JSONObject> watermarkStrategy = WatermarkStrategy.<JSONObject>forMonotonousTimestamps().withTimestampAssigner(new SerializableTimestampAssigner<JSONObject>() {
   
            @Override
            public long extractTimestamp(JSONObject jsonObject, long l) {
   

                return jsonObject.getLong("ts");
            }
        });

        //3.将数据转换为JSON对象
        SingleOutputStreamOperator<JSONObject> jsonObjDS = kafkaDS.process(new ProcessFunction<String, JSONObject>() {
   
            @Override
            <
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RE:0-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值