开篇
这段时间较忙,很久没有来记录一些学习到的东西。额,也有可能是变懒了。。。 恰巧最近在看flink相关的东西,于是就把相关的东西以及自己的一些理解记录一下。
这篇文章主要是简单介绍一下CEP是干哈的,什么场景下可以用到。接着就来实战一下,用CEP来实现一个需求。最后是对CEP的一些比较常用的API进行介绍。
这里贴一下官网链接和下面example的地址。
Complex event processing for Flink
FlinkCEP 是在flink上面实现的一个可以处理复杂事件的库。它可以从无界事件流中提取你想要的事件或者信息。
FlinkCEP提供了一系列 Pattern API ,我们可以定义自己的 pattern sequences
,用于从原始事件流中匹配和提取事件。
我们可以这样理解,输入的简单事件
,flink计算输出的是提取出来的复杂事件
。那么就得知,复杂事件
是根据一些规则
从简单事件流
中计算而得来的,这个规则就是我们定义的pattern sequences
。
由此,假如我们需要检测某些复杂事件
,无需写额外的代码去监测输入的原始事件,按需定义pattern sequences
即可。这么讲一通,确实比较抽象,接下来我们看一个例子,进一步体会体会CEP。
example
我们购物时,通常有一些点击浏览商品和加入收藏的一些行为。现在假设每发生上述的行为,都会作为事件输入到Flink中。
现在我们定义规则
,对于某个用户,若满足下面其中一个条件,就会被标记为潜在客户,可能会作为后续商品的推广对象。
- 先点击浏览商品,然后将商品加入收藏。
- 1分钟内点击浏览了商品3次。
好的,我们开始写代码,首先定义Event
public class Event {
private String name;
/** 事件类型 0--浏览商品 1---收藏商品*/
private int type;
}
为了本地测试方便,通过nc命令打开端口向flink模拟输入事件,flink接收到事件之后进行转换
DataStreamSource dataStreamSource = env.socketTextStream("localhost", 9999, "\n");
KeyedStream partitionedInput = dataStreamSource.filter(Objects::nonNull)
.map(s -> {
// 输入的string,逗号分隔,第一个字段为用户名,第二个字段为事件类型
String[] strings = s.split(",");
if (strings.length != 2) {
return null;
}
Event event = new Event();
event.setName(strings[0]);
event.setType(Integer.parseInt(strings[1]));
return event;
}).returns(Event.class)
.keyBy((KeySelector) Event::getName);
接着我们定义两个pattern sequences
, patternA 和 patternB代表的规则代码中已经进行注释。
// 先点击浏览商品,然后将商品加入收藏
Pattern patternA = Pattern.begin("firstly")
.where(new SimpleCondition() {
@Override
public boolean filter(Event event) throws Exception {
// 点击商品
return event.getType() == 0;
}
})
.followedBy("and")
.where(new SimpleCondition() {
@Override
public boolean filter(Event event) throws Exception {
// 将商品加入收藏
return event.getType() == 1;
}
});
// 1分钟内点击浏览了商品3次。
Pattern patternB = Pattern.begin("start")
.where(new SimpleCondition() {
@Override
public boolean filter(Event event) throws Exception {
// 浏览商品
return event.getType() == 0;
}
})
.timesOrMore(1)
.within(Time.minutes(3));
定义好pattern之后,我们就可以利用这两个pattern
对输入流进行检测,因为我们有两个pattern sequences
, 检测完之后可以对检测出的复杂事件
进行union
。
// CEP用pattern将输入的时间事件流转化为复杂事件流
PatternStream patternStreamA = CEP.pattern(partitionedInput, patternA);
PatternStream patternStreamB = CEP.pattern(partitionedInput, patternB);
DataStream streamA = processPatternStream(patternStreamA, "收藏商品");
DataStream streamB = processPatternStream(patternStreamB, "连续浏览商品");
// 最后两个复杂事件流进行合并
streamA.union(streamB)
.print();
完整的代码可以从这里取 代码地址
运行如下
// 输入
Jack,0
Jack,1
//输出
> Jack 成为潜在客户 ,收藏商品
// 输入
Andy,0
Andy,0
Andy,0
// 输出
> Andy 成为潜在客户 ,连续浏览商品
Pattern API 解释
在上面这个example里面,我们使用了一些比较常用的Pattern API,下面我们来对这些API进行认识。
conditions
对于每个pattern
需要为其设置条件(condition)来判断到来的简单事件是否被接受,上面的example中,就用到了pattern.where()
来进行条件判断。除此之外还提供了, pattern.or()
和 pattern.until()
两个方法。
pattern.where()是给当前的pattern定义一个条件。
pattern.or()是给当前的pattern定义一个or条件。
pattern. until()是给当前的pattern定义一个结束循环的条件。
除了为pattern
指定条件,还可以设置它的循环次数,下面代码摘自官网,每句代码的作用是显而易见的,就不翻译了。
// expecting 4 occurrences
start.times(4);
// expecting 0 or 4 occurrences
start.times(4).optional();
// expecting 2, 3 or 4 occurrences
start.times(2, 4);
// expecting 2, 3 or 4 occurrences and repeating as many as possible
start.times(2, 4).greedy();
// expecting 0, 2, 3 or 4 occurrences
start.times(2, 4).optional();
// expecting 0, 2, 3 or 4 occurrences and repeating as many as possible
start.times(2, 4).optional().greedy();
// expecting 1 or more occurrences
start.oneOrMore();
// expecting 1 or more occurrences and repeating as many as possible
start.oneOrMore().greedy();
// expecting 0 or more occurrences
start.oneOrMore().optional();
// expecting 0 or more occurrences and repeating as many as possible
start.oneOrMore().optional().greedy();
// expecting 2 or more occurrences
start.timesOrMore(2);
// expecting 2 or more occurrences and repeating as many as possible
start.timesOrMore(2).greedy();
// expecting 0, 2 or more occurrences
start.timesOrMore(2).optional()
// expecting 0, 2 or more occurrences and repeating as many as possible
start.timesOrMore(2).optional().greedy();
Combining Patterns
我们定义多个pattern
之后,需要将他们组合起来,如上面我们的example中就:先浏览商品,再点击收藏,我们回忆一下,是这样写的,两个pattern
用followedBy
进行连接。
// 先点击浏览商品,然后将商品加入收藏
Pattern patternA = Pattern.begin("firstly")
.where(new SimpleCondition() {
@Override
public boolean filter(Event event) throws Exception {
// 点击商品
return event.getType() == 0;
}
})
.followedBy("and")
.where(new SimpleCondition() {
@Override
public boolean filter(Event event) throws Exception {
// 将商品加入收藏
return event.getType() == 1;
}
});
CEP中pattern连接有3中模式,
Strict Contiguity:要求一个event之后必须紧跟下一个符合条件的event,中间不允许有其他事件。
Relaxed Contiguity:和上一种不同的是,该模式允许中间有其他无关的event,会对他们进行忽略。
Non-Deterministic Relaxed Contiguity:非确定性宽松连续性,可以对已经匹配的事件就行忽略,对接下来的事件继续匹配。
上面三种模式分别对应的表达式是:
next()
followedBy()
followedByAny()
对于next()
和followedBy()
比较好理解,那我们来看个关于followedByAny()
栗子。
如下 pattern sequence 和input,b+表示一个或者多个b:
// pattern sequence
a (followedByAny) b+ (followedByAny) c
// input
"a", "b1", "d1", "b2", "d2", "b3" "c"
那么,CEP匹配得到的结构有:{a b1 c}, {a b1 b2 c}, {a b1 b3 c}
, {a b1 b2 b3 c}, {a b2 c}
, {a b2 b3 c}
, {a b3 c}。可以看到, {a b1 b3 c}
忽略了已经匹配的事件b2,{a b2 c}
和{a b2 b3 c}
都忽略了已经匹配的事件b1,就是时所谓的Non-Deterministic Relaxed Contiguity。
总结
Flink CEP的简单介绍和使用就到这里,通过本文可以对fink cep有一个简单的认识。对于复杂事件的提取,我们只关注于pattern sequence
的编写,极大简化了程序的复杂性,提高了可维护性。