FlinkCEP复杂事件处理(Complex Event Processing)

概述:

flink官方文档:https://nightlies.apache.org/flink/flink-docs-release-1.19/zh/docs/libs/cep/

所谓 CEP,其实就是“复杂事件处理(Complex Event Processing)”的缩写;而 Flink CEP,
就是 Flink 实现的一个用于复杂事件处理的库(library)。
那到底什么是“复杂事件处理”呢?就是可以在事件流里,检测到特定的事件组合并进行
处理,比如说“连续登录失败”,或者“订单支付超时”等等。
具体的处理过程是,把事件流中的一个个简单事件,通过一定的规则匹配组合起来,这就
是“复杂事件”;然后基于这些满足规则的一组组复杂事件进行转换处理,得到想要的结果进行
输出。
总结起来,复杂事件处理(CEP)的流程可以分成三个步骤:
(1)定义一个匹配规则
(2)将匹配规则应用到事件流上,检测满足规则的复杂事件
(3)对检测到的复杂事件进行处理,得到结果进行输出
在这里插入图片描述
输入是不同形状的事件流,我们可以定义一个匹配规则:在圆形后面紧
跟着三角形。那么将这个规则应用到输入流上,就可以检测到三组匹配的复杂事件。它们构成
了一个新的“复杂事件流”,流中的数据就变成了一组一组的复杂事件,每个数据都包含了一
个圆形和一个三角形。接下来,我们就可以针对检测到的复杂事件,处理之后输出一个提示或
报警信息了。
所以,CEP 是针对流处理而言的,分析的是低延迟、频繁产生的事件流。它的主要目的,
就是在无界流中检测出特定的数据组合,让我们有机会掌握数据中重要的高阶特征。

模式(Pattern)

CEP 的第一步所定义的匹配规则,我们可以把它叫作“模式”(Pattern)。模式的定义主要
就是两部分内容:
⚫ 每个简单事件的特征
⚫ 简单事件之间的组合关系
当然,我们也可以进一步扩展模式的功能。比如,匹配检测的时间限制;每个简单事件是
否可以重复出现;对于事件可重复出现的模式,遇到一个匹配后是否跳过后面的匹配;等等。
所谓“事件之间的组合关系”,一般就是定义“谁后面接着是谁”,也就是事件发生的顺序。
我们把它叫作“近邻关系”。可以定义严格的近邻关系,也就是两个事件之前不能有任何其他
事件;也可以定义宽松的近邻关系,即只要前后顺序正确即可,中间可以有其他事件。另外,
还可以反向定义,也就是“谁后面不能跟着谁”。
CEP 做的事其实就是在流上进行模式匹配。根据模式的近邻关系条件不同,可以检测连
续的事件或不连续但先后发生的事件;模式还可能有时间的限制,如果在设定时间范围内没有
满足匹配条件,就会导致模式匹配超时(timeout)。
Flink CEP 为我们提供了丰富的 API,可以实现上面关于模式的所有功能,这套 API 就叫
作“模式 API”(Pattern API)

快速上手

依赖

<dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-cep -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-cep</artifactId>
            <version>1.17.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients</artifactId>
            <version>1.17.0</version>
        </dependency>
</dependencies>

简单实例

检测用户行为,如果连续三次登录失败,就输出报警信
息。很显然,这是一个复杂事件的检测处理,我们可以使用 Flink CEP 来实现。

public class LoginEvent {
 public String userId;
 public String ipAddress;
 public String eventType;
 public Long timestamp;
 public LoginEvent(String userId, String ipAddress, String eventType, Long 
timestamp) {
 this.userId = userId;
 this.ipAddress = ipAddress;
 this.eventType = eventType;
 this.timestamp = timestamp;
 }
 public LoginEvent() {}
 @Override
 public String toString() {
 return "LoginEvent{" +
 "userId='" + userId + '\'' +
 ", ipAddress='" + ipAddress + '\'' +
 ", eventType='" + eventType + '\'' +
 ", timestamp=" + timestamp +
 '}';
 }
}
package com.zxl.cep;

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.KeyedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import java.util.List;
import java.util.Map;

public class LoginFailDetect {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env =
                StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        // 获取登录事件流,并提取时间戳、生成水位线
        KeyedStream<LoginEvent, String> stream = env
                .fromElements(
                        new LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
                        new LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
                        new LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
                        new LoginEvent("user_2", "192.168.1.29", "success", 6000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 7000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 8000L)
                )
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<LoginEvent>forMonotonousTimestamps()
                                .withTimestampAssigner(
                                        new SerializableTimestampAssigner<LoginEvent>() {
                                            @Override
                                            public long extractTimestamp(LoginEvent loginEvent, long l) {
                                                return loginEvent.timestamp;
                                            }
                                        }
                                )
                )
                .keyBy(r -> r.userId);
        // TODO: 2024/6/2   1. 定义 Pattern,连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> pattern = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("second") // 接着是第二个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("third") // 接着是第三个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                });
        // 2. 将 Pattern 应用到流上,检测匹配的复杂事件,得到一个 PatternStream
        PatternStream<LoginEvent> patternStream = CEP.pattern(stream, pattern);
        // 3. 将匹配到的复杂事件选择出来,然后包装成字符串报警信息输出
        patternStream
                .select(new PatternSelectFunction<LoginEvent, String>() {
                    @Override
                    public String select(Map<String, List<LoginEvent>> map) throws
                            Exception {
                        LoginEvent first = map.get("first").get(0);
                        LoginEvent second = map.get("second").get(0);
                        LoginEvent third = map.get("third").get(0);
                        return first.userId + " 连续三次登录失败!登录时间:" +
                                first.timestamp + ", " + second.timestamp + ", " + third.timestamp;
                    }
                })
                .print("warning");
        env.execute();
    }
}

在这里插入图片描述

不连续的三个事件匹配

Pattern<LoginEvent, LoginEvent> pattern = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .followedBy("second") // 接着是第二个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .followedBy("third") // 接着是第三个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                });

在这里插入图片描述

模式 API(Pattern API)

单个模式

一个模式可以是一个单例或者循环模式。单例模式只接受一个事件,循环模式可以接受多个事件。 在模式匹配表达式中,模式"a b+ c? d"(或者"a",后面跟着一个或者多个"b",再往后可选择的跟着一个"c",最后跟着一个"d"), a,c?,和 d都是单例模式,b+是一个循环模式。默认情况下,模式都是单例的,你可以通过使用量词把它们转换成循环模式。 每个模式可以有一个或者多个条件来决定它接受哪些事件。

模式(Pattern)其实就是将一组简单事件组合成复杂事件
的“匹配规则”。由于流中事件的匹配是有先后顺序的,因此一个匹配规则就可以表达成先后发
生的一个个简单事件,按顺序串联组合在一起。
这里的每一个简单事件并不是任意选取的,也需要有一定的条件规则;所以我们就把每个
简单事件的匹配规则,叫作“个体模式”(Individual Pattern)。

基本形式

每一个登录失败事件的选取规则,就都是一个个体模式。比如:

.<LoginEvent>begin("first") // 以第一个登录失败事件开始
 .where(new SimpleCondition<LoginEvent>() {
 @Override
 public boolean filter(LoginEvent loginEvent) throws Exception {
 return loginEvent.eventType.equals("fail");
 }
 })
或者后面的:
.next("second") // 接着是第二个登录失败事件
 .where(new SimpleCondition<LoginEvent>() {
 @Override
 public boolean filter(LoginEvent loginEvent) throws Exception {
 return loginEvent.eventType.equals("fail");
 }
 })

这些都是个体模式。个体模式一般都会匹配接收一个事件。
每个个体模式都以一个“连接词”开始定义的,比如 begin、next 等等,这是 Pattern 对象
的一个方法(begin 是 Pattern 类的静态方法),返回的还是一个 Pattern。这些“连接词”方法
有一个 String 类型参数,这就是当前个体模式唯一的名字,比如这里的“first”、“second”。在
之后检测到匹配事件时,就会以这个名字来指代匹配事件。
个体模式需要一个“过滤条件”,用来指定具体的匹配规则。这个条件一般是通过调
用.where()方法来实现的,具体的过滤逻辑则通过传入的 SimpleCondition 内的.filter()方法来定
义。
另外,个体模式可以匹配接收一个事件,也可以接收多个事件。这听起来有点奇怪,一个
单独的匹配规则可能匹配到多个事件吗?这是可能的,我们可以给个体模式增加一个“量词”
(quantifier),就能够让它进行循环匹配,接收多个事件。接下来我们就对量词和条件(condition)
进行展开说明。

量词(Quantifiers )

个体模式后面可以跟一个“量词”,用来指定循环的次数。从这个角度分类,个体模式可
以包括“单例(singleton)模式”和“循环(looping)模式”。默认情况下,个体模式是单例
模式,匹配接收一个事件;当定义了量词之后,就变成了循环模式,可以匹配接收多个事件。
在循环模式中,对同样特征的事件可以匹配多次。比如我们定义个体模式为“匹配形状为
三角形的事件”,再让它循环多次,就变成了“匹配连续多个三角形的事件”。注意这里的“连
续”,只要保证前后顺序即可,中间可以有其他事件,所以是“宽松近邻”关系。
在 Flink CEP 中,可以使用不同的方法指定循环模式,主要有:
在这里插入图片描述
未加量词前:
在这里插入图片描述

⚫ .oneOrMore()
匹配事件出现一次或多次,假设 a 是一个个体模式,a.oneOrMore()表示可以匹配 1 个或多
个 a 的事件组合。我们有时会用 a+来简单表示。

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> pattern = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("second") // 接着是第二个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("third") // 接着是第三个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                }).oneOrMore(); ---添加量词oneOrMore()

在这里插入图片描述
⚫ .times(times)
匹配事件发生特定次数(times),例如 a.times(3)表示 aaa;

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> pattern = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("second") // 接着是第二个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("third") // 接着是第三个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                }).times(2); //匹配事件发生特定次数(times)

在这里插入图片描述
所以只有两条数据能匹配三次
在这里插入图片描述
把times参数修改为2就能匹配三条
在这里插入图片描述
在这里插入图片描述
⚫ .times(fromTimes,toTimes)
指定匹配事件出现的次数范围,最小次数为fromTimes,最大次数为toTimes。例如a.times(2,
4)可以匹配 aa,aaa 和 aaaa。

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> pattern = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("second") // 接着是第二个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("third") // 接着是第三个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                }).times(2,3); //匹配事件发生特定次数(times)

在这里插入图片描述

⚫ .greedy() 只能用在循环模式后,使当前循环模式变得“贪心”(greedy),也就是总是尽可能多地去 匹配。例如 a.times(2, 4).greedy(),如果出现了连续 4 个 a,那么会直接把 aaaa 检测出来进行处 理,其他任意 2 个 a 是不算匹配事件的。

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> pattern = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("second") // 接着是第二个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("third") // 接着是第三个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                }).times(2,3).greedy(); //匹配事件发生特定次数(times)并且尽可能的重复次数多

在这里插入图片描述

⚫ .optional()
使当前模式成为可选的,也就是说可以满足这个匹配条件,也可以不满足。

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> optional = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("second") // 接着是第二个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                })
                .next("third") // 接着是第三个登录失败事件
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                }).times(2, 3).optional();// 期望出现0、2、3次

在这里插入图片描述

对于一个个体模式 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();

正是因为个体模式可以通过量词定义为循环模式,一个模式能够匹配到多个事件,所以之
前代码中事件的检测接收才会用 Map 中的一个列表(List)来保存。而之前代码中没有定义量
词,都是单例模式,所以只会匹配一个事件,每个 List 中也只有一个元素:

LoginEvent first = map.get("first").get(0);

条件(Conditions)

对于每个个体模式,匹配事件的核心在于定义匹配条件,也就是选取事件的规则。Flink
CEP 会按照这个规则对流中的事件进行筛选,判断是否接受当前的事件。
对于条件的定义,主要是通过调用 Pattern 对象的.where()方法来实现的,主要可以分为简
单条件、迭代条件、复合条件、终止条件几种类型。此外,也可以调用 Pattern 对象的.subtype()
方法来限定匹配事件的子类型。接下来我们就分别进行介绍。

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

pattern.subtype(SubEvent.class);

这里 SubEvent 是流中数据类型 Event 的子类型。这时,只有当事件是 SubEvent 类型时,
才可以满足当前模式 pattern 的匹配条件。
⚫ 简单条件(Simple Conditions)
简单条件是最简单的匹配规则,只根据当前事件的特征来决定是否接受它。这在本质上其
实就是一个 filter 操作。
代码中我们为.where()方法传入一个 SimpleCondition 的实例作为参数。SimpleCondition 是
表示“简单条件”的抽象类,内部有一个.filter()方法,唯一的参数就是当前事件。所以它可以
当作 FilterFunction 来使用。
下面是一个具体示例:

pattern.where(new SimpleCondition<Event>() {
 @Override
 public boolean filter(Event value) {
 return value.user.startsWith("A");
 }
});

这里我们要求匹配事件的 user 属性以“A”开头。
⚫ 迭代条件(Iterative Conditions)
简单条件只能基于当前事件做判断,能够处理的逻辑比较有限。在实际应用中,我们可能
需要将当前事件跟之前的事件做对比,才能判断出要不要接受当前事件。这种需要依靠之前事
件来做判断的条件,就叫作“迭代条件”(Iterative Condition)。
在 Flink CEP 中,提供了 IterativeCondition 抽象类。这其实是更加通用的条件表达,查看
源码可以发现, .where()方法本身要求的参数类型就是 IterativeCondition;而之前 的
SimpleCondition 是它的一个子类。
在 IterativeCondition 中同样需要实现一个 filter()方法,不过与 SimpleCondition 中不同的
是,这个方法有两个参数:除了当前事件之外,还有一个上下文 Context。调用这个上下文
的.getEventsForPattern()方法,传入一个模式名称,就可以拿到这个模式中已匹配到的所有数
据了。
下面是一个具体示例:

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> optional = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .where(new IterativeCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent, Context<LoginEvent> context) throws Exception {
                        if (loginEvent.ipAddress.equals("192.168.1.29") && loginEvent.eventType.equals("fail")) {
                            return true;
                        } else {
                            return false;
                        }
                    }
                })
                .next("second") // 接着是第二个登录失败事件
                .where(new IterativeCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent, Context<LoginEvent> context) throws Exception {
                        if (loginEvent.ipAddress.equals("192.168.1.29") && loginEvent.eventType.equals("fail")) {
                            return true;
                        } else {
                            return false;
                        }
                    }
                }).times(2, 3);//匹配事件发生特定次数(times)

在这里插入图片描述
在这里插入图片描述
上面代码中当前模式名称就叫作“middle”,这是一个循环模式,可以接受事件发生一次
或多次。于是下面的迭代条件中,我们通过 ctx.getEventsForPattern(“middle”)获取当前模式已
经接受的事件,计算它们的数量(amount)之和;再加上当前事件中的数量,如果总和小于
100,就接受当前事件,否则就不匹配。当然,在迭代条件中我们也可以基于当前事件做出判
断,比如代码中要求 user 必须以 A 开头。最终我们的匹配规则就是:事件的 user 必须以 A 开
头;并且循环匹配的所有事件 amount 之和必须小于 100。这里的 Event 与之前定义的 POJO 不
同,增加了 amount 属性。
可以看到,迭代条件能够获取已经匹配的事件,如果自身又是循环模式(比如量词
oneOrMore),那么两者结合就可以捕获自身之前接收的数据,据此来判断是否接受当前事件。
这个功能非常强大,我们可以由此实现更加复杂的需求,比如可以要求“只有大于之前数据的
平均值,才接受当前事件”。
另外迭代条件中的上下文 Context 也可以获取到时间相关的信息,比如事件的时间戳和当
前的处理时间(processing time)。
⚫ 组合条件(Combining Conditions)
如果一个个体模式有多个限定条件,又该怎么定义呢?
最直接的想法是,可以在简单条件或者迭代条件的.filter()方法中,增加多个判断逻辑。可
以通过 if-else 的条件分支分别定义多个条件,也可以直接在 return 返回时给一个多条件的逻辑
组合(与、或、非)。不过这样会让代码变得臃肿,可读性降低。更好的方式是独立定义多个
条件,然后在外部把它们连接起来,构成一个“组合条件”(Combining Condition)。
最简单的组合条件,就是.where()后面再接一个.where()。因为前面提到过,一个条件就像
是一个 filter 操作,所以每次调用.where()方法都相当于做了一次过滤,连续多次调用就表示多
重过滤,最终匹配的事件自然就会同时满足所有条件。这相当于就是多个条件的“逻辑与”
(AND)。
而多个条件的逻辑或(OR),则可以通过.where()后加一个.or()来实现。这里的.or()方法
与.where()一样,传入一个 IterativeCondition 作为参数,定义一个独立的条件;它和之前.where()
定义的条件只要满足一个,当前事件就可以成功匹配。
当然,子类型限定条件(subtype)也可以和其他条件结合起来,成为组合条件,如下所
示:

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> optional = Pattern
                .<LoginEvent>begin("first") // 以第一个登录失败事件开始
                .subtype(LoginEvent.class)
                .where(new IterativeCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent, Context<LoginEvent> context) throws Exception {
                        if (loginEvent.ipAddress.equals("192.168.1.29") && loginEvent.eventType.equals("fail")) {
                            return true;
                        } else {
                            return false;
                        }
                    }
                })
                .next("second") // 接着是第二个登录失败事件
                .subtype(LoginEvent.class)
                .where(new IterativeCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent, Context<LoginEvent> context) throws Exception {
                        if (loginEvent.ipAddress.equals("192.168.1.29") && loginEvent.eventType.equals("fail")) {
                            return true;
                        } else {
                            return false;
                        }
                    }
                }).times(2, 3);//匹配事件发生特定次数(times)

这里可以看到,SimpleCondition 的泛型参数也变成了 SubEvent,所以匹配出的事件就既
满足子类型限制,又符合过滤筛选的简单条件;这也是一个逻辑与的关系。
⚫ 终止条件(Stop Conditions)
对于循环模式而言,还可以指定一个“终止条件”(Stop Condition),表示遇到某个特定事
件时当前模式就不再继续循环匹配了。
终 止 条 件 的 定 义 是 通 过 调 用 模 式 对 象 的 .until() 方 法 来 实 现 的 , 同 样 传 入 一 个
IterativeCondition 作为参数。需要注意的是,终止条件只与 oneOrMore() 或 者
oneOrMore().optional()结合使用。因为在这种循环模式下,我们不知道后面还有没有事件可以
匹配,只好把之前匹配的事件作为状态缓存起来继续等待,这等待无穷无尽;如果一直等下去,
缓存的状态越来越多,最终会耗尽内存。所以这种循环模式必须有个终点,当.until()指定的条
件满足时,循环终止,这样就可以清空状态释放内存了。

组合模式

有了定义好的个体模式,就可以尝试按一定的顺序把它们连接起来,定义一个完整的复杂
事件匹配规则了。这种将多个个体模式组合起来的完整模式,就叫作“组合模式”(Combining
Pattern),为了跟个体模式区分有时也叫作“模式序列”(Pattern Sequence)。
一个组合模式有以下形式:

Pattern<Event, ?> pattern = Pattern
.<Event>begin("start").where(...)
 .next("next").where(...)
 .followedBy("follow").where(...)
 ...

可以看到,组合模式确实就是一个“模式序列”,是用诸如 begin、next、followedBy 等表
示先后顺序的“连接词”将个体模式串连起来得到的。在这样的语法调用中,每个事件匹配的
条件是什么、各个事件之间谁先谁后、近邻关系如何都定义得一目了然。每一个“连接词”方
法调用之后,得到的都仍然是一个 Pattern 的对象;所以从 Java 对象的角度看,组合模式与个
体模式是一样的,都是 Pattern。

初始模式(Initial Pattern)

所有的组合模式,都必须以一个“初始模式”开头;而初始模式必须通过调用 Pattern 的
静态方法.begin()来创建。如下所示:

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

这里我们调用 Pattern 的.begin()方法创建了一个初始模式。传入的 String 类型的参数就是
模式的名称;而 begin 方法需要传入一个类型参数,这就是模式要检测流中事件的基本类型,
这里我们定义为 Event。调用的结果返回一个 Pattern 的对象实例。Pattern 有两个泛型参数,第
一个就是检测事件的基本类型 Event,跟 begin 指定的类型一致;第二个则是当前模式里事件
的子类型,由子类型限制条件指定。我们这里用类型通配符(?)代替,就可以从上下文直接
推断了。

近邻条件(Contiguity Conditions)

在初始模式之后,我们就可以按照复杂事件的顺序追加模式,组合成模式序列了。模式之
间的组合是通过一些“连接词”方法实现的,这些连接词指明了先后事件之间有着怎样的近邻
关系,这就是所谓的“近邻条件”(Contiguity Conditions,也叫“连续性条件”)。
Flink CEP 中提供了三种近邻关系:
⚫ 严格近邻(Strict Contiguity)
如图 12-2 所示,匹配的事件严格地按顺序一个接一个出现,中间不会有任何其他事件。
代码中对应的就是 Pattern 的.next()方法,名称上就能看出来,“下一个”自然就是紧挨着的。

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

在这里插入图片描述
⚫ 非确定性宽松近邻(Non-Deterministic Relaxed Contiguity)
这种近邻关系更加宽松。所谓“非确定性”是指可以重复使用之前已经匹配过的事件;这
种近邻条件下匹配到的不同复杂事件,可以以同一个事件作为开始,所以匹配结果一般会比宽
松近邻更多,如图 11-3 所示。代码中对应.followedByAny()方法。

在这里插入图片描述
从图中可以看到,我们定义的模式序列中有两个个体模式:一是“选择圆形事件”,一是“选
择三角形事件”;这时它们之间的近邻条件就会导致匹配出的复杂事件有所不同。很明显,严
格近邻由于条件苛刻,匹配的事件最少;宽松近邻可以匹配不紧邻的事件,匹配结果会多一些;
而非确定性宽松近邻条件最为宽松,可以匹配到最多的复杂事件。
其他限制条件
除了上面提到的 next()、followedBy()、followedByAny()可以分别表示三种近邻条件,我
们还可以用否定的“连接词”来组合个体模式。主要包括:
⚫ .notNext()
表示前一个模式匹配到的事件后面,不能紧跟着某种事件。

⚫ .notFollowedBy()
表示前一个模式匹配到的事件后面,不会出现某种事件。这里需要注意,由于
notFollowedBy()是没有严格限定的;流数据不停地到来,我们永远不能保证之后“不会出现某
种事件”。所以一个模式序列不能以 notFollowedBy()结尾,这个限定条件主要用来表示“两个
事件中间不会出现某种事件”。

另外,Flink CEP 中还可以为模式指定一个时间限制,这是通过调用.within()方法实现的。
方法传入一个时间参数,这是模式序列中第一个事件到最后一个事件之间的最大时间间隔,只
有在这期间成功匹配的复杂事件才是有效的。一个模式序列中只能有一个时间限制,调
用.within()的位置不限;如果多次调用则会以最小的那个时间间隔为准。
下面是模式序列中所有限制条件在代码中的定义:

// 严格近邻条件
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));

循环模式中的近邻条件
之前我们讨论的都是模式序列中限制条件,主要用来指定前后发生的事件之间的近邻关系。
而循环模式虽说是个体模式,却也可以匹配多个事件;那这些事件之间自然也会有近邻关系的
讨论。
在循环模式中,近邻关系同样有三种:严格近邻、宽松近邻以及非确定性宽松近邻。对于
定义了量词(如 oneOrMore()、times())的循环模式,默认内部采用的是宽松近邻。也就是说,
当循环匹配多个事件时,它们中间是可以有其他不匹配事件的;相当于用单例模式分别定义、
再用 followedBy()连接起来。这就解释了在 12.2.2 小节的示例代码中,为什么我们检测连续三
次登录失败用了三个单例模式来分别定义,而没有直接指定 times(3):因为我们需要三次登录
失败必须是严格连续的,中间不能有登录成功的事件,而 times()默认是宽松近邻关系。
不过把多个同样的单例模式组合在一起,这种方式还是显得有些笨拙了。连续三次登录失
败看起来不太复杂,那如果要检测连续 100 次登录失败呢?显然使用 times()是更明智的选择。
不过它默认匹配事件之间是宽松近邻关系,我们可以通过调用额外的方法来改变这一点。
⚫ .consecutive()
为循环模式中的匹配事件增加严格的近邻条件,保证所有匹配事件是严格连续的。也就是
说,一旦中间出现了不匹配的事件,当前循环检测就会终止。这起到的效果跟模式序列中的
next()一样,需要与循环量词 times()、oneOrMore()配合使用。

于是,检测连续三次登录失败的代码可以改成:

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.KeyedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import java.util.List;
import java.util.Map;

public class LoginFailDetect {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env =
                StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        // 获取登录事件流,并提取时间戳、生成水位线
        KeyedStream<LoginEvent, String> stream = env
                .fromElements(
                        new LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
                        new LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
                        new LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
                        new LoginEvent("user_2", "192.168.1.28", "success", 6000L),
                        new LoginEvent("user_2", "192.168.1.28", "fail", 7000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 8000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 9000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 10000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 11000L),
                        new LoginEvent("user_2", "192.168.1.29", "fail", 12000L)

                )
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<LoginEvent>forMonotonousTimestamps()
                                .withTimestampAssigner(
                                        new SerializableTimestampAssigner<LoginEvent>() {
                                            @Override
                                            public long extractTimestamp(LoginEvent loginEvent, long l) {
                                                return loginEvent.timestamp;
                                            }
                                        }
                                )
                )
                .keyBy(r -> r.userId);

        // TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件

        Pattern<LoginEvent, LoginEvent> patternss = Pattern
                .<LoginEvent>begin("fails")
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                }).times(3).consecutive();

        // TODO: 2024/6/2   2. 将 Pattern 应用到流上,检测匹配的复杂事件,得到一个 PatternStream
        PatternStream<LoginEvent> pattern = CEP.pattern(stream, patternss);
        // TODO: 2024/6/2   3. 将匹配到的复杂事件选择出来,然后包装成字符串报警信息输出
        pattern
                .select(new PatternSelectFunction<LoginEvent, Object>() {

                    @Override
                    public Object select(Map<String, List<LoginEvent>> map) throws Exception {
                        for (int i = 0; i < map.get("fails").size(); i++) {
                            System.out.println("登录时间" + map.get("fails").get(i).timestamp);
                        }
                        return " 连续"+ map.get("fails").size() +"次登录失败!";
                        
                    }
                })
                .print("warning");
        env.execute();
    }
}

在这里插入图片描述

这样显得更加简洁;而且即使要扩展到连续 100 次登录失败,也只需要改动一个参数而已。
不过这样一来,后续提取匹配事件的方式也会有所不同。
⚫ .allowCombinations()
除严格近邻外,也可以为循环模式中的事件指定非确定性宽松近邻条件,表示可以重复使
用 已 经 匹 配 的 事 件 。 这 需 要 调 用 .allowCombinations() 方 法 来 实 现 , 实 现 的 效 果
与.followedByAny()相同。

 // TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件

        Pattern<LoginEvent, LoginEvent> patternss = Pattern
                .<LoginEvent>begin("fails")
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                }).times(3).allowCombinations(); //非确定性宽松近邻条件allowCombinations()

在这里插入图片描述

模式组

一般来说,代码中定义的模式序列,就是我们在业务逻辑中匹配复杂事件的规则。不过在
有些非常复杂的场景中,可能需要划分多个“阶段”,每个“阶段”又有一连串的匹配规则。为了
应对这样的需求,Flink CEP 允许我们以“嵌套”的方式来定义模式。

之前在模式序列中,我们用 begin()、next()、followedBy()、followedByAny()这样的“连
接词”来组合个体模式,这些方法的参数就是一个个体模式的名称;而现在它们可以直接以一
个模式序列作为参数,就将模式序列又一次连接组合起来了。这样得到的就是一个“模式组”
(Groups of Patterns)。
在模式组中,每一个模式序列就被当作了某一阶段的匹配条件,返回的类型是一个
GroupPattern。而 GroupPattern 本身是 Pattern 的子类;所以个体模式和组合模式能调用的方法,
比如 times()、oneOrMore()、optional()之类的量词,模式组一般也是可以用的。

// 以模式序列作为初始模式
Pattern<Event, ?> start = Pattern.begin(
Pattern.<Event>begin("start_start").where(...)
.followedBy("start_middle").where(...)
);
// 在 start 后定义严格近邻的模式序列,并重复匹配两次
Pattern<Event, ?> strict = start.next(
Pattern.<Event>begin("next_start").where(...)
.followedBy("next_middle").where(...)
).times(2);
// 在 start 后定义宽松近邻的模式序列,并重复匹配一次或多次
Pattern<Event, ?> relaxed = start.followedBy(
Pattern.<Event>begin("followedby_start").where(...)
**加粗样式**.followedBy("followedby_middle").where(...)
).oneOrMore();
//在 start 后定义非确定性宽松近邻的模式序列,可以匹配一次,也可以不匹配
Pattern<Event, ?> nonDeterminRelaxed = start.followedByAny(
Pattern.<Event>begin("followedbyany_start").where(...)
.followedBy("followedbyany_middle").where(...)
).optional();

一个规则:

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> eventPattern = Pattern.begin(
                Pattern.<LoginEvent>begin("start").where(new SimpleCondition<LoginEvent>() {
                            @Override
                            public boolean filter(LoginEvent loginEvent) throws Exception {
                                return loginEvent.eventType.equals("fail");
                            }
                        })
                        .followedBy("first").where(new SimpleCondition<LoginEvent>() {
                            @Override
                            public boolean filter(LoginEvent loginEvent) throws Exception {
                                return loginEvent.eventType.equals("fail");
                            }
                        })
        );

输出的数据:

{start=[LoginEvent{userId='user_1', ipAddress='192.168.0.1', eventType='fail', timestamp=2000}], first=[LoginEvent{userId='user_1', ipAddress='192.168.0.2', eventType='fail', timestamp=3000}]}
warning> 
{start=[LoginEvent{userId='user_1', ipAddress='192.168.0.2', eventType='fail', timestamp=3000}], first=[LoginEvent{userId='user_1', ipAddress='171.56.23.10', eventType='fail', timestamp=5000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=4000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.28', eventType='fail', timestamp=7000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.28', eventType='fail', timestamp=7000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=8000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=8000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=9000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=9000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=10000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=10000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=11000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=11000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=12000}]}
warning> 

两个规则嵌套形成模式组

// TODO: 2024/6/2   1. 定义 Pattern,不连续的三个登录失败事件
        Pattern<LoginEvent, LoginEvent> eventPattern = Pattern.begin(
                Pattern.<LoginEvent>begin("start").where(new SimpleCondition<LoginEvent>() {
                            @Override
                            public boolean filter(LoginEvent loginEvent) throws Exception {
                                return loginEvent.eventType.equals("fail");
                            }
                        })
                        .followedBy("first").where(new SimpleCondition<LoginEvent>() {
                            @Override
                            public boolean filter(LoginEvent loginEvent) throws Exception {
                                return loginEvent.eventType.equals("fail");
                            }
                        })
        );
        // TODO: 2024/6/2 在上一个规则的基础上在进行设定规则
        Pattern<LoginEvent, LoginEvent> loginEventPattern = eventPattern.next("two")
                .where(new SimpleCondition<LoginEvent>() {
                    @Override
                    public boolean filter(LoginEvent loginEvent) throws Exception {
                        return loginEvent.eventType.equals("fail");
                    }
                }).times(2);

可以发现4000L到7000数据已经无法匹配到

{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=4000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.28', eventType='fail', timestamp=7000}], two=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=8000}, LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=9000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.28', eventType='fail', timestamp=7000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=8000}], two=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=9000}, LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=10000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=8000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=9000}], two=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=10000}, LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=11000}]}
warning> 
{start=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=9000}], first=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=10000}], two=[LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=11000}, LoginEvent{userId='user_2', ipAddress='192.168.1.29', eventType='fail', timestamp=12000}]}
warning> 

匹配后跳过策略

在 Flink CEP 中,由于有循环模式和非确定性宽松近邻的存在,同一个事件有可能会重复
利用,被分配到不同的匹配结果中。这样会导致匹配结果规模增大,有时会显得非常冗余。当
然,非确定性宽松近邻条件,本来就是为了放宽限制、扩充匹配结果而设计的;我们主要是针
对循环模式来考虑匹配结果的精简。
之前已经讲过,如果对循环模式增加了.greedy()的限制,那么就会“尽可能多地”匹配事
件,这样就可以砍掉那些子集上的匹配了。不过这种方式还是略显简单粗暴,如果我们想要精
确控制事件的匹配应该跳过哪些情况,那就需要制定另外的策略了。

五种跳过策略

NO_SKIP: 每个成功的匹配都会被输出。
SKIP_TO_NEXT: 丢弃以相同事件开始的所有部分匹配。
SKIP_PAST_LAST_EVENT: 丢弃起始在这个匹配的开始和结束之间的所有部分匹配。
SKIP_TO_FIRST: 丢弃起始在这个匹配的开始和第一个出现的名称为PatternName事件之间的所有部分匹配。
SKIP_TO_LAST: 丢弃起始在这个匹配的开始和最后一个出现的名称为PatternName事件之间的所有部分匹配。

在 Flink CEP 中,提供了模式的“匹配后跳过策略”(After Match Skip Strategy),专门用
来精准控制循环模式的匹配结果。这个策略可以在 Pattern 的初始模式定义中,作为 begin()的
第二个参数传入:

Pattern.begin("start", AfterMatchSkipStrategy.noSkip())
.where(...)
 ...

匹配后跳过策略 AfterMatchSkipStrategy 是一个抽象类,它有多个具体的实现,可以通过
调用对应的静态方法来返回对应的策略实例。这里我们配置的是不做跳过处理,这也是默认策
略。
下面我们举例来说明不同的跳过策略。例如我们要检测的复杂事件模式为:开始是用户名
为 a 的事件(简写为事件 a,下同),可以重复一次或多次;然后跟着一个用户名为 b 的事件,
a 事件和 b 事件之间可以有其他事件(宽松近邻)。用简写形式可以直接写作:“a+ followedBy
b”。在代码中定义 Pattern 如下:

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 a a b”——这里为了区分前后不同的 a 事件,可以记作“a1 a2
a3 b”——那么应该检测到 6 个匹配结果:(a1 a2 a3 b),(a1 a2 b),(a1 b),(a2 a3 b),(a2 b),(a3 b)。如果在初始模式的量词.oneOrMore()后加上.greedy()定义为贪心匹配,那么结果就是:(a1 a2 a3 b),(a2 a3 b),(a3 b),每个事件作为开头只会出现一次。
接下来我们讨论不同跳过策略对匹配结果的影响:
⚫ 不跳过(NO_SKIP)
代码调用 AfterMatchSkipStrategy.noSkip()。这是默认策略,所有可能的匹配都会输出。所
以这里会输出完整的 6 个匹配。

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

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

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

⚫ 跳至最后一个(SKIP_TO_LAST[a])
代码调用 AfterMatchSkipStrategy.skipToLast(“a”),同样传入一个参数,指明跳至哪个模式
的最后一个匹配事件。找到 a1 开始的匹配(a1 a2 a3 b)后,跳过所有 a1、a2 开始的匹配,跳
到以最后一个 a(也就是 a3)为开始的匹配。最终得到(a1 a2 a3 b),(a3 b)。

  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值