数仓开发之DWD层完整使用 (第五章)

数仓开发之DWD层完整使用

  • 一、流量域未精加工的事务事实表
    • 1、 主要任务
      • 1)数据清洗(ETL)
      • 2)新老访客状态标记修复
      • 3)分流
    • 2、思路
      • 1)数据清洗(ETL)
      • 2)新老访客状态标记修复
        • (1)前端埋点新老访客状态标记设置规则
        • (2)新老访客状态标记修复思路
      • 3)利用侧输出流实现数据拆分
        • (1)埋点日志结构分析
        • (2)分流日志分类
        • (3)分流思路
    • 3、图解
    • 4、代码
      • 1)在 KafkaUtil 工具类中补充 getKafkaProducer() 方法
      • 2)创建 DateFormatUtil 工具类用于日期格式化
      • 3)主程序
  • 二、流量域独立访客事务事实表
    • 1、主要任务
    • 2、思路分析
      • 1)过滤 last_page_id 不为 null 的数据
      • 2)筛选独立访客记录
      • 3)状态存活时间设置
    • 3、图解
    • 4、代码

DWD层
	日志数据:5种(启动、页面、曝光、动作、错误) --- topic_log
	消费ODS主题 - -> 直接按照表拆分写入不同的主题 -> 消费不同的主题数据关联 ->写入kafka主题
		区别:先拆再取

	业务数据:N种(所有需要处理的事实表) --- topic_db
	        订单表&订单明细&订单明细购物卷&订单明细表
	        维度退化
	消费ODS主题数据 - -> 使用程序过滤想要的数据并关联 -> 写入kafka主题
		直接消费ODS、取我们想要的

一、流量域未精加工的事务事实表

1、 主要任务

1)数据清洗(ETL)

数据传输过程中可能会出现部分数据丢失的情况,导致 JSON 数据结构不再完整,因此需要对脏数据进行过滤。

2)新老访客状态标记修复

日志数据 common 字段下的 is_new 字段是用来标记新老访客状态的,1 表示新访客,0 表示老访客。前端埋点采集到的数据可靠性无法保证,可能会出现老访客被标记为新访客的问题,因此需要对该标记进行修复。

3)分流

本节将通过分流对日志数据进行拆分,生成五张事务事实表写入 Kafka
⚪ 流量域页面浏览事务事实表
⚪ 流量域启动事务事实表
⚪ 流量域动作事务事实表
⚪ 流量域曝光事务事实表
⚪ 流量域错误事务事实表

2、思路

1)数据清洗(ETL)

对流中数据进行解析,将字符串转换为 JSONObject,如果解析报错则必然为脏数据。定义侧输出流,将脏数据发送到侧输出流,写入 Kafka 脏数据主题

2)新老访客状态标记修复

(1)前端埋点新老访客状态标记设置规则

以神策提供的第三方埋点服务中新老访客状态标记设置规则为例

1、Web 端:用户第一次访问埋入神策 SDK 页面的当天(即第一天),JS SDK 会在网页的 cookie 中设置一个首日访问的标记,并设置第一天 24 点之前,该标记为 true,即第一天触发的网页端所有事件中,is_new = 1。第一天之后,该标记则为 false,即第一天之后触发的网页端所有事件中,is_new = 0;

2、小程序端:用户第一天访问埋入神策 SDK 的页面时,小程序 SDK 会在 storage 缓存中创建一个首日为 true 的标记,并且设置第一天 24 点之前,该标记均为 true。即第一天触发的小程序端所有事件中,is_new = 1。第一天之后,该标记则为 false,即第一天之后触发的小程序端所有事件中,​is_new = 0;

3、APP 端:用户安装 App 后,第一次打开埋入神策 SDK 的 App 的当天,Android/iOS SDK 会在手机本地缓存内,创建一个首日为 true 的标记,并且设置第一天 24 点之前,该标记均为 true。即第一天触发的 APP 端所有事件中,is_new = 1。第一天之后,该标记则为 false,即第一天之后触发的 APP 端所有事件中,is_new = 0。

本项目模拟生成的是 APP 端日志数据。对于此类日志,如果首日之后用户清除了手机本地缓存中的标记再次启动 APP 会重新设置一个首日为 true 的标记,导致本应为 0 的 is_new 字段被置为 1,可能会给相关指标带来误差。因此,有必要对新老访客状态标记进行修复

(2)新老访客状态标记修复思路

运用 Flink 状态编程,为每个 mid 维护一个键控状态,记录首次访问日期。

①如果 is_new 的值为 1

a)如果键控状态为 null,认为本次是该访客首次访问 APP,将日志中 ts 对应的日期更新到状态中,不对 is_new 字段做修改;

b)如果键控状态不为 null,且首次访问日期不是当日,说明访问的是老访客,将 is_new 字段置为 0;

c)如果键控状态不为 null,且首次访问日期是当日,说明访问的是新访客,不做操作;

②如果 is_new 的值为 0

a)如果键控状态为 null,说明访问 APP 的是老访客但本次是该访客的页面日志首次进入程序。当前端新老访客状态标记丢失时,日志进入程序被判定为老访客,Flink 程序就可以纠正被误判的访客状态标记,只要将状态中的日期设置为今天之前即可。本程序选择将状态更新为昨日;

b)如果键控状态不为 null,说明程序已经维护了首次访问日期,不做操作。

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

(1)埋点日志结构分析

前端埋点获取的 JSON 字符串(日志)可能存在 common、start、page、displays、actions、err、ts 七种字段。其中

common 对应的是公共信息,是所有日志都有的字段
err 对应的是错误信息,所有日志都可能有的字段
start 对应的是启动信息,启动日志才有的字段
page 对应的是页面信息,页面日志才有的字段
displays 对应的是曝光信息,曝光日志才有的字段,曝光日志可以归为页面日志,因此必然有 page 字段
actions 对应的是动作信息,动作日志才有的字段,同样属于页面日志,必然有 page 字段。动作信息和曝光信息可以同时存在。
ts 对应的是时间戳,单位:毫秒,所有日志都有的字段

综上,我们可以将前端埋点获取的日志分为两大类:启动日志和页面日志。二者都有 common 字段和 ts 字段,都可能有 err 字段。页面日志一定有 page 字段,一定没有 start 字段,可能有 displays 和 actions 字段;启动日志一定有 start 字段,一定没有 page、displays 和 actions 字段。

(2)分流日志分类
本节将按照内容,将日志分为以下五类
➡ 启动日志
➡ 页面日志
➡ 曝光日志
➡ 动作日志
➡ 错误日志
(3)分流思路

① 所有日志数据都可能拥有 err 字段,所有首先获取 err 字段,如果返回值不为 null 则将整条日志数据发送到错误侧输出流。然后删掉 JSONObject 中的 err 字段及对应值;

② 判断是否有 start 字段,如果有则说明数据为启动日志,将其发送到启动侧输出流;如果没有则说明为页面日志,进行下一步;

③ 页面日志必然有 page 字段、 common 字段和 ts 字段,获取它们的值,ts 封装为包装类 Long,其余两个字段的值封装为 JSONObject;

④ 判断是否有 displays 字段,如果有,将其值封装为 JSONArray,遍历该数组,依次获取每个元素(记为 display),封装为JSONObject。创建一个空的 JSONObject,将 display、common、page和 ts 添加到该对象中,获得处理好的曝光数据,发送到曝光侧输出流。动作日志的处理与曝光日志相同(注意:一条页面日志可能既有曝光数据又有动作数据,二者没有任何关系,因此曝光数据不为 null 时仍要对动作数据进行处理);

⑤ 动作日志和曝光日志处理结束后删除 displays 和 actions 字段,此时主流的 JSONObject 中只有 common 字段、 page 字段和 ts 字段,即为最终的页面日志。
处理结束后,页面日志数据位于主流,其余四种日志分别位于对应的侧输出流,将五条流的数据写入 Kafka 对应主题即可。

3、图解

数仓开发之DWD层完整使用 (第五章)_第1张图片

4、代码

1)在 KafkaUtil 工具类中补充 getKafkaProducer() 方法

       /**
         * 自定义序列化器和上面的反序列化器都使用下
         * 生产者
         * @param topic
         * @param defaultTopic
         * @return
         */
        public static FlinkKafkaProducer<String> getFlinkKafkaProducer(String topic, String defaultTopic) {
            Properties properties = new Properties();
            //设置集群信息
            properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_SERVER);
            return new FlinkKafkaProducer<String>(
                    defaultTopic,
                    new KafkaSerializationSchema<String>() {
                        @Override
                        public ProducerRecord<byte[], byte[]> serialize(String element, @Nullable Long timestamp) {
                            if (element == null) {
                                return new ProducerRecord<>(topic, "".getBytes());
                            }
    
                            return new ProducerRecord<>(topic, element.getBytes());
                        }
                    }, properties,
                    FlinkKafkaProducer.Semantic.EXACTLY_ONCE
            );
        }

2)创建 DateFormatUtil 工具类用于日期格式化

package org.example.utils;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;

public class DateFormatUtil {

    private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter dtfFull = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static Long toTs(String dtStr, boolean isFull) {

        LocalDateTime localDateTime = null;
        if (!isFull) {
            dtStr = dtStr + " 00:00:00";
        }
        localDateTime = LocalDateTime.parse(dtStr, dtfFull);

        return localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();
    }

    public static Long toTs(String dtStr) {
        return toTs(dtStr, false);
    }

    public static String toDate(Long ts) {
        Date dt = new Date(ts);
        LocalDateTime localDateTime = LocalDateTime.ofInstant(dt.toInstant(), ZoneId.systemDefault());
        return dtf.format(localDateTime);
    }

    public static String toYmdHms(Long ts) {
        Date dt = new Date(ts);
        LocalDateTime localDateTime = LocalDateTime.ofInstant(dt.toInstant(), ZoneId.systemDefault());
        return dtfFull.format(localDateTime);
    }

    public static void main(String[] args) {
        System.out.println(toYmdHms(System.currentTimeMillis()));
    }
}

3)主程序

package org.example.app.dwd;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
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.util.Collector;
import org.apache.flink.util.OutputTag;
import org.example.utils.DateFormatUtil;
import org.example.utils.MyKafkaUtil;

/**
 * 数据流 web/app -> Nginx -> 日志服务器(.log) -> Flume -> Kafka(ODS) -> FlinkApp -> Kafka(DWD)
 * 程序:Mock(lg.sh) -> Flume(f1) ->  kafka(zk) ->BaseLogApp ->kafka(ZK)
 */
public class BaseLogApp {
    public static void main(String[] args) throws Exception {

        //TODO 1、获取执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1); //生产环境中设置为kafka主题分区数

        //1.1 开启Checkpoint
        //env.enableCheckpointing(5 * 60000L, CheckpointingMode.EXACTLY_ONCE); //5分钟开启一次
        //env.getCheckpointConfig().setCheckpointTimeout(10 * 60000L); //超时10分钟
        //env.getCheckpointConfig().setMaxConcurrentCheckpoints(2); //共存的有几个Checkpoint
        //env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, 5)); //总共会尝试3次重启、每隔5秒尝试一次(固定匹配重启)

        //1.2 设置状态后端(本地的内存级别的)
        //env.setStateBackend(new HashMapStateBackend());
        //env.getCheckpointConfig().setCheckpointStorage("hdfs://hadoop102:8082/211126/ck");
        //System.setProperty("HADOOP_USER_NAME", "atguigu");


        //TODO 2、消费kafka topic_log 主题的数据创建流
        String topic = "topic_log";
        String groupId = "base_log_app";
        DataStreamSource<String> kafkaDS = env.addSource(MyKafkaUtil.getFlinkKafkaConsumer(topic, groupId));


        //TODO 3、过滤掉非json格式的数据将每行数据转换为JSON对象
        //测输出流标记(测输出流只能用process)
        OutputTag<String> dirtyTag = new OutputTag<String>("Dirty") {
        };

        SingleOutputStreamOperator<JSONObject> jsonDS = kafkaDS.process(new ProcessFunction<String, JSONObject>() {
            @Override
            public void processElement(String value, Context context, Collector<JSONObject> out) throws Exception {
                try {
                    //没有发生异常写入主流
                    JSONObject jsonObject = JSON.parseObject(value);
                    out.collect(jsonObject);
                } catch (Exception e) {
                    //没有发生异常写入测输出流
                    context.output(dirtyTag, value);
                }
            }
        });

        //获取测输出流脏数据并打印
        DataStream<String> dirtyDS = jsonDS.getSideOutput(dirtyTag);
        dirtyDS.print("Dirty>>>>>>>>>>>>>>");


        /**
         * {
         *      "common":{
         *          "ar":"530000","ba":"Xiaomi","ch":"xiaomi","is_new":"1","md":"Xiaomi 9","mid":"mid_26499","os":"Android 11.0","uid":"845","vc":"v2.0.1"
         *          },
         *              "start":{
         *              "entry":"notice","loading_time":11017,"open_ad_id":13,"open_ad_ms":9784,"open_ad_skip_ms":0
         *              },
         *              "ts":1592126246000
         *              }
         *
         */
        //TODO 4、按照Mid分组(每一个mid一个状态)
        KeyedStream<JSONObject, String> keyedStream = jsonDS.keyBy(json -> json.getJSONObject("common").getString("mid"));

        //TODO 5、使用状态编程做新老访客标记的校验
        SingleOutputStreamOperator<JSONObject> jsonObjectWithNewFlagDS = keyedStream.map(new RichMapFunction<JSONObject, JSONObject>() {

            //状态-存储年月日
            private ValueState<String> lastVisitState;

            @Override
            public void open(Configuration parameters) throws Exception {
                lastVisitState = getRuntimeContext().getState(new ValueStateDescriptor<String>("last-visit", String.class));
            }

            @Override
            public JSONObject map(JSONObject value) throws Exception {

                //获取is_new 标记 & ts
                String isNew = value.getJSONObject("common").getString("is_new");
                Long ts = value.getLong("ts");
                String curDate = DateFormatUtil.toDate(ts);

                //获取状态中的日期
                String lastDate = lastVisitState.value();
                //1、判读is_new 标记是否为 "1"
                if ("1".equals(isNew)) {
                    //如果键控状态为null,则将日志ts对应的日期更新到状态中,不对is_new字段做修改
                    if (lastDate == null) {
                        lastVisitState.update(curDate);
                        //如果状态不为null,且首次访问日期不是当日,则将is_new字段值为0
                    } else if (!lastDate.equals(curDate)) {
                        value.getJSONObject("common").put("is_new", "0");
                    } else {
                        //如果键控状态不为null,且首次访问日期不是当日,不做操作、此else可删除
                    }

                    //如果is_new是0
                    //如果键控状态为null,则将状态中的首次访问日期更新为昨日,这样做可以保证同一mid的其它日志到来时,依然会被判定为老访客
                } else if (lastDate == null) {
                    lastVisitState.update(DateFormatUtil.toDate(ts - 24 * 60 * 60 * 100L));
                } else {
                    //如果键控状态不为null,不做操作
                }
                return value;
            }

        });


        //TODO 6、使用测输出流进行分流处理 --页面日志放到主流
        OutputTag<String> startTag = new OutputTag<String>("start") {
        };
        OutputTag<String> displayTag = new OutputTag<String>("display") {
        };
        OutputTag<String> actionTag = new OutputTag<String>("action") {
        };
        OutputTag<String> errorTag = new OutputTag<String>("error") {
        };

        SingleOutputStreamOperator<String> pageDS = jsonObjectWithNewFlagDS.process(new ProcessFunction<JSONObject, String>() {
            @Override
            public void processElement(JSONObject value, Context context, Collector<String> out) throws Exception {

                //尝试获取错误信息
                String err = value.getString("err");
                if (err != null) {
                    //将数据写到error测输出流
                    context.output(errorTag, value.toJSONString());
                }

                //移除错误信息
                value.remove("err");

                //尝试获取启动信息
                String start = value.getString("start");
                if (start != null) {
                    //将数据写到start测输出流
                    context.output(startTag, value.toJSONString());
                } else {

                    //获取公共信息&页面id&时间戳
                    String common = value.getString("common");
                    String pageId = value.getJSONObject("page").getString("page_id");
                    Long ts = value.getLong("ts");


                    //尝试获取曝光数据
                    JSONArray displays = value.getJSONArray("displays");
                    if (displays != null && displays.size() > 0) {
                        //遍历曝光数据&写到display测输出流
                        for (int i = 0; i < displays.size(); i++) {
                            JSONObject display = displays.getJSONObject(i);
                            display.put("common", common);
                            display.put("pageId", pageId);
                            display.put("ts", ts);
                            context.output(displayTag, display.toJSONString());
                        }
                    }

                    //尝试获取动作数据
                    JSONArray actions = value.getJSONArray("actions");
                    if (actions != null && actions.size() > 0) {
                        //遍历曝光数据&写到display测输出流
                        for (int i = 0; i < actions.size(); i++) {
                            JSONObject action = actions.getJSONObject(i);
                            action.put("common", common);
                            action.put("pageId", pageId);
                            context.output(actionTag, action.toJSONString());
                        }
                    }

                    //移除曝光和动作数据&写到页面日志主流
                    value.remove("displays");
                    value.remove("actions");
                    //页面日志写到主流
                    out.collect(value.toJSONString());
                }
            }
        });

        //TODO 7、提取各个测输出流数据
        DataStream<String> startDS = pageDS.getSideOutput(startTag);
        DataStream<String> displayDS = pageDS.getSideOutput(displayTag);
        DataStream<String> actionDS = pageDS.getSideOutput(actionTag);
        DataStream<String> errorDS = pageDS.getSideOutput(errorTag);

        //TODO 8、将数据打印写并入对应的主题
        pageDS.print("page>>>>>>>>>>");
        startDS.print("start>>>>>>>");
        displayDS.print("display>>>>>>>>");
        actionDS.print("action>>>>>>>>");
        errorDS.print("error>>>>>>>");
        String page_topic = "dwd_traffic_page_log";
        String start_topic = "dwd_traffic_start_log";
        String display_topic = "dwd_traffic_display_log";
        String action_topic = "dwd_traffic_action_log";
        String error_topic = "dwd_traffic_error_log";

        //把测输出发送到kafka
        pageDS.addSink(MyKafkaUtil.getFlinkKafkaProducer(page_topic));
        startDS.addSink(MyKafkaUtil.getFlinkKafkaProducer(start_topic));
        displayDS.addSink(MyKafkaUtil.getFlinkKafkaProducer(display_topic));
        actionDS.addSink(MyKafkaUtil.getFlinkKafkaProducer(action_topic));
        errorDS.addSink(MyKafkaUtil.getFlinkKafkaProducer(error_topic));

        //TODO 9、启动任务
        env.execute();
    }
}

二、流量域独立访客事务事实表

1、主要任务

过滤页面数据中的独立访客访问记录。

2、思路分析

1)过滤 last_page_id 不为 null 的数据

独立访客数据对应的页面必然是会话起始页面,last_page_id 必为 null。过滤 last_page_id != null 的数据,减小数据量,提升计算效率。

2)筛选独立访客记录

运用 Flink 状态编程,为每个 mid 维护一个键控状态,记录末次登录日期。

如果末次登录日期为 null 或者不是今日,则本次访问是该 mid 当日首次访问,保留数据,将末次登录日期更新为当日。否则不是当日首次访问,丢弃数据。

3)状态存活时间设置

如果保留状态,第二日同一 mid 再次访问时会被判定为新访客,如果清空状态,判定结果相同,所以只要时钟进入第二日状态就可以清空。

设置状态的 TTL 为 1 天,更新模式为 OnCreateAndWrite,表示在创建和更新状态时重置状态存活时间。如:2022-02-21 08:00:00 首次访问,若 2022-02-22 没有访问记录,则 2022-02-22 08:00:00 之后状态清空。

3、图解

数仓开发之DWD层完整使用 (第五章)_第2张图片

4、代码

package org.example.app.dwd;

import com.alibaba.fastjson.JSONAware;
import com.alibaba.fastjson.JSONObject;
import org.apache.flink.api.common.functions.FlatMapFunction;
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.util.Collector;
import org.example.utils.DateFormatUtil;
import org.example.utils.MyKafkaUtil;

/**
 * DWD流量域
 * // 数据流:web/app ->Nginx->日志服务器(.log) ->Flume ->kafka(ODS) -> FlinkApp -> kafka(DWD) ->FLINKApp ->kafka(DWD)
 * //程 序:Mock(lg.sh)->Flume(f1) ->kafka(ZK) ->dwdTrafficUniqueVisitorDetail -> kafka(ZK)
 * <p>
 */
public class DwdTrafficUniqueVisitorDetail {

    public static void main(String[] args) throws Exception {
        //TODO 1、获取执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1); //生产环境中设置为kafka主题分区数

        //1.1 开启Checkpoint
        //env.enableCheckpointing(5 * 60000L, CheckpointingMode.EXACTLY_ONCE); //5分钟开启一次
        //env.getCheckpointConfig().setCheckpointTimeout(10 * 60000L); //超时10分钟
        //env.getCheckpointConfig().setMaxConcurrentCheckpoints(2); //共存的有几个Checkpoint
        //env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, 5)); //总共会尝试3次重启、每隔5秒尝试一次(固定匹配重启)

        //1.2 设置状态后端(本地的内存级别的)
        //env.setStateBackend(new HashMapStateBackend());
        //env.getCheckpointConfig().setCheckpointStorage("hdfs://hadoop102:8082/211126/ck");
        //System.setProperty("HADOOP_USER_NAME", "atguigu");

        //TODO 2、读取kafka 页面日志主题创建流
        String topic = "dwd_traffic_page_log";
        String groupId = "unique_visitor_detail";
        DataStreamSource<String> kafkaDS = env.addSource(MyKafkaUtil.getFlinkKafkaConsumer(topic, groupId));

        //TODO 3、过滤掉上一条页面不为null的数据并将每行数据转换为JSON对象
        SingleOutputStreamOperator<JSONObject> jsonDS = kafkaDS.flatMap(new FlatMapFunction<String, JSONObject>() {
            @Override
            public void flatMap(String value, Collector<JSONObject> out) throws Exception {

                //只取等于null的
                try {
                    JSONObject jsonObject = JSONObject.parseObject(value);
                    //获取上一跳页面ID
                    String lastPageId = jsonObject.getJSONObject("page").getString("last_page_id");
                    if (lastPageId == null) {
                        out.collect(jsonObject);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println(value);
                }
            }
        });

        //TODO 4、按照Mid分组
        KeyedStream<JSONObject, String> keyedStream = jsonDS.keyBy(json -> json.getJSONObject("common").getString("mid"));

        //TODO 5、使用状态编程实现按照Mid的去重
        SingleOutputStreamOperator<JSONObject> uvDS = keyedStream.filter(new RichFilterFunction<JSONObject>() {

            //状态存日期
            private ValueState<String> lastVisitState;

            @Override
            public void open(Configuration parameters) throws Exception {

                //new 已一个状态描述器
                ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<String>("last-visit", String.class);

                //设置状态的TTL-存活时间数
                StateTtlConfig ttlConfig = new StateTtlConfig.Builder(Time.days(1))
                        //更新状态的时候也可以更新过期时间
                        .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
                        .build();

                stateDescriptor.enableTimeToLive(ttlConfig);

                lastVisitState = getRuntimeContext().getState(stateDescriptor);
            }

            /**
             * 状态记录mid末次登陆日期。如果末次登陆日期为null或者不是今日, 则本次访问是该mid当日首次访问,保留数据,
             * 将末次登陆日期更新为当日。否则不是当日首次访问,丢弃数据.
             * @param value
             * @return
             * @throws Exception
             */
            @Override
            public boolean filter(JSONObject value) throws Exception {
                //获取状态数据&当前数据中的时间戳并转换为日期
                String lastDate = lastVisitState.value();

                Long ts = value.getLong("ts");
                String curDate = DateFormatUtil.toDate(ts);

                if (lastDate == null || !lastDate.equals(curDate)) {
                    lastVisitState.update(curDate);
                    return true;
                } else {
                    return false;
                }

            }
        });

        //TODO 6、将数据写到kafka
        String targetTopic = "dwd_traffic_unique_visitor_detail";
        uvDS.map(JSONAware::toJSONString)
                .addSink(MyKafkaUtil.getFlinkKafkaProducer(targetTopic));

        //TODO 7、启动任务苏
        env.execute("DwdTrafficUniqueVisitorDetail");
    }
}

你可能感兴趣的:(数据仓库,hadoop,scala,kafka)