文章目录
- 处理超时事件
- 使用 PatternProcessFunction 的侧输出流
- 使用 PatternTimeoutFunction
- 应用案例
- 处理迟到数据
处理超时事件
复杂事件的检测结果一般只有两种:要么匹配,要么不匹配。检测处理的过程具体如下:
(1)如果当前事件符合模式匹配的条件,就接受该事件,保存到对应的 Map 中;
(2)如果在模式序列定义中,当前事件后面还应该有其他事件,就继续读取事件流进行检测;如果模式序列的定义已经全部满足,那么就成功检测到了一组匹配的复杂事件,调用PatternProcessFunction 的 processMatch()方法进行处理;
(3)如果当前事件不符合模式匹配的条件,就丢弃该事件;
(4)如果当前事件破坏了模式序列中定义的限制条件,比如不满足严格近邻要求,那么当前已检测的一组部分匹配事件都被丢弃,重新开始检测。
不过在有时间限制的情况下,需要考虑的问题会有一点特别。比如我们用.within()指定了模式检测的时间间隔,超出这个时间当前这组检测就应该失败了。然而这种“超时失败”跟真正的“匹配失败”不同,它其实是一种“部分成功匹配”;因为只有在开头能够正常匹配的前提下,没有等到后续的匹配事件才会超时。所以往往不应该直接丢弃,而是要输出一个提示或报警信息。这就要求我们有能力捕获并处理超时事件。
使用 PatternProcessFunction 的侧输出流
在 Flink CEP 中 , 提供了一个专门捕捉超时的部分匹配事件的接 口 , 叫作TimedOutPartialMatchHandler。这个接口需要实现一个processTimedOutMatch()方法,可以将超时的、已检测到的部分匹配事件放在一个 Map 中,作为方法的第一个参数;方法的第二个参数则是 PatternProcessFunction 的上下文 Context。所以这个接口必须与 PatternProcessFunction结合使用,对处理结果的输出则需要利用侧输出流来进行。
举例:
class MyPatternProcessFunction extends PatternProcessFunction <Event, String> implements TimedOutPartialMatchHandler <Event> {
// 正常匹配事件的处理
@Override
public void processMatch(Map <String, List <Event>> match, Context ctx, Collector <String> out) throws Exception {...
}
// 超时部分匹配事件的处理
@Override
public void processTimedOutMatch(Map <String, List <Event>> match, Context ctx) throws Exception {
Event startEvent = match.get("start").get(0);
OutputTag <Event> outputTag = new OutputTag <Event> ("time-out") {};
ctx.output(outputTag, startEvent);
}
}
可以在 processTimedOutMatch()方法中定义了一个输出标签(OutputTag)。调用 ctx.output()方法,就可以将超时的部分匹配事件输出到标签所标识的侧输出流了。
使用 PatternTimeoutFunction
PatternProcessFunction通过实现TimedOutPartialMatchHandler接口扩展出了处理超时事件的能力,这是官方推荐的做法。此外,Flink CEP 中也保留了简化版的PatternSelectFunction,它无法直接处理超时事件,不过我们可以通过调用 PatternStream的.select()方法时多传入一个 PatternTimeoutFunction 参数来实现这一点。PatternTimeoutFunction 是早期版本中用于捕获超时事件的接口。它需要实现一个 timeout()方法,同样会将部分匹配的事件放在一个 Map 中作为参数传入,此外还有一个参数是当前的时间戳。提取部分匹配事件进行处理转换后,可以将通知或报警信息输出。由于调用.select()方法后会得到唯一的 DataStream,所以正常匹配事件和超时事件的处理结果不应该放在同一条流中。正常匹配事件的处理结果会进入转换后得到的 DataStream,而超时事件的处理结果则会进入侧输出流;这个侧输出流需要另外传入一个侧输出标签(OutputTag)来指定。
所以最终我们在调用 PatternStream 的.select()方法时需要传入三个参数:侧输出流标签( OutputTag ), 超 时 事 件 处 理 函 数 PatternTimeoutFunction , 匹 配 事 件 提 取 函 数PatternSelectFunction。
下面是一个代码中的调用方式:
// 定义一个侧输出流标签,用于标识超时侧输出流
OutputTag < String > timeoutTag = new OutputTag <String> ("timeout") {};
// 将匹配到的,和超时部分匹配的复杂事件提取出来,然后包装成提示信息输出
SingleOutputStreamOperator <String> resultStream = patternStream.select(timeoutTag,
// 超时部分匹配事件的处理
new PatternTimeoutFunction <Event, String> () {
@Override
public String timeout(Map <String, List <Event>> pattern, long timeoutTimestamp) throws Exception {
Event event = pattern.get("start").get(0);
return "超时:" + event.toString();
}
},
// 正常匹配事件的处理
new PatternSelectFunction <Event, String> () {
@Override
public String select(Map <String, List <Event>> pattern) throws Exception {...
}
});
// 将正常匹配和超时部分匹配的处理结果流打印输出
resultStream.print("matched");
resultStream.getSideOutput(timeoutTag).print("timeout");
这里需要注意的是,在超时事件处理的过程中,从 Map 里只能取到已经检测到匹配的那些事件;如果取可能未匹配的事件并调用它的对象方法,则可能会报空指针异常(NullPointerException)。另外,超时事件处理的结果进入侧输出流,正常匹配事件的处理结果进入主流,两者的数据类型可以不同。
应用案例
在电商平台中,最终创造收入和利润的是用户下单购买的环节。用户下单的行为可以表明用户对商品的需求,但在现实中,并不是每次下单都会被用户立刻支付。当拖延一段时间后,用户支付的意愿会降低。所以为了让用户更有紧迫感从而提高支付转化率,同时也为了防范订单支付环节的安全风险,电商网站往往会对订单状态进行监控,设置一个失效时间(比如 15分钟),如果下单后一段时间仍未支付,订单就会被取消。
首先定义出要处理的数据类型。我们面对的是订单事件,主要包括用户对订单的创建(下单)和支付两种行为。因此可以定义 POJO 类 OrderEvent 如下,其中属性字段包括用户 ID、订单 ID、事件类型(操作类型)以及时间戳。
public class OrderEvent {
public String userId; //用户ID
public String orderId; //订单ID
public String eventType; //事件类型(操作类型)
public Long timestamp; //时间戳
public OrderEvent() {}
public OrderEvent(String userId, String orderId, String eventType, Long timestamp) {
this.userId = userId;
this.orderId = orderId;
this.eventType = eventType;
this.timestamp = timestamp;
}
@Override
public String toString() {
return "OrderEvent[" +
"userId='" + userId + '\'' +
"orderId='" + orderId + '\'' +
", eventType='" + eventType + '\'' +
", timestamp=" + timestamp +
']';
}
}
当前需求的重点在于对超时未支付的用户进行监控提醒,也就是需要检测有下单行为、但15 分钟内没有支付行为的复杂事件。在下单和支付之间,可以有其他操作(比如对订单的修改),所以两者之间是宽松近邻关系。
重点要处理的是超时的部分匹配事件。对原始的订单事件流按照订单 ID 进行分组,然后检测每个订单的“下单-支付”复杂事件,如果出现超时事件需要输出报警提示信息。
整体代码实现如下:
public class OrderTimeoutDetectExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 1.获取订单事件流,并提取时间戳、生成水位线
SingleOutputStreamOperator<OrderEvent> orderStream = env.fromElements(
new OrderEvent("user_1", "order_1", "create", 1000L),
new OrderEvent("user_2", "order_2", "create", 2000L),
new OrderEvent("user_1", "order_1", "modify", 10 * 1000L),
new OrderEvent("user_1", "order_1", "pay", 60 * 1000L),
new OrderEvent("user_2", "order_3", "create", 10 * 60 * 1000L),
new OrderEvent("user_2", "order_3", "pay", 20 * 60 * 1000L)
).assignTimestampsAndWatermarks(WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<OrderEvent>() {
@Override
public long extractTimestamp(OrderEvent element, long recordTimestamp) {
return element.timestamp;
}
})
);
//2.定义模式
Pattern<OrderEvent, OrderEvent> pattern = Pattern.<OrderEvent>begin("create")
.where(new SimpleCondition<OrderEvent>() { //首先是下单事件
@Override
public boolean filter(OrderEvent value) throws Exception {
return value.eventType.equals("create");
}
})
.followedBy("pay")
.where(new SimpleCondition<OrderEvent>() { //其次是支付事件,中间可以修改订单,宽松近邻
@Override
public boolean filter(OrderEvent value) throws Exception {
return value.eventType.equals("pay");
}
}).within(Time.minutes(15));// 要求在十五分钟之内完成
//3.将应用模式应用到订单流上,检测匹配的复杂事件
PatternStream<OrderEvent> patternStream = CEP.pattern(orderStream.keyBy(event -> event.orderId), pattern);
//4.定义一个侧输出流标签
OutputTag<String> timeoutTag = new OutputTag<String>("timeout"){};
//5.将完全匹配和超时部分匹配的复杂事件提取出来,进行处理
SingleOutputStreamOperator<String> result = patternStream.process(new OrderPayMatch());
// 将正常匹配和超时部分匹配的处理结果流打印输出
result.print("正常支付");
result.getSideOutput(timeoutTag).print("timeout");
env.execute();
}
//实现自定义的 PatternProcessFunction,需实现 TimedOutPartialMatchHandler 接口
public static class OrderPayMatch extends PatternProcessFunction<OrderEvent,String> implements TimedOutPartialMatchHandler<OrderEvent>{
// 处理正常匹配事件
@Override
public void processMatch(Map<String, List<OrderEvent>> match, Context context, Collector<String> out) throws Exception {
//获取当前的支付事件
OrderEvent payEvent = match.get("pay").get(0);
out.collect("用户" + payEvent.userId + "的订单:" + payEvent.orderId + " 已支付!");
}
//处理超时未支付事件
@Override
public void processTimedOutMatch(Map<String, List<OrderEvent>> match, Context context) throws Exception {
OrderEvent createEvent = match.get("create").get(0);
OutputTag<String> timeoutTag = new OutputTag<String>("timeout"){};
context.output(timeoutTag,"用户" + createEvent.userId + "的订单 :" + createEvent.orderId + " 超时未支付!");
}
}
}
运行代码,控制台打印结果如下:
订单 1 和订单 3 都在 15 分钟进行了支付,订单 1 中间的修改行为不会影响结果;而订单 2 未能支付,因此侧输出流输出了一条报警信息。且同一用户可以下多个订单,最后的判断只是基于同一订单做出的。这与我们预期的效果完全一致。用处理函数进行状态编程,结合定时器也可以实现同样的功能,但明显 CEP 的实现更加方便,也更容易迁移和扩展。
处理迟到数据
CEP 主要处理的是先后发生的一组复杂事件,所以事件的顺序非常关键。事件先后顺序的具体定义与时间语义有关。如果是处理时间语义,那比较简单,只要按照数据处理的系统时间算就可以了;而如果是事件时间语义,需要按照事件自身的时间戳来排序。这就有可能出现时间戳大的事件先到、时间戳小的事件后到的现象,也就是所谓的“乱序数据” 或“迟到数据”。 在 Flink CEP 中沿用了通过设置水位线(watermark)延迟来处理乱序数据的做法。
当一个事件到来时,并不会立即做检测匹配处理,而是先放入一个缓冲区(buffer)。缓冲区内的数据,会按照时间戳由小到大排序;当一个水位线到来时,就会将缓冲区中所有时间戳小于水位线的事件依次取出,进行检测匹配。这样就保证了匹配事件的顺序和事件时间的进展一致,处理的顺序就一定是正确的。这里水位线的延迟时间,也就是事件在缓冲区等待的最大时间。
这样又会带来另一个问题:水位线延迟时间不可能保证将所有乱序数据完美包括进来,总会有一些事件延迟比较大,以至于等它到来的时候水位线早已超过了它的时间戳。这时之前的数据都已处理完毕,这样的“迟到数据”就只能被直接丢弃了——这与窗口对迟到数据的默认处理一致。
如果不希望迟到数据丢掉,应该也可以借鉴窗口的做法。Flink CEP 同样提 供 了 将 迟 到 事 件 输 出 到 侧 输 出 流 的 方 式 : 我 们 可 以 基 于 PatternStream 直接调用.sideOutputLateData()方法,传入一个 OutputTag,将迟到数据放入侧输出流另行处理。代码中调用方式如下:
PatternStream <Event> patternStream = CEP.pattern(input, pattern);
// 定义一个侧输出流的标签
OutputTag <String> lateDataOutputTag = new OutputTag <String> ("late-data") {};
// 将迟到数据输出到侧输出流
SingleOutputStreamOperator <ComplexEvent> result = patternStream.sideOutputLateData(lateDataOutputTag)
.select( // 处理正常匹配数据
new PatternSelectFunction <Event, ComplexEvent> () {...
});
// 从结果中提取侧输出流
DataStream <String> lateData = result.getSideOutput(lateDataOutputTag);
可以看到,整个处理流程与窗口非常相似。经处理匹配数据得到结果数据流之后,可以调用.getSideOutput()方法来提取侧输出流,捕获迟到数据进行额外处理。