Flink实时数仓项目—DWS层设计与实现

本文详细介绍了使用Flink构建实时数仓的DWS层设计与实现,涵盖访客主题和商品主题宽表的构建,包括需求梳理、表设计、实现思路及代码实现,旨在实现轻度聚合和主题数据管理。


前言

在前面通过使用分流等方法,把数据拆分成了独立的Kafka Topic,接下来我们就要根据需求得出要计算哪些指标项。我们把指标以主题宽表的形式输出就是DWS层要做的事情。


一、需求梳理

1.需求梳理

项目需求整理如下:
在这里插入图片描述

2.DWS层定位

1)轻度聚合。因为DWS层要应对很多事实查询,如果是完全的明细那么查询的压力是非常大的。
2)将更多的实时数据以主题的方式组合起来便于管理,同时也能减少维度查询的次数。

二、DWS层—访客主题宽表的实现

1.访客主题的需求

在这里插入图片描述

2.访客主题宽表的设计

DWS层中一张主题宽表包括两部分内容:维度数据+度量值(事实数据)
维度数据:维度数据就是数据里面的一些维度信息,这里就指的是日志数据里面的渠道、地区、版本、新老用户等这些数据
度量值:度量值就是上面的需求指标,这里就是PV、UV、跳出次数(这里不是要算出具体结果的,只是轻度聚合)、进入页面次数、连续访问时长

3.实现思路

在观察统计出来的需求表,一些需求指标需要的是DWD层的数据,一些需求指标需要的是DWM层的数据,所以对应了好几个Topic主题,我们要从不同的主题中读取数据,然后把读取到的数据转化为定义的主题宽表的对象,然后再UNION起来。

4.代码实现

1)首先,我们要建一个访客主题的宽表的实体类,用来保存结果。我们需要渠道、地区、新老用户标识、版本这四个维度(根据需求来,其余维度可选),其次,我们的度量值是独立访客数、页面访问数、首次进入页面次数、跳出页面次数、访问页面时间这几个度量值,然后因为是开窗统计,所以为了区分是哪个窗口,要加上窗口开始时间和结束时间。最后,开窗需要事件时间语义,那么就需要提取时间戳,所以要一个时间戳字段,实体类如下:

@Data
@AllArgsConstructor
public class VisitorStats {
    //统计开始时间,为了区分是哪个窗口
    private String stt;
    //统计结束时间
    private String edt;
    //维度:版本
    private String vc;
    //维度:渠道
    private String ch;
    //维度:地区
    private String ar;
    //维度:新老用户标识
    private String is_new;
    //度量:独立访客数
    private Long uv_ct=0L;
    //度量:页面访问数
    private Long pv_ct=0L;
    //度量: 进入次数
    private Long sv_ct=0L;
    //度量: 跳出次数
    private Long uj_ct=0L;
    //度量: 持续访问时间
    private Long dur_sum=0L;
    //统计时间
    private Long ts;
}

2)我们根据需求可以知道,总共需要Kafka的dwd_page_log、dwm_unique_visit、dwm_user_jump_detail三个主题的数据,所以,先从这些主题中读取相应的数据,代码如下:

    //2、分别从对应的主题中读取需要的数据
    String pageViewSourceTopic = "dwd_page_log";
    String uniqueVisitSourceTopic = "dwm_unique_visit";
    String userJumpDetailSourceTopic = "dwm_user_jump_detail";
    String groupId = "visitor_stats_app";

    DataStreamSource<String> pageViewDS = env.addSource(MyKafkaUtil.getKafkaSource(pageViewSourceTopic, groupId));
    DataStreamSource<String> uniqueVisitDS = env.addSource(MyKafkaUtil.getKafkaSource(uniqueVisitSourceTopic, groupId));
    DataStreamSource<String> userJumpDS = env.addSource(MyKafkaUtil.getKafkaSource(userJumpDetailSourceTopic, groupId));

3)读取到相应的数据后,拿dwm_user_jump_detail这个主题来说,这个主题存放的是用户跳出页面的次数,它只是用来求这部分的数据的,所以它求的是VisitorStats实体类中的uj_ct这个变量值;dwm_unique_visit这个主题里是只求uv_ct这个变量值的;dwd_page_log这个主题是最全的页面日志,用来求页面总访问次数、页面浏览时间和第一次进入页面的次数的。
**可以看出,这三个流分别求的是实体类中不同的字段,所以我们可以先将它们转化为实体类,将自己能够求的值设置为1或别的,然后将三个流union起来,再进行聚合操作,就可以得到在一个窗口内的一个实体类,这个实体类里面的变量是最终的和。**代码如下:

    //3、将数据的格式转换成相同的主题宽表对象格式
    //处理PV数据
    SingleOutputStreamOperator<VisitorStats> visitorStatsWithPvDS = pageViewDS.map(data -> {
        //页面日志数据可以统计出PV、进入页面数、连续访问时长
        JSONObject jsonObject = JSON.parseObject(data);
        //获取维度信息
        JSONObject common = jsonObject.getJSONObject("common");
        //获取页面信息
        JSONObject page = jsonObject.getJSONObject("page");
        //获取上个页面id
        String last_page_id = page.getString("last_page_id");
        //判断上个页面id是否为null
        Long sv=0L;
        if(last_page_id==null || last_page_id.length()==0)
            sv=1L;
        return new VisitorStats("", "",
                common.getString("vc"),
                common.getString("ch"),
                common.getString("ar"),
                common.getString("is_new"),
                0L, 1L, sv, 0L, page.getLong("during_time"),
                jsonObject.getLong("ts"));
    });

    //处理UV数据
    SingleOutputStreamOperator<VisitorStats> visitorStatsWithUvDS = uniqueVisitDS.map(data -> {
        //UV数据可以统计出uv数据
        JSONObject jsonObject = JSON.parseObject(data);
        //获取维度信息
        JSONObject common = jsonObject.getJSONObject("common");
        return new VisitorStats("", "",
                common.getString("vc"),
                common.getString("ch"),
                common.getString("ar"),
                common.getString("is_new"),
                1L, 0L, 0L, 0L, 0L,
                jsonObject.getLong("ts"));
    });

    //处理UJ数据
    SingleOutputStreamOperator<VisitorStats> visitorStatsWithUjDS = userJumpDS.map(data -> {
        //UJ数据可以统计出跳出页面的次数
        JSONObject jsonObject = JSON.parseObject(data);
        //获取维度信息
        JSONObject common = jsonObject.getJSONObject("common");
        return new VisitorStats("", "",
                common.getString("vc"),
                common.getString("ch"),
                common.getString("ar"),
                common.getString("is_new"),
                0L, 0L, 0L, 1L, 0L,
                jsonObject.getLong("ts"));
    });


    //4、将几个流Union起来
    DataStream<VisitorStats> unionDS = visitorStatsWithPvDS.union(visitorStatsWithUvDS, visitorStatsWithUjDS);

4)经过上面的步骤得到了union之后的流,我们要开窗做计算,所以需要提取水位线生成时间戳,然后开窗计算。因为这里需要提取窗口的开始时间和结束时间,所以可以选择process或者增量函数+全窗口函数。这里选择增量函数+全窗口函数的方法在增量函数中进行规约计算,然后将结果发送到全窗口函数中进行加工,提取窗口开始时间和结束时间,再向下游进行传输。代码如下:

    //6、按照维度信息进行分组
    KeyedStream<VisitorStats, Tuple4<String, String, String, String>> keyedStream = visitorStatsWithWMDS.keyBy(data -> Tuple4.of(data.getVc(), data.getCh(), data.getAr(), data.getIs_new()));

    //7、开窗聚合,10s的滚动窗口
    SingleOutputStreamOperator<VisitorStats> resultDS = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(10)))
            .reduce(new ReduceFunction<VisitorStats>() {
                @Override
                public VisitorStats reduce(VisitorStats visitorStats, VisitorStats t1) throws Exception {
                    return new VisitorStats("", "",
                            visitorStats.getVc(),
                            visitorStats.getCh(),
                            visitorStats.getAr(),
                            visitorStats.getIs_new(),
                            visitorStats.getUv_ct() + t1.getUv_ct(),
                            visitorStats.getPv_ct() + t1.getPv_ct(),
                            visitorStats.getSv_ct() + t1.getSv_ct(),
                            visitorStats.getUj_ct() + t1.getUj_ct(),
                            visitorStats.getDur_sum() + t1.getDur_sum(),
                            visitorStats.getTs());
                }
            }, new WindowFunction<VisitorStats, VisitorStats, Tuple4<String, String, String, String>, TimeWindow>() {
                @Override
                public void apply(Tuple4<String, String, String, String> stringStringStringStringTuple4, TimeWindow timeWindow, Iterable<VisitorStats> iterable, Collector<VisitorStats> collector) throws Exception {
                    //获取reduce聚合的结果
                    VisitorStats next = iterable.iterator().next();

                    //包装结果,将窗口信息填写到对象中
                    long start = timeWindow.getStart();
                    long end = timeWindow.getEnd();

                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

                    next.setStt(simpleDateFormat.format(start));
                    next.setEdt(simpleDateFormat.format(end));

                    collector.collect(next);
                }
            });

5)在计算完一个窗口内的数据后,将结果发送到ClickHouse中,使用JabcSink,代码如下:

    //8、将结果写入ClickHouse
    resultDS.addSink(ClickHouseUtil.getCHVisitorStatsSink("insert into visitor_stats_2022 values(?,?,?,?,?,?,?,?,?,?,?,?)"));
    public static SinkFunction getCHVisitorStatsSink(String sql){
        return JdbcSink.<VisitorStats>sink(sql,
                ((preparedStatement, visitorStats) -> {
                    preparedStatement.setString(1, visitorStats.getStt());
                    preparedStatement.setString(2, visitorStats.getEdt());
                    preparedStatement.setString(3, visitorStats.getVc());
                    preparedStatement.setString(4, visitorStats.getCh());
                    preparedStatement.setString(5, visitorStats.getAr());
                    preparedStatement.setString(6, visitorStats.getIs_new());
                    preparedStatement.setLong(7,visitorStats.getUv_ct());
                    preparedStatement.setLong(8,visitorStats.getPv_ct());
                    preparedStatement.setLong(9,visitorStats.getSv_ct());
                    preparedStatement.setLong(10,visitorStats.getUj_ct());
                    preparedStatement.setLong(11,visitorStats.getDur_sum());
                    preparedStatement.setLong(12,visitorStats.getTs());
                }),
                JdbcExecutionOptions.builder()
                        .withBatchSize(5)
                        .withBatchIntervalMs(200)
                        .withMaxRetries(5)
                        .build(),
                new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
                        .withUrl(GmallConfig.CLICKHOUSE_URL)
                        .withDriverName(GmallConfig.CLICKHOUSE_DRIVER)
                        .build());
    }

三、DWS层—商品主题宽表的实现

1.商品主题需求

在这里插入图片描述

2.商品主题宽表的设计

2.1 维度字段

有关商品的维度包含以下部分:sku编号、sku名称、sku单价、spu编号、spu名称、tm名称、tm编号、category3编号、category3名称
共计9个维度字段。

2.2 度量值

点击:商品点击次数
曝光:商品曝光次数
收藏:商品收藏次数
加购:商品加购物车次数
下单:商品下单的次数(一个order里对应一次)、商品被下单的总件数、商品被下单的总金额
支付:商品被支付的总金额、商品被支付的订单个数
退款:商品被退款的总金额、商品被退款的订单数量
评价:商品被评论的次数、商品的好评数量
共计13个字段。

2.3 其他字段

用于区分窗口的字段:窗口开始时间、窗口结束时间
用于提取时间戳的字段:时间戳ts
共计3个字段。

3.商品主题宽表实体类

这里使用了构造者模式,方便之处在于创建一个对象的时候,可以只对其中部分字段赋值,其余字段使用默认值。
实体类创建如下:

@Data
@Builder
public class ProductStats {

    String stt;//窗口起始时间
    String edt;	//窗口结束时间
    Long sku_id; //sku 编号
    String sku_name;//sku 名称
    BigDecimal sku_price; //sku 单价
    Long spu_id; //spu 编号
    String spu_name;//spu 名称
    Long tm_id; //品牌编号
    String tm_name;//品牌名称
    Long category3_id;//品类编号
    String category3_name;//品类名称

    @Builder.Default
    Long display_ct = 0L; //曝光数

    @Builder.Default
    Long click_ct = 0L;	//点击数

    @Builder.Default
    Long favor_ct = 0L; //收藏数

    @Builder.Default
    Long cart_ct = 0L;	//添加购物车数

    @Builder.Default
    Long order_sku_num = 0L; //下单商品个数

    @Builder.Default	//下单商品金额
    BigDecimal order_amount = BigDecimal.ZERO;

    @Builder.Default
    Long order_ct = 0L; //订单数

    @Builder.Default	//支付金额
    BigDecimal payment_amount = BigDecimal.ZERO;

    @Builder.Default
    Long paid_order_ct = 0L;	//支付订单数

    @Builder.Default
    Long refund_order_ct = 0L; //退款订单数

    @Builder.Default      //退款金额
    BigDecimal refund_amount = BigDecimal.ZERO;

    @Builder.Default
    Long comment_ct = 0L;//评论订单数

    @Builder.Default
    Long good_comment_ct = 0L; //好评订单数


    Long ts; //统计时间戳
}

4.功能实现

分析需求,我们可以知道:点击和曝光可以从dwd_page_log这个主题算出,收藏可以从dwd_favor_info主题计算出,加购可以从dwd_cart_info主题计算出,下单可以从dwm_order_wide主题算出,支付可以从dwm_payment_wide主题算出,退款可以从dwd_order_refund_info主题算出,评价可以从dwd_comment_info主题算出。
1)所以首先,我们需要从7个流中读取数据,代码如下:

    //2、从7个流中读取数据
    String groupId = "product_stats_app";

    String pageViewSourceTopic = "dwd_page_log";
    String orderWideSourceTopic = "dwm_order_wide";
    String paymentWideSourceTopic = "dwm_payment_wide";
    String cartInfoSourceTopic = "dwd_cart_info";
    String favorInfoSourceTopic = "dwd_favor_info";
    String refundInfoSourceTopic = "dwd_order_refund_info";
    String commentInfoSourceTopic = "dwd_comment_info";

    DataStreamSource<String> pageViewSource = env.addSource(MyKafkaUtil.getKafkaSource(pageViewSourceTopic, groupId));
    DataStreamSource<String> orderWideSource = env.addSource(MyKafkaUtil.getKafkaSource(orderWideSourceTopic, groupId));
    DataStreamSource<String> paymentSource = env.addSource(MyKafkaUtil.getKafkaSource(paymentWideSourceTopic, groupId));
    DataStreamSource<String> cartInfoSource = env.addSource(MyKafkaUtil.getKafkaSource(cartInfoSourceTopic, groupId));
    DataStreamSource<String> favorInfoSource = env.addSource(MyKafkaUtil.getKafkaSource(favorInfoSourceTopic, groupId));
    DataStreamSource<String> refundInfoSource = env.addSource(MyKafkaUtil.getKafkaSource(refundInfoSourceTopic, groupId));
    DataStreamSource<String> commentInfoSource = env.addSource(MyKafkaUtil.getKafkaSource(commentInfoSourceTopic, groupId));

2)将这7个流转化为相同的数据类型,转化的时候分别根据对应的功能进行相应的处理,然后合并成一条流

    //3、将每个流转化为相同的数据类型
    //处理点击和曝光功能
    //因为一个页面日志中包含了多个曝光日志,所以要输出多条数据,要用flatMap
    SingleOutputStreamOperator<ProductStats> productStatsWithClickAndDisplayDS = pageViewSource.flatMap(new FlatMapFunction<String, ProductStats>() {
        @Override
        public void flatMap(String s, Collector<ProductStats> collector) throws Exception {
            //可以获得点击数据和曝光数据
            //先处理点击数据
            JSONObject jsonObject = JSON.parseObject(s);

            //取出page信息和时间戳ts
            JSONObject page = jsonObject.getJSONObject("page");
            String page_id = page.getString("page_id");
            Long ts = jsonObject.getLong("ts");
            //点击数据的特征:当前页面id为good_detail且item_type为sku_id
            if ("good_detail".equals(page_id) && "sku_id".equals(page.getString("item_type"))) {
                collector.collect(ProductStats.builder()
                        .sku_id(page.getLong("item"))
                        .click_ct(1L)  //设置点击次数为1
                        .ts(ts)
                        .build());
            }

            //再处理曝光数据
            //取出曝光数据
            JSONArray displays = jsonObject.getJSONArray("displays");
            if (displays != null && displays.size() > 0) {
                for (int i = 0; i < displays.size(); i++) {
                    //从数组中取出一条条的曝光数据
                    JSONObject displaysJSONObject = displays.getJSONObject(i);
                    //判断是否是曝光的商品
                    if ("sku_id".equals(displaysJSONObject.getString("item_type")))
                        collector.collect(ProductStats.builder()
                                .sku_id(displaysJSONObject.getLong("item"))
                                .display_ct(1L)   //设置曝光次数为1
                                .ts(ts)
                                .build());
                }
            }
        }
    });

    //处理收藏部分功能
    SingleOutputStreamOperator<ProductStats> productStatsWithFavorDS = favorInfoSource.map(data -> {
        JSONObject jsonObject = JSON.parseObject(data);

        //这是数据库里的业务数据,所以来一条就是一条收藏数据
        String create_time = jsonObject.getString("create_time");
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //转化为时间戳
        long ts = simpleDateFormat.parse(create_time).getTime();
        return ProductStats.builder()
                .sku_id(jsonObject.getLong("sku_id"))
                .favor_ct(1L)
                .ts(ts)
                .build();
    });

    //处理加入购物车功能
    SingleOutputStreamOperator<ProductStats> productStatsWithCartDS = cartInfoSource.map(data -> {
        JSONObject jsonObject = JSON.parseObject(data);

        //这是数据库里的业务数据,所以来一条就是一条加购数据
        String create_time = jsonObject.getString("create_time");
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //转化为时间戳
        long ts = simpleDateFormat.parse(create_time).getTime();
        return ProductStats.builder()
                .sku_id(jsonObject.getLong("sku_id"))
                .cart_ct(1L)
                .ts(ts)
                .build();
    });

    //处理下单功能
    SingleOutputStreamOperator<ProductStats> productStatsWithOrderDS = orderWideSource.map(data -> {
        //有对应的实体类,可以转化为相应的实体类对象
        OrderWide orderWide = JSON.parseObject(data, OrderWide.class);

        String create_time = orderWide.getCreate_time();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //转化为时间戳
        long ts = simpleDateFormat.parse(create_time).getTime();

        return ProductStats.builder()
                .sku_id(orderWide.getSku_id())
                .order_sku_num(orderWide.getSku_num())  //商品下单的件数
                .order_ct(1L)   //商品下单次数即订单数
                .order_amount(orderWide.getOrder_price())   //本订单本商品总价格
                .ts(ts)
                .build();
    });

    //处理支付功能
    SingleOutputStreamOperator<ProductStats> productStatsWithPaymentDS = paymentSource.map(data -> {
        PaymentWide paymentWide = JSON.parseObject(data, PaymentWide.class);

        //整合成了支付宽表,里面还是一条数据代表一次支付记录
        String create_time = paymentWide.getPayment_create_time();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //转化为时间戳
        long ts = simpleDateFormat.parse(create_time).getTime();

        return ProductStats.builder()
                .sku_id(paymentWide.getSku_id())
                .paid_order_ct(1L)
                .payment_amount(paymentWide.getOrder_price())
                .ts(ts)
                .build();
    });

    //处理退款数据
    SingleOutputStreamOperator<ProductStats> productStatsWithRefundDS = refundInfoSource.map(data -> {
        JSONObject jsonObject = JSON.parseObject(data);

        //数据库里的数据,一条数据代表一次退款记录
        String create_time = jsonObject.getString("create_time");
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //转化为时间戳
        long ts = simpleDateFormat.parse(create_time).getTime();

        return ProductStats.builder()
                .sku_id(jsonObject.getLong("sku_id"))
                .refund_order_ct(1L)
                .refund_amount(jsonObject.getBigDecimal("refund_amount"))
                .ts(ts)
                .build();
    });

    //处理评论数据
    SingleOutputStreamOperator<ProductStats> productStatsWithCommentDS = commentInfoSource.map(data -> {
        JSONObject jsonObject = JSON.parseObject(data);

        //数据库里的数据,一条数据代表一次评论记录
        String create_time = jsonObject.getString("create_time");
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //转化为时间戳
        long ts = simpleDateFormat.parse(create_time).getTime();

        //判断是否是好评
        Long goodCt = GmallConstant.APPRAISE_GOOD.equals(jsonObject.getString("appraise")) ? 1L : 0L;
        return ProductStats.builder()
                .sku_id(jsonObject.getLong("sku_id"))
                .comment_ct(1L)
                .good_comment_ct(goodCt)
                .ts(ts)
                .build();
    });


    //4、将处理完的流合并为一个流
    DataStream<ProductStats> unionDS = productStatsWithClickAndDisplayDS.union(productStatsWithOrderDS, productStatsWithCartDS,
            productStatsWithPaymentDS, productStatsWithRefundDS, productStatsWithFavorDS, productStatsWithCommentDS);

3)将合并后的流提取时间戳,生成水位线,然后开10秒的滚动窗口进行统计计算,代码如下:

    //5、提取水位线,生成时间戳
    SingleOutputStreamOperator<ProductStats> productStatsWithTsDS = unionDS.assignTimestampsAndWatermarks(WatermarkStrategy.<ProductStats>forBoundedOutOfOrderness(Duration.ofSeconds(2))
            .withTimestampAssigner(new SerializableTimestampAssigner<ProductStats>() {
                @Override
                public long extractTimestamp(ProductStats productStats, long l) {
                    return productStats.getTs();
                }
            }));

    //6、按sku_id分组,然后开窗聚合
    SingleOutputStreamOperator<ProductStats> resultDS = productStatsWithTsDS.keyBy(data -> data.getSku_id())
            .window(TumblingEventTimeWindows.of(Time.seconds(10)))
            .reduce(new ReduceFunction<ProductStats>() {
                @Override
                public ProductStats reduce(ProductStats stats1, ProductStats stats2) throws Exception {
                    //将两部分的值聚合起来
                    stats1.setDisplay_ct(stats1.getDisplay_ct() + stats2.getDisplay_ct());
                    stats1.setClick_ct(stats1.getClick_ct() + stats2.getClick_ct());
                    stats1.setCart_ct(stats1.getCart_ct() + stats2.getCart_ct());
                    stats1.setFavor_ct(stats1.getFavor_ct() + stats2.getFavor_ct());
                    stats1.setOrder_amount(stats1.getOrder_amount().add(stats2.getOrder_amount()));
                    stats1.setOrder_ct(stats1.getOrder_ct() + stats2.getOrder_ct());
                    stats1.setOrder_sku_num(stats1.getOrder_sku_num() +
                            stats2.getOrder_sku_num());
                    stats1.setPayment_amount(stats1.getPayment_amount().add(stats2.getPayment_amount()));
                    stats1.setRefund_order_ct(stats1.getRefund_order_ct() + stats2.getRefund_order_ct());
                    stats1.setRefund_amount(stats1.getRefund_amount().add(stats2.getRefund_amount()));
                    stats1.setPaid_order_ct(stats1.getPaid_order_ct() + stats2.getPaid_order_ct());
                    stats1.setComment_ct(stats1.getComment_ct() + stats2.getComment_ct());
                    stats1.setGood_comment_ct(stats1.getGood_comment_ct() + stats2.getGood_comment_ct());

                    return stats1;
                }
            }, new WindowFunction<ProductStats, ProductStats, Long, TimeWindow>() {
                @Override
                public void apply(Long aLong, TimeWindow timeWindow, Iterable<ProductStats> iterable, Collector<ProductStats> collector) throws Exception {
                    //添加窗口时间信息
                    long start = timeWindow.getStart();
                    long end = timeWindow.getEnd();
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

                    ProductStats next = iterable.iterator().next();
                    next.setStt(simpleDateFormat.format(start));
                    next.setEdt(simpleDateFormat.format(end));
                    next.setTs(new Date().getTime());

                    collector.collect(next);
                }
            });

4)因为这7条流中只有部分流包含所有的维度信息,但是它们都含有一个相同的字段sku_id,所以最终union后计算的结果中是不含有维度信息的,它只有sku_id这一个字段,我们要通过这一个字段关联维度信息,通过sku_id得到sku_name、sku_price、spu_id、tm_id、category3_id,然后再依次进行关联这里面剩余的三个维度信息。代码如下:

    //7、将数据写入ClickHouse
    resultDS.addSink(ClickHouseUtil.getProductStatsSink("insert into product_stats_2022 values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"));
	//7、关联维度信息,根据sku_id查询
	//异步查询维度信息
	
	//7.1 关联SKU维度信息
	SingleOutputStreamOperator<ProductStats> productStatsWithSkuDstream  = AsyncDataStream.unorderedWait(resultDS,
	        new DimAsyncFunction<ProductStats>("DIM_SKU_INFO") {
	            @Override
	            protected void joinData(JSONObject singleDimInfo, ProductStats productStats) {
	                //填充关于SKU的维度信息
	                productStats.setSku_name(singleDimInfo.getString("SKU_NAME"));
	                productStats.setSku_price(singleDimInfo.getBigDecimal("PRICE"));
	                productStats.setSpu_id(singleDimInfo.getLong("SPU_ID"));
	                productStats.setTm_id(singleDimInfo.getLong("TM_ID"));
	                productStats.setCategory3_id(singleDimInfo.getLong("CATEGORY3_ID"));
	            }
	
	            @Override
	            public String getKey(ProductStats productStats) {
	                return productStats.getSku_id().toString();
	            }
	        }, 60, TimeUnit.SECONDS);
	
	//7.2 关联SPU维度信息
	SingleOutputStreamOperator<ProductStats> productStatsWithSpuDStream  = AsyncDataStream.unorderedWait(productStatsWithSkuDstream ,
	        new DimAsyncFunction<ProductStats>("DIM_SPU_INFO") {
	
	            @Override
	            protected void joinData(JSONObject singleDimInfo, ProductStats productStats) {
	                productStats.setSpu_name(singleDimInfo.getString("SPU_NAME"));
	            }
	
	            @Override
	            public String getKey(ProductStats productStats) {
	                return productStats.getSpu_id().toString();
	            }
	        }, 60, TimeUnit.SECONDS);
	
	//7.3 关联品类维度信息
	SingleOutputStreamOperator<ProductStats> productStatsWithCategory3Dstream  = AsyncDataStream.unorderedWait(productStatsWithSpuDStream,
	        new DimAsyncFunction<ProductStats>("DIM_BASE_CATEGORY3") {
	
	            @Override
	            protected void joinData(JSONObject singleDimInfo, ProductStats productStats) {
	                productStats.setCategory3_name(singleDimInfo.getString("CATEGORY3_NAME"));
	            }
	
	            @Override
	            public String getKey(ProductStats productStats) {
	                return productStats.getCategory3_id().toString();
	            }
	        }, 60, TimeUnit.SECONDS);
	
	//7.4 关联品牌维度信息
	SingleOutputStreamOperator<ProductStats> productStatsWithTmDstream  = AsyncDataStream.unorderedWait(productStatsWithCategory3Dstream,
	        new DimAsyncFunction<ProductStats>("DIM_BASE_TRADEMARK") {
	            @Override
	            protected void joinData(JSONObject singleDimInfo, ProductStats productStats) {
	                productStats.setTm_name(singleDimInfo.getString("TM_NAME"));
	            }
	
	            @Override
	            public String getKey(ProductStats productStats) {
	                return productStats.getTm_id().toString();
	            }
	        },
	        60, TimeUnit.SECONDS);
	```
	5)将关联后的数据写入到ClickHouse中。代码如下:
	
	```java
	public static SinkFunction<ProductStats> getProductStatsSink(String sql){
	    return JdbcSink.<ProductStats>sink(sql,
	            //通过反射拿到对象里所有字段的值,就不需要遍历了
	            ((preparedStatement, productStats) -> {
	                Field[] declaredFields = productStats.getClass().getDeclaredFields();
	                for (int i = 0; i < declaredFields.length; i++) {
	                    //设置是否可以访问,如果不设置将报错
	                    declaredFields[i].setAccessible(true);
	                    try {
	                        Object o = declaredFields[i].get(productStats);
	                        System.out.println("字段值:" + declaredFields[i].get(productStats));
	                        preparedStatement.setObject(i+1,o);
	                    } catch (IllegalAccessException e) {
	                        e.printStackTrace();
	                    }
	                }
	            }),JdbcExecutionOptions.builder()
	                    .withBatchSize(5)
	                    .withBatchIntervalMs(200)
	                    .withMaxRetries(5)
	                    .build(),
	            new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
	                    .withUrl(GmallConfig.CLICKHOUSE_URL)
	                    .withDriverName(GmallConfig.CLICKHOUSE_DRIVER)
	                    .build());
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值