4. Flink CEP核心API:Pattern API详解、个体模式与组合模式

好,咱们进入正题。Flink CEP 的核心,说白了就是 Pattern API。你想想看,没有 Pattern,你拿什么去定义“什么样的事件序列是异常的”?我刚开始接触 CEP 的时候,觉得这玩意儿就是个高级版的 if-else,后来才发现,它背后是一套完整的有限状态机理论。

今天我就带你把这套 API 拆开揉碎了讲。咱们从最基础的个体模式开始,再到组合模式,一步步把规则定义清楚。

4.1 个体模式(Single Pattern)—— 规则的原子单元

个体模式,就是一条规则里最小的、不可再分的事件匹配条件。比如“登录失败次数超过3次”,这里的“登录失败”就是一个个体模式。

在 Flink CEP 里,个体模式通过 Pattern.begin()Pattern.next() 等方法创建,然后通过 .where() 来定义过滤条件。

核心方法一览:

  • .where(condition):定义事件必须满足的条件
  • .or(condition):定义“或”关系,满足其一即可
  • .until(condition):定义终止条件(用于循环模式)
  • .times(n):指定事件出现的次数
  • .oneOrMore():一次或多次
  • .timesOrMore(n):n次或更多
  • .optional():可选出现
  • .greedy():贪婪匹配

举个例子,假设我们要检测“连续3次登录失败”:

Pattern<LoginEvent, LoginEvent> failPattern = Pattern
    .<LoginEvent>begin("first_fail")
    .where(new SimpleCondition<LoginEvent>() {
        @Override
        public boolean filter(LoginEvent event) {
            return event.getStatus().equals("FAIL");
        }
    })
    .times(3);

这里 times(3) 表示这个“失败”事件要连续出现3次。嗯,这里要注意:times() 默认是严格连续的,中间不能有其他事件插入。

我的经验: 我在做支付风控时,遇到过一种情况:用户连续输错密码,但中间穿插了一次“忘记密码”操作。如果用 times(3) 严格连续,就会漏掉这种场景。后来我改用 .times(3).allowCombinations(),允许中间跳过一些不相关的事件,才把召回率提上来。

4.2 个体模式的进阶用法

除了简单的 .where(),个体模式还支持更复杂的条件定义。我个人习惯用 IterativeCondition,因为它可以引用之前匹配到的事件。

Pattern<LoginEvent, LoginEvent> failPattern = Pattern
    .<LoginEvent>begin("first_fail")
    .where(new IterativeCondition<LoginEvent>() {
        @Override
        public boolean filter(LoginEvent event, Context<LoginEvent> ctx) throws Exception {
            // 获取之前匹配到的第一个失败事件
            LoginEvent firstFail = ctx.getEventsForPattern("first_fail").iterator().next();
            // 判断两次失败是否在5分钟内
            return event.getTimestamp() - firstFail.getTimestamp() < 5 * 60 * 1000;
        }
    })
    .times(3);

你看,通过 ctx.getEventsForPattern(),我们可以拿到之前匹配到的事件,做跨事件的逻辑判断。这在风控场景里太常用了——比如“同一设备在短时间内多次登录失败”。

避坑指南: 我曾经在 IterativeCondition 里不小心用了 ctx.getEventsForPattern() 去获取还没匹配到的事件,结果抛了空指针。记住:你只能引用当前模式之前已经匹配到的模式名称,不能引用后面的。

4.3 组合模式(Combining Patterns)—— 把规则串起来

个体模式是积木块,组合模式就是把这些积木搭建成一个完整的规则。Flink CEP 提供了三种组合方式:

方法 含义 示例场景
.next() 严格连续:下一个事件必须紧跟着当前事件 “登录失败”后紧接着“转账”
.followedBy() 宽松连续:后续事件可以出现在任意位置 “登录失败”之后某时刻发生“转账”
.followedByAny() 非确定性宽松连续:允许事件被重复使用 同一个事件可以参与多个匹配

我画了一张图,帮你理解这三种组合方式的区别:

三种组合模式对比 事件序列: A B C D B .next() 严格连续: A → B → C 必须紧挨着出现 A B C ✓ 匹配 .followedBy() 宽松连续: A → B → C 中间可以跳过其他事件 A B C ✓ 匹配(跳过D) .followedByAny() 非确定性宽松: A → B → B 同一个B可以参与多个匹配 A B1 B2 ✓ 匹配(A→B1→B2)

说白了,.next() 最严格,.followedBy() 最常用,.followedByAny() 最灵活但也最容易产生大量匹配。

4.4 实战:构建一个完整的风控规则

咱们来写一个完整的例子。假设我们要检测“同一设备在5分钟内,先登录失败3次,然后成功登录,最后发起转账”这个风险模式。

Pattern<LoginEvent, LoginEvent> riskPattern = Pattern
    .<LoginEvent>begin("fail_attempts")
    .where(new SimpleCondition<LoginEvent>() {
        @Override
        public boolean filter(LoginEvent event) {
            return event.getStatus().equals("FAIL");
        }
    })
    .times(3)
    .next("success_login")
    .where(new SimpleCondition<LoginEvent>() {
        @Override
        public boolean filter(LoginEvent event) {
            return event.getStatus().equals("SUCCESS");
        }
    })
    .followedBy("transfer")
    .where(new SimpleCondition<LoginEvent>() {
        @Override
        public boolean filter(LoginEvent event) {
            return event.getType().equals("TRANSFER");
        }
    })
    .within(Time.minutes(5));  // 整个模式在5分钟内完成

关键点解析:

  • .times(3) 后面跟 .next(),表示3次失败后必须紧接着成功登录
  • .followedBy() 表示成功登录后,转账可以在任意时刻发生
  • .within(Time.minutes(5)) 是整个模式的超时时间,超过5分钟就不算

你想想看,如果没有 .within(),用户今天登录失败3次,明天成功登录,后天转账,这能算风险吗?显然不能。时间窗口是风控规则里必不可少的约束。

我的经验: 我在做电商风控时,发现很多规则漏报的原因就是时间窗口设得太短。比如“同一IP注册多个账号”,如果窗口只设1小时,那些慢速注册的羊毛党就抓不到。后来我改成6小时窗口,配合 .timesOrMore(5),召回率提升了30%。

4.5 组合模式中的循环与可选

除了基本的组合,Pattern API 还支持一些高级特性:

  • 循环模式.times(n).oneOrMore().timesOrMore(n)
  • 可选模式.optional(),表示这个事件可以出现也可以不出现
  • 贪婪匹配.greedy(),尽可能多地匹配事件

举个例子,检测“用户可能连续失败多次,然后成功登录”:

Pattern<LoginEvent, LoginEvent> pattern = Pattern
    .<LoginEvent>begin("fail_attempts")
    .where(new SimpleCondition<LoginEvent>() {
        @Override
        public boolean filter(LoginEvent event) {
            return event.getStatus().equals("FAIL");
        }
    })
    .oneOrMore()
    .greedy()  // 尽可能多地匹配失败事件
    .next("success_login")
    .where(new SimpleCondition<LoginEvent>() {
        @Override
        public boolean filter(LoginEvent event) {
            return event.getStatus().equals("SUCCESS");
        }
    });

这里 .greedy() 的作用是:如果有5次失败,它会匹配全部5次,而不是只匹配1次就结束。嗯,这个细节很容易被忽略,但实际影响很大。

避坑指南: 我曾经在 .oneOrMore() 后面忘了加 .greedy(),结果同一个事件序列产生了多个匹配结果(匹配1次、2次、3次...),导致告警风暴。后来我养成了习惯:只要用 oneOrMoretimesOrMore,就顺手加上 .greedy(),除非你有特殊需求。

4.6 小结

Pattern API 是 Flink CEP 的基石。个体模式定义“什么样的事件”,组合模式定义“事件之间的顺序关系”。

记住几个要点:

  • .where().or() 定义事件条件
  • .next().followedBy().followedByAny() 定义事件顺序
  • .times().oneOrMore() 定义事件次数
  • .within() 定义时间窗口
  • .greedy() 控制匹配策略

这些 API 组合起来,几乎能覆盖风控场景里 90% 的规则需求。剩下的 10%,咱们后面会讲到进阶技巧和自定义算子。


专注资料整理