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());
}

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

被折叠的 条评论
为什么被折叠?



