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.<