「这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战」
一、概述
在实际生产中,随着数据的实时性要求越来越高,实时数据的量也在不断膨胀,在某些业务场景中需要根据连续的实时数据,发现其中有价值的那些事件
CEP(Complex Event Processing)复杂事件处理, Flink CEP 是在 Flink 中实现的复杂时间处理(CEP)库。
处理事件的规则, 被叫做“模式”(
Pattern),Flink CEP提供了Pattern API, 用于对输入流数据进行复杂事件规则定义, 用来提取符合规则的事件序列。
CEP一篇论文“Efficient Pattern Matching over Event Streams”。
Flink CEP 应用场景: CEP 在互联网各个行业都有应用, 例如金融、物流、电商、智能交通、物联网行业等行业:
-
实时监控:
- 在网站的访问日志中寻找那些使用脚本或者工具“爆破”登录的用户;
- 在大量的订单交易中发现那些虚假交易(超时未支付)或发现交易活跃用户;
- 在快递运输中发现那些滞留很久没有签收的包裹等。
-
风险控制:
比如金融行业可以用来进行风险控制和欺诈识别, 从交易信息中寻找那些可能存在的危险交易和非法交易。
-
营销广告:
跟踪用户的实时行为, 指定对应的推广策略进行推送, 提高广告的转化率。
(1)NFA : 非确定有限自动机
Flink CEP 在运行时会将用户的逻辑转化成这样的一个 NFA Graph (nfa对象)。
所以有限状态机的工作过程, 就是从开始状态,根据不同的输入, 自动进行状态转换的过程。
举个栗子,检测二进制数是否含有偶数个 0 ,如下图所示:
从图上可以看出, 输入只有 1 和 0 两种。
从 S1 状态开始, 只有输入 0 才会转换到 S2 状态, 同样 S2 状态下只有输入 0 才会转换到 S1。所以, 二进制数输入完毕, 如果满足最终状态, 也就是最后停在 S1 状态, 那么输入的二进制数就含有偶数个 0。
(2)Pattern API
Pattern API 大致分为三种: 基本模式、组合模式,、模式分组。
1)基本模式
基本模式(Individual Pattern)分为两类:单例模式(Singleton)和 循环模式(Looping)
-
单例模式(
Singleton):只读入单个事件单例模式可以通过量词转化成循环模式,如
times、oneOrMore。 -
循环模式(Looping):读入多个事件
- 量词
API
API | 说明 | 案例 |
|---|---|---|
times() | 模式发生次数 | pattern.times(2, 4): 模式发生 2、3、4次 |
timesOrMore、oneOrMore | 模式发生大于等于 N 次 | pattern.timesOrMore(): 模式发生大于等于 2次 |
optional() | 模式可以不匹配 | pattern.times(2).optional(): 模式发生2次或者0次 |
greedy() | 模式发生越多越好 | pattern.times(2).greedy(): 模式发生2次且重复次数越来越多 |
- 条件
API
API | 说明 | 案例 |
|---|---|---|
where() | 模式的条件 | pattern.where(event => event.getId == 1):模式的条件为 id = 1 |
or() | 模式的条件或者 | pattern.where(event => event.getId == 1).or(event => event.getId == 2): 模式的条件为 id=1 或 id=2 |
util() | 模式的条件直至 | pattern.oneOrMore().until(event => event.getId == 1): 模式发生一次或者多次,直至 id=1 |
2)组合模式
基本模式通过不同的连接方式可以组成成更复杂的模式。
Flink CEP 提供如下三种模式:
-
严紧邻模式(
Strict Contigity): 紧邻的两个模式必须按照先后顺序匹配,且都匹配。方法有:
next()、notNext()如图:
-
松紧邻模式(
Relaxed Contigity):忽略两个模式均不匹配的事件,从输入中找到符合两个模式的事件流。方法有:
followBy()、notFollowBy()如图: -
非确定型松紧邻模式(
Non-Deterministic Relaxed Contiguity): 可以忽略某些匹配事件。方法有:
followedByAny()
举个栗子:
输入有:a、c、b1、b2
其中,b1、b2,均为 b事件,只是用来表示顺序。
需求:要匹配出 a b 的模式,即 a 后面有 b。
-
严紧邻模式:匹配结果为 空
-
松紧邻模式:匹配结果为
{a b1} -
非确定型松紧邻模式:匹配结果为
{a b1}和{a b2}
3)模式分组
Flink CEP 引入模式分组机制,以提升模式编程的灵活度。
所谓分组是将 begin、followedBy、followedByAny、next 的序列组合成一个基本模式。
例如,5秒内, 连续登录失败:
Pattern<LoginBean, LoginBean> pattern = Pattern.<LoginBean>begin("start").where(new IterativeCondition<LoginBean>() {
@Override
public boolean filter(LoginBean loginBean, Context<LoginBean> context) {
return loginBean.getState().equals("fail");
}
}).next("next").where(new IterativeCondition<LoginBean>() {
@Override
public boolean filter(LoginBean loginBean, Context<LoginBean> context) {
return loginBean.getState().equals("fail");
}
}).within(Time.seconds(5));
二、实战案例
添加依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep_2.12</artifactId>
<version>1.11.1</version>
</dependency>
编程模板
Flink CEP 的程序结构主要分为两个步骤:
- 定义模式
- 匹配结果
官网中 Flink 提供的案例:
DataStream<Event> input = ...
Pattern<Event, ?> pattern = Pattern.<Event>begin("start").where(
new SimpleCondition<Event>() {
@Override
public boolean filter(Event event) {
return event.getId() == 42;
}
}
).next("middle").subtype(SubEvent.class).where(
new SimpleCondition<SubEvent>() {
@Override
public boolean filter(SubEvent subEvent) {
return subEvent.getVolume() >= 10.0;
}
}
).followedBy("end").where(
new SimpleCondition<Event>() {
@Override
public boolean filter(Event event) {
return event.getName().equals("end");
}
}
);
PatternStream<Event> patternStream = CEP.pattern(input, pattern);
DataStream<Alert> result = patternStream.process(
new PatternProcessFunction<Event, Alert>() {
@Override
public void processMatch(
Map<String, List<Event>> pattern,
Context ctx,
Collector<Alert> out) throws Exception {
out.collect(createAlertFrom(pattern));
}
});
在这个案例中可以看到程序结构分别是:
- 定义一个模式
Pattern:即在所有接收到的事件中匹配那些以id等于 42 的事件,然后匹配volume大于 10.0 的事件,继续匹配一个name等于end的事件; - 匹配模式并且发出报警,根据定义的
pattern在输入流上进行匹配,一旦命中模式,就发出一个报警。
(1)案例一:搜索数据
模拟电商网站用户搜索的数据来作为数据的输入源,然后查找其中重复搜索某一个商品的人。
步骤:
- 定义一个数据源
- 模拟用户的搜索数据
- 定义自己的
Pattern
这个模式的特点就是连续两次搜索商品 “帽子” ,然后进行匹配,发现匹配后输出一条提示信息,直接打印在控制台上。
代码如下:
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternSelectFunction;
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.DataStreamSource;
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 java.util.List;
import java.util.Map;
public class CepDemo {
public static void main(String[] args) throws Exception{
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource source = env.fromElements(
//浏览记录
Tuple3.of("Marry", "外套", 1L),
Tuple3.of("Marry", "帽子",1L),
Tuple3.of("Marry", "帽子",2L),
Tuple3.of("Marry", "帽子",3L),
Tuple3.of("Ming", "衣服",1L),
Tuple3.of("Marry", "鞋子",1L),
Tuple3.of("Marry", "鞋子",2L),
Tuple3.of("LiLei", "帽子",1L),
Tuple3.of("LiLei", "帽子",2L),
Tuple3.of("LiLei", "帽子",3L)
);
//定义Pattern,寻找连续搜索帽子的用户
Pattern<Tuple3<String, String, Long>, Tuple3<String, String, Long>> pattern = Pattern
.<Tuple3<String, String, Long>>begin("start")
.where(new SimpleCondition<Tuple3<String, String, Long>>() {
@Override
public boolean filter(Tuple3<String, String, Long> value) throws Exception {
return value.f1.equals("帽子");
}
}) //.timesOrMore(3);
.next("middle")
.where(new SimpleCondition<Tuple3<String, String, Long>>() {
@Override
public boolean filter(Tuple3<String, String, Long> value) throws Exception {
return value.f1.equals("帽子");
}
});
KeyedStream keyedStream = source.keyBy(0);
PatternStream patternStream = CEP.pattern(keyedStream, pattern);
SingleOutputStreamOperator matchStream = patternStream.select(new PatternSelectFunction<Tuple3<String, String, Long>, String>() {
@Override
public String select(Map<String, List<Tuple3<String, String, Long>>> pattern) throws Exception {
List<Tuple3<String, String, Long>> middle = pattern.get("middle");
return middle.get(0).f0 + ":" + middle.get(0).f2 + ":" + "连续搜索两次帽子!";
}
});
matchStream.printToErr();
env.execute("execute cep");
}
}
输出结果如下:
Marry:2:连续搜索两次帽子!
Marry:3:连续搜索两次帽子!
LiLei:2:连续搜索两次帽子!
LiLei:3:连续搜索两次帽子!
(2)案例二:恶意登录检测
Flink CEP 开发流程:
DataSource中的数据转换为DataStream;watermark、keyby- 定义
Pattern, 并将DataStream和Pattern组合转换为PatternStream; PatternStream经过select、process等算子转换为DataStream;- 再次转换的
DataStream经过处理后,sink到目标库。
需求: 找出5秒内, 连续登录失败的账号。
思路:
- 获取数据源
- 在数据源上做出
watermark(允许一些乱序数据) - 在
watermark上根据id分组keyby - 做出模式
pattern - 在数据流上进行模式匹配
- 提取匹配成功的数据
代码如下:
import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.functions.PatternProcessFunction;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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 java.util.List;
import java.util.Map;
/**
* @author donald
* @date 2021/04/23
*/
public class LoginCepDemo {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
env.setParallelism(1);
DataStreamSource<LoginBean> data = env.fromElements(new LoginBean(1L, "fail", 1597905234000L),
new LoginBean(1L, "success", 1597905235000L),
new LoginBean(2L, "fail", 1597905236000L),
new LoginBean(2L, "fail", 1597905237000L),
new LoginBean(2L, "fail", 1597905238000L),
new LoginBean(3L, "fail", 1597905239000L),
new LoginBean(3L, "success", 1597905240000L));
// 2. 在数据源上作出 watermark
SingleOutputStreamOperator<LoginBean> watermarks =
data.assignTimestampsAndWatermarks(new WatermarkStrategy<LoginBean>() {
@Override
public WatermarkGenerator<LoginBean> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
return new WatermarkGenerator<LoginBean>() {
long maxTimeStamp = Long.MIN_VALUE;
@Override
public void onEvent(LoginBean event, long eventTimestamp, WatermarkOutput output) {
maxTimeStamp = Math.max(maxTimeStamp, event.getTs());
}
long maxOutOfOrderness = 500L;
@Override
public void onPeriodicEmit(WatermarkOutput output) {
output.emitWatermark(new Watermark(maxTimeStamp - maxOutOfOrderness));
}
};
}
}.withTimestampAssigner((element, recordTimestamp) -> element.getTs()));
// 3. 在 watermark 上根据 id 分组 keyby
KeyedStream<LoginBean, Long> keyedStream = watermarks.keyBy(LoginBean::getId);
// 4. 做出模式 Pattern
Pattern<LoginBean, LoginBean> pattern = Pattern.<LoginBean>begin("start").where(new IterativeCondition<LoginBean>() {
@Override
public boolean filter(LoginBean loginBean, Context<LoginBean> context) {
return loginBean.getState().equals("fail");
}
}).next("next").where(new IterativeCondition<LoginBean>() {
@Override
public boolean filter(LoginBean loginBean, Context<LoginBean> context) {
return loginBean.getState().equals("fail");
}
}).within(Time.seconds(5));
// 5. 在数据流上进行模式匹配
PatternStream<LoginBean> patternStream = CEP.pattern(keyedStream, pattern);
// 6. 提取匹配成功的数据
SingleOutputStreamOperator<String> process = patternStream.process(new PatternProcessFunction<LoginBean, String>() {
@Override
public void processMatch(Map<String, List<LoginBean>> match, Context ctx,
Collector<String> out) {
List<LoginBean> start = match.get("start");
List<LoginBean> next = match.get("next");
String res = "start:" + start + "...next:" + next;
out.collect(res + start.get(0).getId());
}
});
process.print();
env.execute();
}
}
LoginBean.java 如下:
// 省略:get、set、toString 方法
public class LoginBean {
private Long id;
private String state;
private Long ts;
public LoginBean() {
}
public LoginBean(Long id, String state, Long ts) {
this.id = id;
this.state = state;
this.ts = ts;
}
}
输出结果如下:
{start=[LoginBean{id=2, state='fail', ts=1597905236000}], next=[LoginBean{id=2, state='fail', ts=1597905237000}]}
start:[LoginBean{id=2, state='fail', ts=1597905236000}]...next:[LoginBean{id=2, state='fail', ts=1597905237000}]2
{start=[LoginBean{id=2, state='fail', ts=1597905237000}], next=[LoginBean{id=2, state='fail', ts=1597905238000}]}
start:[LoginBean{id=2, state='fail', ts=1597905237000}]...next:[LoginBean{id=2, state='fail', ts=1597905238000}]2
(3)案例三:超时未支付
需求: 找出下单后10分钟没有支付的订单。
思路:
-
数据源
-
转化
watermark -
keyby转化 -
做出
Pattern(下单以后10分钟内未支付)注意:下单为
create支付为pay,create和pay之间不需要是严格临近, 所以选择followedBy -
模式匹配
-
取出匹配成功的数据
代码如下:
import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternSelectFunction;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.PatternTimeoutFunction;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.IterativeCondition;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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.OutputTag;
import java.util.List;
import java.util.Map;
public class PayDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
environment.setParallelism(1);
DataStreamSource<PayBean> data = environment.fromElements(
new PayBean(1L, "create", 1597905234000L),
new PayBean(1L, "pay", 1597905235000L),
new PayBean(2L, "create", 1597905236000L),
new PayBean(2L, "pay", 1597905237000L),
new PayBean(3L, "create", 1597905239000L)
);
SingleOutputStreamOperator<PayBean> watermarks = data.assignTimestampsAndWatermarks(new WatermarkStrategy<PayBean>() {
@Override
public WatermarkGenerator<PayBean> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
return new WatermarkGenerator<PayBean>() {
long maxTimeStamp = Long.MIN_VALUE;
long maxOutOfOrderness = 500L;
@Override
public void onEvent(PayBean event, long eventTimestamp, WatermarkOutput output) {
maxTimeStamp = Math.max(maxTimeStamp, event.getTs());
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
output.emitWatermark(new Watermark(maxTimeStamp - maxOutOfOrderness));
}
};
}
}.withTimestampAssigner((element, recordTimestamp) -> element.getTs()));
KeyedStream<PayBean, Long> keyedStream = watermarks.keyBy(PayBean::getId);
Pattern<PayBean, PayBean> pattern = Pattern.<PayBean>begin("start").where(new IterativeCondition<PayBean>() {
@Override
public boolean filter(PayBean payBean, Context<PayBean> context) {
return payBean.getState().equals("create");
}
}).followedBy("next").where(new IterativeCondition<PayBean>() {
@Override
public boolean filter(PayBean payBean, Context<PayBean> context) {
return payBean.getState().equals("pay");
}
}).within(Time.seconds(600));
PatternStream<PayBean> patternStream = CEP.pattern(keyedStream, pattern);
OutputTag<PayBean> outoftime = new OutputTag<PayBean>("outoftime"){};
SingleOutputStreamOperator<PayBean> result = patternStream.select(outoftime, new PatternTimeoutFunction<PayBean, PayBean>() {
@Override
public PayBean timeout(Map<String, List<PayBean>> map, long l) {
return map.get("start").get(0);
}
}, new PatternSelectFunction<PayBean, PayBean>() {
@Override
public PayBean select(Map<String, List<PayBean>> map) {
return map.get("start").get(0);
}
});
DataStream<PayBean> sideOutput = result.getSideOutput(outoftime);
sideOutput.print();
environment.execute();
}
}
PayBean.java如下:
// 省略:get、set、toString 方法
public class PayBean {
private Long id;
private String state;
private Long ts;
public PayBean() {
}
public PayBean(Long id, String state, Long ts) {
this.id = id;
this.state = state;
this.ts = ts;
}
}
输出结果如下:
PayBean{id=3, state='create', ts=1597905239000}