使用Flink-CEP标记网页跳出用户代码开发
一、需求说明:
对页面日志数据进行ETL,对跳出用户进行标记后输出到Kafka。
跳出用户定义:
条件1:不是从其他页面跳转过来的页面,是一个首次访问页面。日志数据表现为不存在last_page_id字段。
条件2:距离首次访问结束后10秒内,没有对其他的页面再进行访问。
ps:该需求一般为实时项目中对kafka日志数据进行消费后处理,后续输出到kafka计算页面跳出率用于运营分析使用,该文重点在于代码部分的处理,因此测试数据简化输入及输出。
- 测试数据说明:
//mid对应设备,page_id对应当前访问页面,ts访问时间戳,last_page_id指来源页面(有该字段代表从其他页面跳转过来)
{common:{mid:101},page:{page_id:home},ts:10000} ,
{common:{mid:102},page:{page_id:home},ts:12000},
{common:{mid:102},page:{page_id:good_list,last_page_id:home},ts:15000} ,
{common:{mid:102},page:{page_id:good_list,last_page_id:detail},ts:300000}
//mid101访问home之后没有访问其他页面,定义为跳出用户,
//mid102在10秒内访问了其他用户定义为非跳出用户,最后的结果输出应该如下:
{"common":{"mid":"101"},"page":{"page_id":"home"},"ts":10000}
二、CEP简单说明:
Flink CEP是一个基于Flink的复杂事件处理库,可以从多个数据流中发现复杂事件,CEP API的核心是Pattern(模式) API,它允许快速定义复杂的事件模式。每个模式包含多个阶段(stage)或者也可称为状态(state)。从一个状态切换到另一个状态,用户可以指定条件,这些条件可以作用在邻近的事件或独立事件上。
三、代码部分:
//新建Maven项目
package com.flink.cep;
import com.alibaba.fastjson.JSONObject;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternFlatSelectFunction;
import org.apache.flink.cep.PatternFlatTimeoutFunction;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
import org.apache.flink.streaming.api.datastream.DataStream;
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.windowing.time.Time;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import java.util.List;
import java.util.Map;
/**
* @author: Rango
* @create: 2021-04-18 13:26
* @description: CEP测试小案例 基于Flink 1.12
**/
public class FlinkCepTest {
public static void main(String[] args) throws Exception {
//TODO 1.建立环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//TODO 2.创建测试数据
DataStream<String> dataStream = env
.fromElements(
"{\"common\":{\"mid\":\"101\"},\"page\":{\"page_id\":\"home\"},\"ts\":10000} ",
"{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"home\"},\"ts\":12000}",
"{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"good_list\",\"last_page_id\":" +
"\"home\"},\"ts\":15000} ",
"{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"good_list\",\"last_page_id\":" +
"\"detail\"},\"ts\":30000}"
);
//TODO 3.转换数据为JSON格式,这里使用阿里的fastjson
SingleOutputStreamOperator<JSONObject> jsonObjDS = dataStream.map(jsonStr -> JSONObject.parseObject(jsonStr));
//TODO 4.指定事件时间字段
//ps:1.12以前版本需要指定事件时间语义env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
SingleOutputStreamOperator<JSONObject> jsonObjWithTSDS = jsonObjDS.assignTimestampsAndWatermarks(
WatermarkStrategy.<JSONObject>forMonotonousTimestamps().withTimestampAssigner(
new SerializableTimestampAssigner<JSONObject>() {
@Override
public long extractTimestamp(JSONObject element, long recordTimestamp) {
return element.getLong("ts");
}
}
));
//TODO 5.按照mid进行分组
KeyedStream<JSONObject, String> keyMyMids = jsonObjWithTSDS.keyBy(
jsonObj -> jsonObj.getJSONObject("common").getString("mid")
);
//TODO 6.配置CEP表达式
Pattern<JSONObject,JSONObject> pattern = Pattern.<JSONObject>begin("first")
.where(
//条件1:不是从其他页面跳转过来的页面,是一个首次范围页面,也就是没有last_page_id
new SimpleCondition<JSONObject>() {
@Override
public boolean filter(JSONObject jsonObj) throws Exception {
//获取last_page_id
String lastPageId = jsonObj.getJSONObject("page").getString("last_page_id");
//判断是lastPageId是否为空
if (lastPageId == null || lastPageId.length() == 0) {
return true; //返回true代表符合筛选条件
}
return false;
}
}
).next("next")
.where(
//条件2:首次访问结束后10秒内,没有对其他页面再进行访问
new SimpleCondition<JSONObject>() {
@Override
public boolean filter(JSONObject jsonObj) throws Exception {
//获取pageid
String pageId = jsonObj.getJSONObject("page").getString("page_id");
//判断pageId是否为空
if (pageId != null || pageId.length() > 0) {
return true;
}
return false;
}
}
).within(Time.milliseconds(10000));//10秒
//TODO 7.根据CEP表达式筛选流
PatternStream<JSONObject> patternStream = CEP.pattern(keyMyMids, pattern);
//TODO 8.从筛选之后的流中,提取数据,将超时数据放到侧输出流中
//侧输出流先定义标签
OutputTag<String> timeoutTag = new OutputTag<String>("timeout"){};
//3个参数,标签,超时数据处理类,未超时数据处理类,使用匿名内部类方式
SingleOutputStreamOperator<String> firstDS = patternStream.flatSelect(
timeoutTag,
new PatternFlatTimeoutFunction<JSONObject, String>() {
@Override
public void timeout(Map<String, List<JSONObject>> map, long l, Collector<String> collector) throws Exception {
//获取所有符合first的json对象
//说明:该类是处理超时数据,如果超时则不会存在cep表达式里next的数据,也就是只需要通过first就可以获取所需mid
List<JSONObject> jsonObjectList = map.get("first");
//该方法数据会被参数1标记,因此使用增强打印输出集合里面各数据即可
for (JSONObject jsonObject : jsonObjectList) {
collector.collect(jsonObject.toJSONString());
}
}
},
new PatternFlatSelectFunction<JSONObject, String>() {
@Override
public void flatSelect(Map<String, List<JSONObject>> map, Collector<String> collector) throws Exception {
//按需求,未超时部分数据无需处理
}
}
);
// TODO 9.从侧输出流获取超时数据
DataStream<String> jumpDS = firstDS.getSideOutput(timeoutTag);
jumpDS.print(">>>>>>>");
//后续如果需要发送到消息队列,配置kafka相关依赖,通过addSink方法输出到kafka即可
env.execute();
}
}
相关依赖如下:
<properties>
<flink.version>1.12.0</flink.version>
<scala.version>2.12</scala.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
<!--Flink默认使用的是slf4j记录日志,相当于一个日志的接口,我们这里使用log4j作为具体的日志实现-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.14.0</version>
</dependency>
</dependencies>
结果输出如下:
学习交流,有任何问题还请随时评论指出交流。