Flink实时数仓项目—ODS层日志数据到DWD层
Flink实时数仓项目—ODS层日志数据到DWD层
-
前言
-
一、日志数据需要做的处理
-
- 1.识别新老用户
- 2.日志数据的处理
- 3.发送数据到Kafka
-
二、功能实现
-
- 1.读取Kafka数据并转换数据格式
- 2.识别新老用户
- 3.日志数据分流
- 4.分流后的数据写入Kafka对应主题
前言
前面已经将日志数据和业务数据采集到了Kafka中,Kafka中的ods_xx主题就作为了实时数仓的ODS层。
行为日志分为三类,页面日志、启动日志和曝光日志,这三类日志的格式不一样,我们需要分别进行处理,然后将处理完的数据再写入到Kafka中,作为日志的DWD层。
一、日志数据需要做的处理
1.识别新老用户
本身客户端业务有新老用户的标识,但是不够准确(比如有些用户卸载了软件,然后再次下载了回来,is_new仍然为1),这就需要实时计算再次确认,如下图:

2.日志数据的处理
前言中提到日志数据分为三类,页面日志、启动日志和曝光日志。我们要将这三种不同的日志区分开来,分别发送到不同的Kafka的主题中。
Flink中可以使用测输出流这个方法将不同类型的数据放到不同的流里,这里设计将页面日志输出到主流,启动日志输出到启动日志的测输出流,曝光日志输出到曝光日志的测输出流。
3.发送数据到Kafka
在日志数据拆分之后,分别将不同类型的日志数据发送到不同的Kafka的主题中。
二、功能实现
1.读取Kafka数据并转换数据格式
首先,要从Kafka的ods_base_log主题中读取日志数据,但是读取出来的数据是String类型的,我们要将数据转化为jsonObject类型的,然后才方便从里面取出对应的数据。
因为是Flink从Kafka中读取数据,所以我们可以将获取KafkaConsumer也封装成一个方法,MyKafkaUtil方法如下:
public class MyKafkaUtil {
//Kafka链接地址
private static String KAFKA_SERVE="hadoop102:9092,hadoop103:9092,hadoop104:9092";
//Kafka消费者的配置
static Properties sinkProperties=new Properties();
//Kafka生产者的配置
static Properties sourceProperties=new Properties();
static{
sinkProperties.setProperty("bootstrap.servers",KAFKA_SERVE);
sourceProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,KAFKA_SERVE);
}
public static FlinkKafkaProducer<String> getKafkaSink(String topic){
return new FlinkKafkaProducer<String>(topic,new SimpleStringSchema(),sinkProperties);
}
public static FlinkKafkaConsumer<String> getKafkaSource(String topic,String groupId){
sourceProperties.put(ConsumerConfig.GROUP_ID_CONFIG,groupId);
return new FlinkKafkaConsumer<String>(topic,new SimpleStringSchema(),sourceProperties);
}
}
AI写代码java
运行

接下来,编写主程序,消费Kafka里的数据(注意:可能会出现异常 ):
//2、消费ods_vase_log的数据
String sourceTopic="ods_base_log";
String groupId="base_log_app";
DataStreamSource<String> stringDataStreamSource = env.addSource(MyKafkaUtil.getKafkaSource(sourceTopic, groupId));
//3、将每行数据转化为JSON对象
//这里把string转化为jsonObject对象可能会出现异常情况
//即脏数据的情况,出现异常会导致整个作业出错,所以不能用map
//同时我们有时需要把错误的数据也保留下来,因此可以用处理函数,将错误数据保存到错误的错输出流里
//SingleOutputStreamOperator<JSONObject> map = stringDataStreamSource.map(str -> JSON.parseObject(str));
OutputTag<String> dirtyOutputTag = new OutputTag<>("Dirty"){};
SingleOutputStreamOperator<JSONObject> jsonObjectDataStream = stringDataStreamSource.process(new ProcessFunction<String, JSONObject>() {
@Override
public void processElement(String s, ProcessFunction<String, JSONObject>.Context context, Collector<JSONObject> collector) throws Exception {
try {
//将获取到的数据转化为json对象
JSONObject jsonObject = JSON.parseObject(s);
//将正确格式的数据正常输出
collector.collect(jsonObject);
} catch (Exception e) {
//将错误格式的数据输出到脏数据的测输出流里
context.output(dirtyOutputTag, s);
}
}
});
AI写代码java
运行

2.识别新老用户
日志数据中可能存在一些不正确的数据,比如一些用户之前使用过该app,但是后来卸载了,再后来又下载了又登录了,那么我们就不能只根据is_new这个字段来判断是否是新用户,我们还要根据设备id来判断新老用户。
is_new为0的表示是老用户,这部分数据肯定是没有问题的;is_new为1的表示是新用户,我们要根据mid是否重复出现来判断新老用户,因此需要一个状态来保存mid是否出现过,因此使用了状态编程,对mid进行keyBy分组,然后定义一个valueState,来标识mid是否出现过。
代码如下:
//4、识别新老用户
SingleOutputStreamOperator<JSONObject> mapDS = jsonObjectDataStream.keyBy(data -> data.getJSONObject("common").getString("mid"))
.map(new RichMapFunction<JSONObject, JSONObject>() {
//对每个mid定义一个状态,用来标记是否曾经登录过
private ValueState<String> valueState;
@Override
public void open(Configuration parameters) throws Exception {
//状态初始化
getRuntimeContext().getState(new ValueStateDescriptor<String>("value-state", String.class));
}
@Override
public void close() throws Exception {
super.close();
}
@Override
public JSONObject map(JSONObject jsonObject) throws Exception {
//先判断数据里的is_new是否为1,为1才要处理
String isNew = jsonObject.getJSONObject("common").getString("is_new");
if ("1".equals(isNew)) {
//获取状态,判断是否曾经登录过
String value = valueState.value();
if (value != null) {
//不为null,代表曾经登录过,那么就把isNew修改为0
jsonObject.getJSONObject("common").put("is_new", "0");
} else {
//为null,就把整个状态随便赋予一个值,代表已经登录了
valueState.update("1");
}
}
return jsonObject;
}
});
AI写代码java
运行

3.日志数据分流
根据日志数据内容,将日志数据分为三类,页面日志、启动日志和曝光日志,要使用测输出流,所以要使用处理函数。(注意:曝光日志也包含页面信息,因此也要往页面日志流里写一份;曝光数据里面有多条曝光信息,可以拆分开写入曝光数据的流里)
//5、分流:页面日志->主流,启动日志->测输出流,曝光日志->测输出流
//定义对应的测输出流
OutputTag<String> startTag = new OutputTag<String>("start") {
};
OutputTag<String> displayTag = new OutputTag<String>("display") {
};
SingleOutputStreamOperator<String> pageDataStream = mapDS.process(new ProcessFunction<JSONObject, String>() {
@Override
public void processElement(JSONObject jsonObject, ProcessFunction<JSONObject, String>.Context context, Collector<String> collector) throws Exception {
//带有start字段且不为空的为启动日志
JSONObject start = jsonObject.getJSONObject("start");
if (start != null && start.size() > 0) {
context.output(startTag, jsonObject.toJSONString());
} else {
//曝光数据也包含了页面信息,因此不管是曝光数据还是页面日志数据都要往页面日志这个流里面写一份数据
collector.collect(jsonObject.toString());
//取出曝光数据
JSONArray displays = jsonObject.getJSONArray("displays");
if (displays != null && displays.size() > 0) {
//如果是曝光数据,那么就将曝光数据进行拆分,然后依次写入曝光数据的流中
//获取页面ID
String pageId = jsonObject.getJSONObject("page").getString("page_id");
for (int i = 0; i < displays.size(); i++) {
JSONObject display = displays.getJSONObject(i);
//对拆分出来的每一条曝光数据,添加页面ID
display.put("page_id", pageId);
//将曝光数据写入到测输出流
context.output(displayTag, display.toString());
}
}
}
}
});
AI写代码java
运行

4.分流后的数据写入Kafka对应主题
//6、提取测输出流
DataStream<String> startOutput = pageDataStream.getSideOutput(startTag);
DataStream<String> displayOutput = pageDataStream.getSideOutput(displayTag);
//7、将对应数据写入到Kafka对应的主题中
pageDataStream.addSink(MyKafkaUtil.getKafkaSink("dwd_page_log"));
startOutput.addSink(MyKafkaUtil.getKafkaSink("dwd_start_log"));
displayOutput.addSink(MyKafkaUtil.getKafkaSink("dwd_display_log"));
AI写代码java
运行
