摘要:CEP 篇讲了模式匹配的全景和 NFA 引擎,这篇把 Pattern API 本身磨透------它是 CEP 的"语言",语法细节直接决定匹配结果。文章聚焦四件事:序列语义三兄弟(next/followedBy/followedByAny 的精确差异,用同一事件序列演示)、循环模式与组合约束(times/consecutive/allowCombinations/greedy 的搭配)、时间约束与匹配跳过策略(within + AfterMatchSkipStrategy)、完整 CEP 作业装配(含 PatternProcessFunction 超时处理)。每个知识点都配代码和真实踩坑,读完能写出"5 分钟内连续 3 次失败"这类模式而不再被误报/漏报困扰。
关键词:Flink Pattern API、CEP、next/followedBy/followedByAny、times/consecutive/allowCombinations、greedy、within、AfterMatchSkipStrategy、PatternProcessFunction、TimedOutPartialMatchHandler、代码实现
一、Pattern API:CEP 的"语言",语法细节就是匹配结果
CEP 篇我们看了全景和 NFA 引擎,三个业务案例也跑通了。但很多同学在真实开发里会卡在"语法细节"上:为什么我写了 times(3) 结果还是误报?为什么 followedBy 和 followedByAny 结果不一样?为什么同一个模式跑出成百上千条告警?
答案都在 Pattern API 本身。这篇把它当一门小语言来拆:序列操作符、循环约束、时间与跳过策略、输出装配,每部分配代码和精确语义。
二、Pattern API 语法全景

一条模式 = 起点 + 序列操作符 + 条件 + 循环约束 + 时间 + 跳过策略。先记住骨架:
java
Pattern<Event, Event> p = Pattern
.<Event>begin("start") // 起点(模式名 = 结果 Map 的 key)
.where(条件) // 事件条件
.next("mid") // 序列操作符 + 下一个模式名
.where(条件)
.times(2).consecutive() // 循环约束 + 连续性修饰
.within(Time.minutes(5)); // 总时间窗
六个组件里,序列操作符和循环约束是最容易写错的两块,下面重点拆。
三、序列语义三兄弟:next / followedBy / followedByAny

用一个具体事件序列验证三兄弟的差异。输入序列:A → C → B1 → B2(时间 t=1...4),模式都是"A 后接 B":
java
// 输入:A(t=1) C(t=2) B1(t=3) B2(t=4)
// 注意:以下三个模式是独立的,分别绑定同一输入流跑
// ── ① A next B:严格连续 ──
Pattern<Event, Event> strict = Pattern
.<Event>begin("a").where(e -> e.type == A)
.next("b").where(e -> e.type == B);
// 结果:❌ 无匹配
// 原因:A(t=1) 后紧接的是 C,不是 B------严格连续要求 A 和 B 之间没有其他事件
// ── ② A followedBy B:宽松连续(主力操作符) ──
Pattern<Event, Event> relaxed = Pattern
.<Event>begin("a").where(e -> e.type == A)
.followedBy("b").where(e -> e.type == B);
// 结果:✅ 1 个匹配 (A, B1)
// 说明:C 被跳过;B2 不参与------宽松连续的匹配不允许共享事件(non-overlapping)
// 即 A 只能匹配一个 B
// ── ③ A followedByAny B:非确定宽松连续 ──
Pattern<Event, Event> nonDet = Pattern
.<Event>begin("a").where(e -> e.type == A)
.followedByAny("b").where(e -> e.type == B);
// 结果:✅ 2 个匹配 (A, B1) 和 (A, B2)
// 说明:与 followedBy 相同(可隔事件),但允许匹配重叠------同一个 A 匹配两个 B
// 代价:NFA 活跃实例翻倍,状态占用更高
三兄弟的差别浓缩成一句话:next/followedBy 决定"中间能不能隔事件",followedBy/followedByAny 决定"匹配能不能重叠"。工程选型:默认 followedBy;要求紧邻(如"两次失败之间不能有成功")用 next;确实需要"一个起点多个匹配"(如一个告警源关联多个故障事件)才用 followedByAny。
四、循环模式与组合约束:times 的默认语义是"宽松"的
循环模式(times/oneOrMore)是误报重灾区,因为 Flink 循环模式默认是宽松连续:
java
// ⚠️ 默认宽松:times(3) 允许中间夹着不匹配的事件
Pattern<Event, Event> loose = Pattern
.<Event>begin("f").where(e -> e.type == FAIL).times(3);
// 输入 [F1, OK, F2, F3] → ✅ 匹配!(F1 后夹了 OK 也算)
// ✅ 严格连续:必须 consecutive()
Pattern<Event, Event> strict = Pattern
.<Event>begin("f").where(e -> e.type == FAIL).times(3).consecutive();
// 输入 [F1, OK, F2, F3] → ❌ 不匹配(中间有 OK)
// 输入 [F1, F2, F3] → ✅ 匹配
// 更多组合:
Pattern<Event, Event> combo = Pattern
.<Event>begin("f").where(e -> e.type == FAIL)
.times(2, 4) // 2~4 次
.consecutive() // 严格连续
.greedy() // 贪婪:尽可能多消费(影响后续模式)
.followedBy("s").where(e -> e.type == SUCCESS);
// oneOrMore + until:循环直到出现某类事件
Pattern<Event, Event> loop = Pattern
.<Event>begin("f").where(e -> e.type == FAIL)
.oneOrMore() // 至少一次
.until(e -> e.type == RESET); // 出现 RESET 停止循环
// optional:该环节可缺失(如"可选的二次确认")
Pattern<Event, Event> opt = Pattern
.<Event>begin("login").where(e -> e.type == LOGIN)
.next("verify").where(e -> e.type == VERIFY).optional()
.next("ok").where(e -> e.type == OK);
记忆口诀:循环 = 次数(times/oneOrMore)+ 连续性(consecutive/allowCombinations)+ 消费方式(greedy)。三个最常踩的:
times(3)不连续 → 误报;业务上"连续 N 次"必须加consecutive();greedy()只影响"循环后还接模式"的场景------贪婪会让循环尽可能多吃,留给后续模式的更少;zeroOrMore()后面必须再接其他模式,否则循环永远等不到终止。
五、时间约束与匹配跳过策略
5.1 within:总时间窗口
java
Pattern<Event, Event> p = Pattern
.<Event>begin("a").where(...)
.next("b").where(...)
.within(Time.minutes(10)); // 整个模式必须在 10 分钟内完成(事件时间)
within 从首事件的时间戳 起算,超时部分匹配走侧输出(需要配合 select/flatSelect/process 的超时参数)。事件时间模式下没配 watermark,within 超时永远不触发------这是 CEP 篇就强调过的红线。
5.2 AfterMatchSkipStrategy:匹配完成之后,哪些事件还能复用
同一个模式,skip 策略不同,输出数量天差地别:
java
// 输入:A1 A2 B(模式 A followedBy B)
// 不配 skip(默认 noSkip):
// 匹配 (A1, B) 和 (A2, B)?------noSkip 允许重叠匹配全部输出
// 若 B 同时被 A1、A2 匹配,输出 2 条告警 → 告警风暴
// skipPastLastEvent():匹配完成后,跳过本次匹配的全部事件
PatternStream<Event> ps1 = CEP.pattern(stream, p,
AfterMatchSkipStrategy.skipPastLastEvent());
// 输出 1 条 (A1, B),A2 不再参与 → 告警防风暴首选
// skipToFirst("a"):跳过直到回到模式 "a" 的位置重新开始
PatternStream<Event> ps2 = CEP.pattern(stream, p,
AfterMatchSkipStrategy.skipToFirst("a"));
// 输出多条,但每次匹配都从最近的 A 重新累积 → 适合"连续多次命中都要告警"的场景
工程判断:告警类默认 skipPastLastEvent(同一批事件只告警一次);要"每次触发都记录"用 noSkip 但务必评估状态量;skipToFirst/skipToLast 用于复杂模式回跳。
六、完整 CEP 作业装配:从事件流到匹配输出

五步装配,缺一不可。以"10 分钟内下单未支付"为例,用最灵活的 PatternProcessFunction 完整实现(含超时侧输出):
java
// 依赖:flink-cep artifact(1.x)或 flink-core(2.x FLIP-303)
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
// ① keyBy:按订单分组,CEP 按 key 独立匹配
KeyedStream<OrderEvent, String> keyed = orders.keyBy(OrderEvent::getOrderId);
// ② 事件时间:within 超时依赖 watermark(事件定义篇的地基)
DataStream<OrderEvent> withTime = keyed.assignTimestampsAndWatermarks(
WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((e, ts) -> e.getEventTs()));
// ③ 模式定义:下单 → 10 分钟内必须支付
Pattern<OrderEvent, OrderEvent> p = Pattern
.<OrderEvent>begin("order").where(e -> e.type == CREATE)
.followedBy("pay").where(e -> e.type == PAY)
.within(Time.minutes(10));
// ④ 绑定:流 + 模式 + 跳过策略
PatternStream<OrderEvent> ps = CEP.pattern(withTime, p,
AfterMatchSkipStrategy.skipPastLastEvent());
// ⑤ 输出:PatternProcessFunction(最灵活,支持超时处理)
OutputTag<OrderEvent> timeoutTag = new OutputTag<OrderEvent>("timeout") {};
DataStream<String> result = ps.process(
new PatternProcessFunction<OrderEvent, String>() {
// 完整匹配:下单后 10 分钟内支付
@Override
public void processMatch(Map<String, List<OrderEvent>> match,
Context ctx, Collector<String> out) {
out.collect("PAID:" + match.get("pay").get(0).getOrderId());
}
// 超时部分匹配:只有 order 没有 pay → 侧输出
@Override
public void handleTimeout(Map<String, List<OrderEvent>> partial,
long ts, Context ctx) throws Exception {
ctx.output(timeoutTag, partial.get("order").get(0));
}
});
// 超时订单 → 关单/提醒链路;正常支付 → 继续加工
DataStream<OrderEvent> timeoutOrders = result.getSideOutput(timeoutTag);
PatternProcessFunction 相比 select 的两个优势:processMatch 和 handleTimeout 可以同时实现(select 要传两个函数+tag 三参);Context 还能访问当前时间戳、做侧输出------完整匹配和超时两条链路在一个类里收口,可读性更好。
七、实战避坑清单
- times 默认宽松连续:要"连续 N 次"必须 consecutive()------误报头号来源;
- next vs followedBy:中间能否隔事件;followedByAny 允许重叠但状态翻倍;
- greedy 只影响循环后续模式:别在无后续模式的循环上用,没效果还难懂;
- zeroOrMore 必须尾随模式:否则循环永远等待,作业状态膨胀;
- within + watermark 配套:事件时间没配 assignTimestampsAndWatermarks,超时永不触发;
- skip 策略必选:告警场景 skipPastLastEvent 防风暴;noSkip 要评估状态量;
- 模式名是结果 key:match.get("order") 的名字必须和 begin/next 里的完全一致,拼错 = 运行期 NPE;
- 超时处理必须有出口:handleTimeout 不写,超时部分匹配静默丢弃。
八、总结:我的判断
Pattern API 是 CEP 的价值所在------用声明式语法替代手写状态机。它的小语言只有二十来个方法,但组合语义非常丰富:三兄弟管序列、times/consecutive 管循环、within/skip 管时间与去重。我的三条实操建议:
- 先画事件序列图再写模式:把输入序列按时间画出来,标注哪些算命中、哪些不算,再对应到操作符------三兄弟的语义错误几乎都能在画图阶段消灭;
- 循环模式默认加 consecutive:除非业务明确允许中间夹事件,否则"连续"语义十有八九是对的;
- 告警场景默认 skipPastLastEvent + 超时侧输出:匹配数量可控、超时兜底不丢,两条链路都有归宿。