Flink CEP - 复杂事件处理(Complex Event Processing)

文章目录


Flink官网: Flink 官网
参考博客: Alienware^博客

持续更新中...

Flink CEP - 复杂事件处理(Complex Event Processing)

1. Flink CEP 定义:

所谓 CEP,其实就是“复杂事件处理(Complex Event Processing)”的缩写;而 Flink CEP,就是 Flink 实现的一个用于复杂事件处理的库(library)。那到底什么是“复杂事件处理”呢?就是可以在事件流里,检测到特定的事件组合并进行处理,比如说“连续登录失败”,或者“订单支付超时”等等。具体的处理过程是,把事件流中的一个个简单事件,通过一定的规则匹配组合起来,这就是“复杂事件”;然后基于这些满足规则的一组组复杂事件进行转换处理,得到想要的结果进行输出。
总结起来,复杂事件处理(CEP)的流程可以分成三个步骤:
(1)定义一个匹配规则
(2)将匹配规则应用到事件流上,检测满足规则的复杂事件
(3)对检测到的复杂事件进行处理,得到结果进行输出
模式匹配

2. 示例代码

  • pom文件依赖
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-cep_2.11</artifactId>
  <version>1.13.5</version>
</dependency>
  • 示例代码
package com.ali.flink.demo.driver.flink_cep;

import cn.hutool.core.util.StrUtil;
import com.ali.flink.demo.bean.UserLoginEventBean;
import com.ali.flink.demo.utils.DataGeneratorImpl005;
import com.ali.flink.demo.utils.FlinkEnv;
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.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.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.datagen.DataGeneratorSource;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

import java.time.Duration;
import java.util.List;
import java.util.Map;

/**
 * 检测三次连续登录失败的用户,并输出告警信息
 */
public class FlinkCEPPatternOfIndividualPattern {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = FlinkEnv.FlinkDataStreamRunEnv();
        env.setParallelism(1);

        StreamTableEnvironment tableEnv = FlinkEnv.getStreamTableEnv(env);

        DataGeneratorSource<UserLoginEventBean> dataGeneratorSource = new DataGeneratorSource<>(new DataGeneratorImpl005());

        // 添加source流,并设置 watermark
        SingleOutputStreamOperator<UserLoginEventBean> sourceStream = env.addSource(dataGeneratorSource).returns(UserLoginEventBean.class)
                .assignTimestampsAndWatermarks(WatermarkStrategy.<UserLoginEventBean>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                        .withTimestampAssigner(new SerializableTimestampAssigner<UserLoginEventBean>() {
                            @Override
                            public long extractTimestamp(UserLoginEventBean userLoginEventBean, long l) {
                                return userLoginEventBean.getTimestamp();
                            }
                        }));
        // 打印source流数据
        sourceStream.print("source stream");

        // 定义模式,连续三次登录失败
        Pattern<UserLoginEventBean, UserLoginEventBean> pattern = Pattern.<UserLoginEventBean>begin("first")
                .where(new SimpleCondition<UserLoginEventBean>() {
                    @Override
                    public boolean filter(UserLoginEventBean userLoginEventBean) throws Exception {
                        return StrUtil.equals(userLoginEventBean.getLoginType(), "fail");
                    }
                })
                .next("second")
                .where(new SimpleCondition<UserLoginEventBean>() {
                    @Override
                    public boolean filter(UserLoginEventBean userLoginEventBean) throws Exception {
                        return StrUtil.equals(userLoginEventBean.getLoginType(), "fail");
                    }
                })
                .next("third")
                .where(new SimpleCondition<UserLoginEventBean>() {
                    @Override
                    public boolean filter(UserLoginEventBean userLoginEventBean) throws Exception {
                        return StrUtil.equals(userLoginEventBean.getLoginType(), "fail");
                    }
                });

        // 将模式应用到数据流上
        PatternStream<UserLoginEventBean> patternStream = CEP.pattern(sourceStream.keyBy(user -> user.getUserId()), pattern);

        // 将检测的结果事件提取出来,进行处理得到告警信息
        SingleOutputStreamOperator<String> warningStream = patternStream.select(new PatternSelectFunction<UserLoginEventBean, String>() {
            @Override
            public String select(Map<String, List<UserLoginEventBean>> map) throws Exception {
                UserLoginEventBean firstStream = map.get("first").get(0);
                UserLoginEventBean secondStream = map.get("second").get(0);
                UserLoginEventBean thirdStream = map.get("third").get(0);
                return firstStream.getUserId() + "连续三次登录失败,登录时间分别是:" + firstStream.getLoginTime() + secondStream.getLoginTime() + thirdStream.getLoginTime();
            }
        });

        warningStream.print("warning Stream");

        env.execute("Flink CEP start");
    }
}

--------------------------结果--------------------------------------
source stream> UserLoginEventBean{userId='u2', loginAddress='南京', loginType='fail', loginTime='2022-07-15 11:22:54', timestamp=1657855374497}
source stream> UserLoginEventBean{userId='u3', loginAddress='杭州', loginType='fail', loginTime='2022-07-15 11:22:55', timestamp=1657855375512}
source stream> UserLoginEventBean{userId='u4', loginAddress='杭州', loginType='success', loginTime='2022-07-15 11:22:57', timestamp=1657855377513}
source stream> UserLoginEventBean{userId='u2', loginAddress='上海', loginType='success', loginTime='2022-07-15 11:22:59', timestamp=1657855379525}
source stream> UserLoginEventBean{userId='u1', loginAddress='上海', loginType='success', loginTime='2022-07-15 11:23:00', timestamp=1657855380537}
source stream> UserLoginEventBean{userId='u1', loginAddress='北京', loginType='success', loginTime='2022-07-15 11:23:00', timestamp=1657855380538}
source stream> UserLoginEventBean{userId='u2', loginAddress='上海', loginType='fail', loginTime='2022-07-15 11:23:02', timestamp=1657855382550}
source stream> UserLoginEventBean{userId='u4', loginAddress='北京', loginType='fail', loginTime='2022-07-15 11:23:03', timestamp=1657855383559}
source stream> UserLoginEventBean{userId='u1', loginAddress='上海', loginType='fail', loginTime='2022-07-15 11:23:03', timestamp=1657855383559}
source stream> UserLoginEventBean{userId='u4', loginAddress='南京', loginType='fail', loginTime='2022-07-15 11:23:05', timestamp=1657855385561}
source stream> UserLoginEventBean{userId='u4', loginAddress='南京', loginType='fail', loginTime='2022-07-15 11:23:07', timestamp=1657855387570}
source stream> UserLoginEventBean{userId='u3', loginAddress='杭州', loginType='fail', loginTime='2022-07-15 11:23:07', timestamp=1657855387571}
source stream> UserLoginEventBean{userId='u1', loginAddress='杭州', loginType='fail', loginTime='2022-07-15 11:23:07', timestamp=1657855387571}
source stream> UserLoginEventBean{userId='u4', loginAddress='上海', loginType='success', loginTime='2022-07-15 11:23:09', timestamp=1657855389572}
warning Stream> u4连续三次登录失败,登录时间分别是:2022-07-15 11:23:032022-07-15 11:23:052022-07-15 11:23:07

3. 模式分类

模式(Pattern)其实就是将一组简单事件组合成复杂事件的“匹配规则”。由于流中事件的匹配是有先后顺序的,因此一个匹配规则就可以表达成先后发生的一个个简单事件,按顺序串联组合在一起。

3.1 个体模式

3.1.1 定义:

这里的每一个简单事件并不是任意选取的,也需要有一定的条件规则;所以我们就把每个简单事件的匹配规则,叫作“个体模式”(Individual Pattern)。

每个个体模式都以一个“连接词”开始定义的,比如 begin、next 等等,这是 Pattern 对象的一个方法(begin 是 Pattern 类的静态方法),返回的还是一个 Pattern。这些“连接词”方法有一个 String 类型参数,这就是当前个体模式唯一的名字,比如这里的“first”、“second”。在之后检测到匹配事件时,就会以这个名字来指代匹配事件。

.next("second")
.where(new SimpleCondition<UserLoginEventBean>() {
@Override
     public boolean filter(UserLoginEventBean userLoginEventBean) throws Exception {
         return StrUtil.equals(userLoginEventBean.getLoginType(), "fail");
     }
 })

个体模式需要一个“过滤条件”,用来指定具体的匹配规则。这个条件一般是通过调用.where()方法来实现的,具体的过滤逻辑则通过传入的 SimpleCondition 内的.filter()方法来定义。

3.1.2 量词:

个体模式后面可以跟一个“量词”,用来指定循环的次数。个体模式包括“单例(singleton)模式”和“循环(looping)模式”。默认情况下,个体模式是单例模式,匹配接收一个事件;当定义了量词之后,就变成了循环模式,可以匹配接收多个事件。默认匹配关系是“宽松近邻”关系。

1) .oneOrMore()
匹配事件出现一次或多次,假设 a 是一个个体模式,a.oneOrMore()表示可以匹配 1 个或多个 a 的事件组合。我们有时会用 a+来简单表示。
2) .times(times)
匹配事件发生特定次数(times),例如 a.times(3)表示 aaa;
3) .times(fromTimes,toTimes)
指定匹配事件出现的次数范围,最小次数为fromTimes,最大次数为toTimes。例如a.times(2, 4)可以匹配 aa,aaa 和 aaaa。
4) .greedy()
只能用在循环模式后,使当前循环模式变得“贪心”(greedy),也就是总是尽可能多地去匹配。例如 a.times(2, 4).greedy(),如果出现了连续 4 个 a,那么会直接把 aaaa 检测出来进行处理,其他任意 2 个 a 是不算匹配事件的。
5) .optional()
使当前模式成为可选的,也就是说可以满足这个匹配条件,也可以不满足。
对于一个个体模式 pattern 来说,后面所有可以添加的量词如下:

// 匹配事件出现 4 次
pattern.times(4);
// 匹配事件出现 4 次,或者不出现
pattern.times(4).optional();
// 匹配事件出现 2, 3 或者 4 次
pattern.times(2, 4);
// 匹配事件出现 2, 3 或者 4 次,并且尽可能多地匹配
pattern.times(2, 4).greedy();
// 匹配事件出现 2, 3, 4 次,或者不出现
pattern.times(2, 4).optional();
// 匹配事件出现 2, 3, 4 次,或者不出现;并且尽可能多地匹配
pattern.times(2, 4).optional().greedy();
// 匹配事件出现 1 次或多次
pattern.oneOrMore();
// 匹配事件出现 1 次或多次,并且尽可能多地匹配
pattern.oneOrMore().greedy();
// 匹配事件出现 1 次或多次,或者不出现
pattern.oneOrMore().optional();
// 匹配事件出现 1 次或多次,或者不出现;并且尽可能多地匹配
pattern.oneOrMore().optional().greedy();
// 匹配事件出现 2 次或多次
pattern.timesOrMore(2);
// 匹配事件出现 2 次或多次,并且尽可能多地匹配
pattern.timesOrMore(2).greedy();
// 匹配事件出现 2 次或多次,或者不出现
pattern.timesOrMore(2).optional()
// 匹配事件出现 2 次或多次,或者不出现;并且尽可能多地匹配
pattern.timesOrMore(2).optional().greedy();
3.1.3 条件:

通过调用 Pattern 对象的.where()方法来实现的,主要可以分为简单条件、迭代条件、复合条件、终止条件几种类型。此外,也可以调用 Pattern 对象的.subtype()方法来限定匹配事件的子类型。

3.1.3.1 限定子类型

调用.subtype()方法可以为当前模式增加子类型限制条件。例如:

pattern.subtype(SubEvent.class);

这里 SubEvent 是流中数据类型 Event 的子类型。这时,只有当事件是 SubEvent 类型时,才可以满足当前模式 pattern 的匹配条件。

3.1.3.2 简单条件(Simple Conditions):只能对当前事件做处理

简单条件是最简单的匹配规则,只根据当前事件的特征来决定是否接受它。这在本质上其实就是一个 filter 操作。
代码中我们为.where()方法传入一个 SimpleCondition 的实例作为参数。SimpleCondition 是表示“简单条件”的抽象类,内部有一个.filter()方法,唯一的参数就是当前事件。所以它可以当作 FilterFunction 来使用

.where(new SimpleCondition<UserLoginEventBean>() {
      @Override
      public boolean filter(UserLoginEventBean userLoginEventBean) throws Exception {
           return StrUtil.equals(userLoginEventBean.getLoginType(), "fail");
      }
})
3.1.3.3 迭代条件(Iterative Conditions): 与之前事件做对比处理

简单条件只能基于当前事件做判断,能够处理的逻辑比较有限。在实际应用中,我们可能需要将当前事件跟之前的事件做对比,才能判断出要不要接受当前事件。这种需要依靠之前事件来做判断的条件,就叫作“迭代条件”(Iterative Condition)。

调用ctx.getEventsForPattern(...)可以获得所有前面已经接受作为可能匹配的事件。 调用这个操作的代价可能很小也可能很大,所以在实现你的条件时,尽量少使用它

3.1.3.4 组合条件(Combining Conditions)
  1. 组合条件,就是.where()后面再接一个.where()。因为前面提到过,一个条件就像是一个 filter 操作,所以每次调用.where()方法都相当于做了一次过滤,连续多次调用就表示多重过滤,最终匹配的事件自然就会同时满足所有条件。这相当于就是多个条件的“逻辑与(AND)。
  2. 多个条件的逻辑或(OR),则可以通过.where()后加一个.or()来实现。这里的.or()方法与.where()一样,传入一个 IterativeCondition 作为参数,定义一个独立的条件;它和之前.where()定义的条件只要满足一个,当前事件就可以成功匹配。
  3. 子类型限定条件(subtype)也可以和其他条件结合起来,成为组合条件
3.1.3.5 终止条件(Stop Conditions)

终止条件的定义是通过调用模式对象的.until()方法来实现的 ,同样传入一个IterativeCondition 作为参数。需要注意的是,终止条件只与 oneOrMore() 或者oneOrMore().optional()结合使用。因为在这种循环模式下,我们不知道后面还有没有事件可以匹配,只好把之前匹配的事件作为状态缓存起来继续等待,这等待无穷无尽;如果一直等下去,缓存的状态越来越多,最终会耗尽内存。所以这种循环模式必须有个终点,当.until()指定的条件满足时,循环终止,这样就可以清空状态释放内存了。
在这里插入图片描述
在这里插入图片描述

3.2 组合模式

3.2.1 定义:

将多个个体模式组合起来的完整模式,就叫作“组合模式”(Combining Pattern),为了跟个体模式区分有时也叫作“模式序列”(Pattern Sequence)。所有的组合模式,都必须以一个“初始模式”开头;而初始模式必须通过调用 Pattern 的静态方法.begin()来创建。一个组合模式有以下形式:

Pattern <Event, ?> pattern = Pattern
			.<Event>begin("start")
			.where(...)
			.next("next")
			.where(...)
			.followedBy("follow")
			.where(...)
			...
3.2.2 连续条件(连续策略):

连续条件包括:近邻条件(Contiguity Conditions),严格近邻(Strict Contiguity),宽松近邻(Relaxed Contiguity),非确定性宽松近邻(Non-Deterministic Relaxed Contiguity)

3.2.2.1 近邻条件(Contiguity Conditions)

初始模式之后,就可以按照复杂事件的顺序追加模式,组合成模式序列了。模式之间的组合是通过一些“连接词”方法实现的,这些连接词指明了先后事件之间有着怎样的近邻关系,这就是所谓的“近邻条件”(Contiguity Conditions,也叫“连续性条件”)。

3.2.2.2 严格近邻(Strict Contiguity): next()

匹配的事件严格地按顺序一个接一个出现,中间不会有任何其他事件。代码中对应的就是 Pattern 的.next()方法,名称上就能看出来,“下一个”自然就是紧挨着的。
严格近邻

3.2.2.3 宽松近邻(Relaxed Contiguity):followedBy()

宽松近邻只关心事件发生的顺序,而放宽了对匹配事件的“距离”要求,也就是说两个匹配的事件之间可以有其他不匹配的事件出现。代码中对应.followedBy()方法,很明显这表示“跟在后面”就可以,不需要紧紧相邻。
宽松近邻

3.2.2.4 非确定性宽松近邻(Non-Deterministic Relaxed Contiguity):followedByAny()

这种近邻关系更加宽松。所谓“非确定性”是指可以重复使用之前已经匹配过的事件;这种近邻条件下匹配到的不同复杂事件,可以以同一个事件作为开始,所以匹配结果一般会比宽松近邻更多,代码中对应.followedByAny()方法。
非确定性宽松近邻

3.2.2.5 其他限制条件 : notNext()、notFollowedBy()

除了上面提到的 next()、followedBy()、followedByAny()可以分别表示三种近邻条件,还可以用否定的“连接词”来组合个体模式。主要包括:
1) .notNext()
表示前一个模式匹配到的事件后面,不能紧跟着某种事件。
2) .notFollowedBy()
表示前一个模式匹配到的事件后面,不会出现某种事件。这里需要注意,由于notFollowedBy()是没有严格限定的;流数据不停地到来,我们永远不能保证之后“不会出现某种事件”。所以一个模式序列不能以 notFollowedBy()结尾,这个限定条件主要用来表示“两个事件中间不会出现某种事件”

// 严格近邻条件
Pattern<Event, ?> strict = start.next("middle").where(...);
// 宽松近邻条件
Pattern<Event, ?> relaxed = start.followedBy("middle").where(...);
// 非确定性宽松近邻条件
Pattern<Event, ?> nonDetermin = start.followedByAny("middle").where(...);
// 不能严格近邻条件
Pattern<Event, ?> strictNot = start.notNext("not").where(...);
// 不能宽松近邻条件
Pattern<Event, ?> relaxedNot = start.notFollowedBy("not").where(...);
// 时间限制条件
middle.within(Time.seconds(10));
3.2.2.6 循环模式中的近邻条件:consecutive()、allowCombinations()

对于循环模式(例如oneOrMore()和times())),默认是松散连续。如果想使用严格连续,你需要使用consecutive()方法明确指定, 如果想使用不确定松散连续,你可以使用allowCombinations()方法。

 //定义模式,连续三次登陆失败
Pattern<LoginEvent, ?> pattern = Pattern.<LoginEvent>begin("first")
    .where(new SimpleCondition<LoginEvent>() { // 以第一个登录失败事件开始
     	@Override
        public boolean filter(LoginEvent value) throws Exception {
            return value.eventType.equals("fail");
        }
}).times(3).consecutive();

3.3 模式组

定义一个模式序列作为begin,followedBy,followedByAny和next的条件。这个模式序列在逻辑上会被当作匹配的条件, 并且返回一个GroupPattern,可以在GroupPattern上使用oneOrMore(),times(#ofTimes), times(#fromTimes, #toTimes),optional(),consecutive(),allowCombinations()。

Pattern<Event, ?> start = Pattern.begin(
    Pattern.<Event>begin("start").where(...).followedBy("start_middle").where(...)
);

// 严格连续
Pattern<Event, ?> strict = start.next(
    Pattern.<Event>begin("next_start").where(...).followedBy("next_middle").where(...)
).times(3);

// 松散连续
Pattern<Event, ?> relaxed = start.followedBy(
    Pattern.<Event>begin("followedby_start").where(...).followedBy("followedby_middle").where(...)
).oneOrMore();

// 不确定松散连续
Pattern<Event, ?> nonDetermin = start.followedByAny(
    Pattern.<Event>begin("followedbyany_start").where(...).followedBy("followedbyany_middle").where(...)
).optional();

在这里插入图片描述
在这里插入图片描述

4. 匹配后跳过策略

对于一个给定的模式,同一个事件可能会分配到多个成功的匹配上。为了控制一个事件会分配到多少个匹配上,你需要指定跳过策略AfterMatchSkipStrategy。 有五种跳过策略,如下:

Pattern. < Event > begin("a").where(new SimpleCondition < Event > () {
    @Override
    public boolean filter(Event value) throws Exception {
        return value.user.equals("a");
    }
}).oneOrMore().followedBy("b").where(new SimpleCondition < Event > () {
    @Override
    public boolean filter(Event value) throws Exception {
        return value.user.equals("b");
    }
});

由上面代码可以看出匹配策略为:a+ b,如果a出现3次,则分别记为a1, a2, a3,呢么能匹配的结果有6中,分别为:(a1 a2 a3 b),(a1 a2 b),(a1 b),(a2 a3 b),(a2 b),(a3 b)。

4.1 不跳过策略:NO_SKIP

调用 AfterMatchSkipStrategy.noSkip()。这是默认策略,所有可能的匹配都会输出。所以这里会输出完整的 6 个匹配(a1 a2 a3 b),(a1 a2 b),(a1 b),(a2 a3 b),(a2 b),(a3 b)。

4.2 跳至下一个策略:SKIP_TO_NEXT

调用 AfterMatchSkipStrategy.skipToNext()。找到一个 a1 开始的最大匹配之后,跳过a1 开始的所有其他匹配,直接从下一个 a2 开始匹配起。当然 a2 也是如此跳过其他匹配。最终得到(a1 a2 a3 b),(a2 a3 b),(a3 b)。可以看到,这种跳过策略跟使用.greedy()效果是相同的。

4.3 跳过所有子匹配策略:SKIP_PAST_LAST_EVENT

调用 AfterMatchSkipStrategy.skipPastLastEvent()。找到 a1 开始的匹配(a1 a2 a3 b)之后,直接跳过所有 a1 直到 a3 开头的匹配,相当于把这些子匹配都跳过了。最终得到(a1 a2 a3 b),这是最为精简的跳过策略。

4.4 跳至第一个策略:SKIP_TO_FIRST

调用 AfterMatchSkipStrategy.skipToFirst(“a”),这里传入一个参数,指明跳至哪个模式的第一个匹配事件。找到 a1 开始的匹配(a1 a2 a3 b)后,跳到以最开始一个 a(也就是 a1)为开始的匹配,相当于只留下 a1 开始的匹配。最终得到(a1 a2 a3 b),(a1 a2 b),(a1 b)。

4.5 跳至最后一个策略:SKIP_TO_LAST

调用 AfterMatchSkipStrategy.skipToLast(“a”),同样传入一个参数,指明跳至哪个模式的最后一个匹配事件。找到 a1 开始的匹配(a1 a2 a3 b)后,跳过所有 a1、a2 开始的匹配,跳到以最后一个 a(也就是 a3)为开始的匹配。最终得到(a1 a2 a3 b),(a3 b)。
在这里插入图片描述
在这里插入图片描述

  • 设置策略的api文档
    在这里插入图片描述
// 不跳过策略:NO_SKIP
.begin("first", AfterMatchSkipStrategy.noSkip())
.begin("first", AfterMatchSkipStrategy.skipToNext())
.begin("first", AfterMatchSkipStrategy.skipPastLastEvent())
.begin("first", AfterMatchSkipStrategy.skipToFirst())
.begin("first", AfterMatchSkipStrategy.skipToLast())

// AfterMatchSkipStrategy.class的方法文档
public static NoSkipStrategy noSkip() {
    return NoSkipStrategy.INSTANCE;
}

public static AfterMatchSkipStrategy skipToNext() {
    return SkipToNextStrategy.INSTANCE;
}

public static SkipPastLastStrategy skipPastLastEvent() {
   return SkipPastLastStrategy.INSTANCE;
}

public static SkipToFirstStrategy skipToFirst(String patternName) {
    return new SkipToFirstStrategy(patternName, false);
}

public static SkipToLastStrategy skipToLast(String patternName) {
    return new SkipToLastStrategy(patternName, false);
}
  • 注意
    使用SKIP_TO_FIRST/LAST时,有两个选项可以用来处理没有事件可以映射到对应的变量名上的情况。 默认情况下会使用NO_SKIP策略,另外一个选项是抛出异常。 可以使用如下的选项:
AfterMatchSkipStrategy.skipToFirst(patternName).throwExceptionOnMiss()

5. 模式(Pattern)的应用

5.1 将模式(Pattern)在数据流上使用,产生模式流(PatternStream)

// DataStream 定义
DataStream<Event> input = ...
// Pattern 定义
Pattern<Event, ?> pattern = ...
// 可选的事件比较器
EventComparator<Event> comparator = ... // 可选的

PatternStream<Event> patternStream = CEP.pattern(input, pattern, comparator);

注意:
1) DataStream流类型还可以是keyed或者non-keyed。
2) 在non-keyed流上使用模式将会使你的作业并发度被设为1。

5.2 模式流(PatternStream)的数据处理

5.2.1 select 方式处理

处理匹配事件最简单的方式,就是从 PatternStream 中直接把匹配的复杂事件提取出来,包装成想要的信息输出,这个操作就是“选择”(select)。

5.2.1.1 PatternSelectFunction 类
SingleOutputStreamOperator<String> warningStream = patternStream.select(new PatternSelectFunction<UserLoginEventBean, String>() {
    @Override
    public String select(Map<String, List<UserLoginEventBean>> map) throws Exception {
        UserLoginEventBean firstStream = map.get("first").get(0);
        UserLoginEventBean secondStream = map.get("second").get(0);
        UserLoginEventBean thirdStream = map.get("third").get(0);
        return firstStream.getUserId() + "连续三次登录失败,登录时间分别是:" + firstStream.getLoginTime() + secondStream.getLoginTime() + thirdStream.getLoginTime();
    }
});

PatternSelectFunction 是 Flink CEP 提供的一个函数类接口,它会将检测到的匹配事件保存在一个 Map 里,对应的 key 就是这些事件的名称。这里的“事件名称”就对应着在模式中定义的每个个体模式的名称;而个体模式可以是循环模式,一个名称会对应多个事件,所以最终保存在 Map 里的 value 就是一个事件的列表(List),如果个体模式是单例的,那么 List 中只有一个元素,直接调用.get(0)就可以把它取出。

如果个体模式是循环的,List 中就有可能有多个元素了。例如我们在快速上手案例中对连续登录失败检测的改进,我们可以将匹配到的事件包装成 String 类型的报警信息输出,代码如下:

Pattern<UserLoginEventBean, UserLoginEventBean> pattern = Pattern
    .<UserLoginEventBean>begin("fail")
    .where(new SimpleCondition<UserLoginEventBean>() {
        @Override
        public boolean filter(UserLoginEventBean userLoginEventBean) throws Exception {
            return StrUtil.equals(userLoginEventBean.getLoginType(), "fail");
        }
     })
     .times(3)
     .consecutive();

PatternStream<UserLoginEventBean> patternStream = CEP.pattern(sourceStream.keyBy(user -> user.getUserId()), pattern);

DataStreamSink<String> warningStream = patternStream.select(new PatternSelectFunction<UserLoginEventBean, String>() {
    @Override
    public String select(Map<String, List<UserLoginEventBean>> map) throws Exception {
        UserLoginEventBean firstStream = map.get("fail").get(0);
        UserLoginEventBean secondStream = map.get("fail").get(1);
        UserLoginEventBean thirdStream = map.get("fail").get(2);
        return firstStream.getUserId() + "连续三次登录失败,登录时间分别是:" + firstStream.getLoginTime() + secondStream.getLoginTime() + thirdStream.getLoginTime();
    }
}).print("warning Stream");
5.2.1.2 PatternFlatSelectFunction 类

PatternStream 还有一个类似的方法是.flatSelect(),传入的参数是一个PatternFlatSelectFunction。从名字上就能看出,这是 PatternSelectFunction 的“扁平化”版本;内部需要实现一个 flatSelect()方法,它与之前 select()的不同就在于没有返回值,而是多了一个收集器(Collector)参数 out,通过调用out.collet()方法就可以实现多次发送输出数据了。

// 3. 将匹配到的复杂事件选择出来,然后包装成报警信息输出
patternStream.flatSelect(new PatternFlatSelectFunction <UserLoginEventBean, String> () {
    @Override
    public void flatSelect(Map <String, List <UserLoginEventBean>> map, Collector <String> out) throws Exception {
        UserLoginEventBean first = map.get("fail").get(0);
        UserLoginEventBean second = map.get("fail").get(1);
        UserLoginEventBean third = map.get("fail").get(2);
        out.collect(first.getUserId()+ " 连续三次登录失败!登录时间:" + first.getLoginTime() + ", " + second.getLoginTime() + ", " + third.getLoginTime());
    }
}).print("warning Stream");

可见 PatternFlatSelectFunction 使用更加灵活,完全能够覆盖PatternSelectFunction 的功能。这跟 FlatMapFunction 与 MapFunction 的区别是一样的。

5.2.2 process 方式处理
5.2.2.1 PatternProcessFunction 类

自 1.8 版本之后,Flink CEP 引入了对于匹配事件的通用检测处理方式,那就是直接调用PatternStream 的.process()方法,传入一个 PatternProcessFunction。这看起来就像是我们熟悉的处理函数(process function),它也可以访问一个上下文(Context),进行更多的操作。

所以 PatternProcessFunction 功能更加丰富、调用更加灵活,可以完全覆盖其他接口,也就成为了目前官方推荐的处理方式。事实上,PatternSelectFunction 和 PatternFlatSelectFunction在 CEP 内部执行时也会被转换成 PatternProcessFunction。我们可以使用 PatternProcessFunction 将之前的代码重写如下:

// 3. 将匹配到的复杂事件选择出来,然后包装成报警信息输出
patternStream.process(new PatternProcessFunction < LoginEvent, String > () {
    @Override
    public void processMatch(Map <String, List <UserLoginEventBean>> map, Context ctx, Collector < String > out) throws Exception {
        UserLoginEventBean first = map.get("fail").get(0);
        UserLoginEventBean second = map.get("fail").get(1);
        UserLoginEventBean third = map.get("fail").get(2);
        out.collect(first.getUserId()+ " 连续三次登录失败!登录时间:" + first.getLoginTime() + ", " + second.getLoginTime() + ", " + third.getLoginTime());
    }
}).print("warning Stream");

PatternProcessFunction 中必须实现一个 processMatch()方法;这个方法与之前的 flatSelect()类似,只是多了一个上下文 Context 参数。利用这个上下文可以获取当前的时间信息,比如事件的时间戳(timestamp)或者处理时间(processing time);还可以调用.output()方法将数据输出到侧输出流。侧输出流的功能是处理函数的一大特性,我们已经非常熟悉;而在 CEP 中,侧输出流一般被用来处理超时事件。

5.2.2.2 处理超时事件(TimedOutPartialMatchHandler)

在 Flink CEP 中 , 提供了一个专门捕捉超时的部分匹配事件的接 口 , 叫作TimedOutPartialMatchHandler。这个接口需要实现一个processTimedOutMatch()方法,可以将超时的、已检测到的部分匹配事件放在一个 Map 中,作为方法的第一个参数;方法的第二个参数则是 PatternProcessFunction 的上下文 Context。所以这个接口必须与 PatternProcessFunction结合使用,对处理结果的输出则需要利用侧输出流来进行。

package com.ali.flink.demo.driver.flink_cep;

import cn.hutool.core.util.StrUtil;
import com.ali.flink.demo.bean.UserLoginEventBean;
import com.ali.flink.demo.utils.DataGeneratorImpl005;
import com.ali.flink.demo.utils.FlinkEnv;
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.PatternStream;
import org.apache.flink.cep.functions.PatternProcessFunction;
import org.apache.flink.cep.functions.TimedOutPartialMatchHandler;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.datagen.DataGeneratorSource;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;

import java.time.Duration;
import java.util.List;
import java.util.Map;

/**
 * 处理超时数据
 */
public class FlinkCEPPatternProcessFunction {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = FlinkEnv.FlinkDataStreamRunEnv();
        env.setParallelism(1);

        StreamTableEnvironment tableEnv = FlinkEnv.getStreamTableEnv(env);

        DataGeneratorSource<UserLoginEventBean> dataGeneratorSource = new DataGeneratorSource<>(new DataGeneratorImpl005());

        // 添加source流,并设置 watermark
        SingleOutputStreamOperator<UserLoginEventBean> sourceStream = env.addSource(dataGeneratorSource).returns(UserLoginEventBean.class)
                .assignTimestampsAndWatermarks(WatermarkStrategy.<UserLoginEventBean>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                        .withTimestampAssigner(new SerializableTimestampAssigner<UserLoginEventBean>() {
                            @Override
                            public long extractTimestamp(UserLoginEventBean userLoginEventBean, long l) {
                                return userLoginEventBean.getTimestamp();
                            }
                        }));
        // 打印source流数据
        sourceStream.print("source stream");

        // 定义模式
        Pattern<UserLoginEventBean, UserLoginEventBean> pattern = Pattern.<UserLoginEventBean>begin("success login")
                .where(new SimpleCondition<UserLoginEventBean>() {
                    @Override
                    public boolean filter(UserLoginEventBean userLoginEventBean) throws Exception {
                        return StrUtil.equals(userLoginEventBean.getLoginType(), "success");
                    }
                })
                // 宽松近邻
                .followedBy("fail login")
                .where(new SimpleCondition<UserLoginEventBean>() {
                    @Override
                    public boolean filter(UserLoginEventBean userLoginEventBean) throws Exception {
                        return StrUtil.equals(userLoginEventBean.getLoginType(), "fail");
                    }
                })
                .within(Time.seconds(5));

        // 将 pattern使用到数据流
        PatternStream<UserLoginEventBean> patternStream = CEP.pattern(sourceStream.keyBy(user -> user.getUserId()), pattern);

        // 定义一个测输出流
        OutputTag<String> timeoutTag = new OutputTag<String>("timeout"){};

        SingleOutputStreamOperator<String> processStream = patternStream.process(new myProcess());

        processStream.print("正常匹配");
        processStream.getSideOutput(timeoutTag).print("timeout");

        env.execute("Flink CEP start");
    }

    public static class myProcess extends PatternProcessFunction<UserLoginEventBean, String> implements TimedOutPartialMatchHandler<UserLoginEventBean>{

        @Override
        public void processMatch(Map<String, List<UserLoginEventBean>> map, Context context, Collector<String> collector) throws Exception {
            // 正常匹配的事件
            UserLoginEventBean failLogin = map.get("fail login").get(0);
            collector.collect(failLogin.getUserId() + "登录失败");
        }

        @Override
        public void processTimedOutMatch(Map<String, List<UserLoginEventBean>> map, Context context) throws Exception {
            // 处理超时的事件
            UserLoginEventBean successLogin = map.get("success login").get(0);
            OutputTag<String> timeoutTag = new OutputTag<String>("timeout"){};
            context.output(timeoutTag, successLogin.getUserId() + "登录失败");
        }
    }
}

----------------------------结果------------------------------------
source stream> UserLoginEventBean{userId='u3', loginAddress='南京', loginType='success', loginTime='2022-07-18 13:56:56', timestamp=1658123816799}
source stream> UserLoginEventBean{userId='u3', loginAddress='上海', loginType='fail', loginTime='2022-07-18 13:57:04', timestamp=1658123824811}
timeout> u3登录失败
source stream> UserLoginEventBean{userId='u3', loginAddress='上海', loginType='fail', loginTime='2022-07-18 13:57:22', timestamp=1658123842823}
source stream> UserLoginEventBean{userId='u3', loginAddress='上海', loginType='success', loginTime='2022-07-18 13:57:23', timestamp=1658123843835}
source stream> UserLoginEventBean{userId='u3', loginAddress='上海', loginType='fail', loginTime='2022-07-18 13:57:25', timestamp=1658123845838}
source stream> UserLoginEventBean{userId='u3', loginAddress='北京', loginType='fail', loginTime='2022-07-18 13:57:33', timestamp=1658123853842}
正常匹配> u3登录失败

未完,更新中...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值