计算用户行为日志 DWD 层

33 篇文章 4 订阅
16 篇文章 8 订阅

2.2、计算用户行为日志 DWD 层

2.2.1、准备用户行为日志 DWD 层

  前面采集的日志数据已经保存到 Kafka 中,作为日志数据的 ODS 层,从 kafka 的ODS 层读取的日志数据分为 3 类, 页面日志、启动日志和曝光日志。这三类数据虽然都是用户行为数据,但是有着完全不一样的数据结构,所以要拆分处理。将拆分后的不同的日志写回 Kafka 不同主题中,作为日志 DWD 层。

  页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出到曝光侧输出流

2.2.2 主要任务

➢ 识别新老用户

  • 本身客户端业务有新老用户的标识,但是不够准确,需要用实时计算再次确认(不涉及
    业务操作,只是单纯的做个状态确认)。

➢ 利用侧输出流实现数据拆分

  • 根据日志数据内容,将日志数据分为 3 类, 页面日志、启动日志和曝光日志。页面日志
    输出到主流,启动日志输出到启动侧输出流,曝光日志输出到曝光日志侧输出流

➢ 将不同流的数据推送下游的 kafka 的不同 Topic 中

2.2.3、代码实现

2.2.3.1、接收 Kafka 数据,并进行转换
  1. 封装操作 Kafka 的工具类,并提供获取 kafka 消费者的方法(读)
public class MyKafkaUtil {
    private static String KAFKA_SERVER = "s202:9092,s203:9092,s204:9092";
    private static String DEFAULT_TOPIC = "DEFAULT_DATA";

    //获取FlinkKafkaConsumer
    public static FlinkKafkaConsumer<String> getKafkaSource(String topic, String groupId) {
        //Kafka连接的一些属性配置
        Properties props = new Properties();
        props.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_SERVER);
        return new FlinkKafkaConsumer<String>(topic, new SimpleStringSchema(), props);
    }
}
  1. Flink 调用工具类读取数据的主程序
/**
 * Author: chb
 * Date: 2021/1/30
 * Desc: 准备用户行为日志的DWD层
 */
public class BaseLogApp {
    private static final String TOPIC_START = "dwd_start_log";
    private static final String TOPIC_DISPLAY = "dwd_display_log";
    private static final String TOPIC_PAGE = "dwd_page_log";

    public static void main(String[] args) throws Exception {
        //TODO 1.准备环境
        //1.1 创建Flink流执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //1.2设置并行度
        env.setParallelism(1);

        //1.3设置Checkpoint
        //每5000ms开始一次checkpoint,模式是EXACTLY_ONCE(默认)
        //env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE);
        //env.getCheckpointConfig().setCheckpointTimeout(60000);
        //env.setStateBackend(new FsStateBackend("hdfs://s202:8020/chb/checkpoint/baselogApp"));

        //System.setProperty("HADOOP_USER_NAME","atguigu");

        //TODO 2.从Kafka中读取数据
        String topic = "ods_base_log";
        String groupId = "base_log_app_group";

        //2.1 调用Kafka工具类,获取FlinkKafkaConsumer
        FlinkKafkaConsumer<String> kafkaSource = MyKafkaUtil.getKafkaSource(topic, groupId);
        DataStreamSource<String> kafkaDS = env.addSource(kafkaSource);

        //TODO 3.对读取到的数据格式进行转换         String->json
        SingleOutputStreamOperator<JSONObject> jsonObjDS = kafkaDS.map(
                new MapFunction<String, JSONObject>() {
                    @Override
                    public JSONObject map(String value) throws Exception {
                        JSONObject jsonObject = JSON.parseObject(value);
                        return jsonObject;
                    }
                }
        );
        jsonObjDS.print("json>>>>>>>>");
   }
2.2.3.2、识别新老访客

  保存每个 mid 的首次访问日期(参考使用KeyState–ValueState),每条进入该算子的访问记录,都会把 mid 对应的首次访问时间读取出来,跟当前日期进行比较,只有首次访问时间不为空,且首次访问时间早于当日的,则认为该访客是老访客,否则是新访客。
  同时如果是新访客且没有访问记录的话,会写入首次访问时间。

        /*
        TODO 4.识别新老访客     前端也会对新老状态进行记录,有可能会不准,咱们这里是再次做一个确认
            保存mid某天方法情况(将首次访问日期作为状态保存起来),等后面该设备在有日志过来的时候,从状态中获取日期
            和日志产生日志进行对比。如果状态不为空,并且状态日期和当前日期不相等,说明是老访客,如果is_new标记是1,那么对其状态进行修复
        */
        //4.1 根据mid对日志进行分组
        KeyedStream<JSONObject, String> midKeyedDS = jsonObjDS.keyBy(
            data -> data.getJSONObject("common").getString("mid")
        );
        //4.2 新老方法状态修复   状态分为算子状态和键控状态,我们这里要记录某一个设备的访问,使用键控状态比较合适
        SingleOutputStreamOperator<JSONObject> jsonDSWithFlag = midKeyedDS.map(
            new RichMapFunction<JSONObject, JSONObject>() {
                //定义该mid访问状态
                private ValueState<String> firstVisitDateState;
                //定义日期格式化对象
                private SimpleDateFormat sdf;

                @Override
                public void open(Configuration parameters) throws Exception {
                    //对状态以及日期格式进行初始化
                    firstVisitDateState = getRuntimeContext().getState(new ValueStateDescriptor<String>("newMidDateState", String.class));
                    sdf = new SimpleDateFormat("yyyyMMdd");
                }

                @Override
                public JSONObject map(JSONObject jsonObj) throws Exception {
                    //获取当前日志标记状态
                    String isNew = jsonObj.getJSONObject("common").getString("is_new");

                    //获取当前日志访问时间戳
                    Long ts = jsonObj.getLong("ts");

                    if ("1".equals(isNew)) {
                        //获取当前mid对象的状态
                        String stateDate = firstVisitDateState.value();
                        //对当前条日志的日期格式进行抓换
                        String curDate = sdf.format(new Date(ts));
                        //如果状态不为空,并且状态日期和当前日期不相等,说明是老访客
                        if (stateDate != null && stateDate.length() != 0) {
                            //判断是否为同一天数据
                            if (!stateDate.equals(curDate)) {
                                isNew = "0";
                                jsonObj.getJSONObject("common").put("is_new", isNew);
                            }
                        } else {
                            //如果还没记录设备的状态,将当前访问日志作为状态值
                            firstVisitDateState.update(curDate);
                        }
                    }
                    return jsonObj;
                }
            }
        );

        jsonDSWithFlag.print(">>>>>>>>>>>");
2.2.3.3、 利用侧输出流实现数据拆分

本小节考察的是侧输出流,基本使用案例
  根据日志数据内容,将日志数据分为 3 类, 页面日志、启动日志和曝光日志。页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出到曝光日志侧输出流

2.2.3.3.1、报错Could not determine TypeInformation for the OutputTag type
Exception in thread "main" org.apache.flink.api.common.functions.InvalidTypesException: Could not determine TypeInformation for the OutputTag type. The most common reason is forgetting to make the OutputTag an anonymous inner class. It is also not possible to use generic type variables with OutputTags, such as 'Tuple2<A, B>'.
	at org.apache.flink.util.OutputTag.<init>(OutputTag.java:69)
	at com.chb.realtime.app.dwd.BaseLogApp.main(BaseLogApp.java:134)
Caused by: org.apache.flink.api.common.functions.InvalidTypesException: The types of the interface org.apache.flink.util.OutputTag could not be inferred. Support for synthetic interfaces, lambdas, and generic or raw types is limited at this point
	at org.apache.flink.api.java.typeutils.TypeExtractor.getParameterType(TypeExtractor.java:1185)
	at org.apache.flink.api.java.typeutils.TypeExtractor.privateCreateTypeInfo(TypeExtractor.java:733)
	at org.apache.flink.api.java.typeutils.TypeExtractor.createTypeInfo(TypeExtractor.java:713)
	at org.apache.flink.api.java.typeutils.TypeExtractor.createTypeInfo(TypeExtractor.java:706)
	at org.apache.flink.util.OutputTag.<init>(OutputTag.java:66)
	... 1 more

解决: 在new OutputTag<String>("start")后加上{}

        //定义启动侧输出流标签
        OutputTag<String> startTag = new OutputTag<String>("start"){};

实现分流
在这里插入图片描述

2.2.3.4、将不同流的数据推送到下游 kafka 的不同 Topic(分流)
  1. 在 MyKafkaUtil 工具类中封装获取生产者的方法(写)
    //封装FlinkKafkaProducer
    public static FlinkKafkaProducer<String> getKafkaSink(String topic) {
        return new FlinkKafkaProducer<String>(KAFKA_SERVER, topic, new SimpleStringSchema());
    }

  1. 程序中调用 kafka 工具类获取 sink–KafkaSink
        //TODO 6.将不同流的数据写回到kafka的不同topic中
        FlinkKafkaProducer<String> startSink = MyKafkaUtil.getKafkaSink(TOPIC_START);
        startDS.addSink(startSink);

        FlinkKafkaProducer<String> displaySink = MyKafkaUtil.getKafkaSink(TOPIC_DISPLAY);
        displayDS.addSink(displaySink);

        FlinkKafkaProducer<String> pageSink = MyKafkaUtil.getKafkaSink(TOPIC_PAGE);
        pageDS.addSink(pageSink);


2.2.4、测试

1、启动chb-logger
2、中运行 DwdBaseLog 类
3、运行 rt_applog 下模拟生成数据的 jar 包
4、到 kafka 不同的主题下查看输出效果

关注我的公众号【宝哥大数据】, 更多干货

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值