Flink CEP复杂事件处理(二)

检测模式

Pattern应用

Flink CEP中定义了Pattern检测模式后,就可以将该Pattern应用到输入流进行模式检测,关于这一点在前面的小节中我们已经学习,使用方式如下:

复制代码
DataStream<Event> input = ...;
Pattern<Event, ?> pattern = ...;
EventComparator<Event> comparator = ...; // 可选的
PatternStream<Event> patternStream = CEP.pattern(input, pattern, comparator);
  • input:实时数据流,可以是keyed或者non-keyed DataStream,如果在non-keyed DataStream上应用Pattern,Flink 作业的并行度为1。

  • pattern:传入的模式匹配规则。

  • comparator:该参数是可选的,传入该参数可以决定如何对具有相同事件时间戳的数据进行排序。

选择数据

想要从PatternStream对象中获取到匹配的复杂事件,建议调用process方法并实现PatternProcessFunction抽象类来获取匹配的复杂事件并处理,操作如下:

复制代码
patternStream.process(new PatternProcessFunction<IN, OUT>() {
    @Override
    public void processMatch(Map<String, List<IN>> match,
                             Context ctx,
                             Collector<OUT> out) throws Exception {
        List<IN> firstPatternInfo = match.get("first");
        List<IN> secondPatternInfo = match.get("second");
        List<IN> thirdPatternInfo = match.get("third");
        ... ...
​
  
        //输出
        out.collect(OUT(... ...));
    }
})

PatternProcessFunction抽象类中需要实现processMatch方法,该方法参数解释如下:

  • Map<String,List<IN>>:该对象存储了对应模式中匹配的复杂事件。由于每种模式中可能会使用times类似量词导致不仅有一个匹配事件,所以每种模式对应的结果是List<IN>。

  • Context:通过Context对象可以访问时间、状态等属性,也可以将数据输出到侧输出流。

  • Collector<OUT>:对各模式复杂事件处理后用于返回数据的对象。

以上PatternProcessFunction是在Flink1.8版本后引入,推荐实现该抽象类进行复杂事件匹配处理,但用户也可以使用PatternStream.select这种旧API,在内部会自动转换成PatternProcessFunction的实现。

超时数据处理

当一个Pattern使用了within后表示该模式会匹配在一定时间内符合规则的事件,如果超过这个指定的时间Pattern中部分匹配的事件序列就会被丢弃,我们可以使用 TimedOutPatialMatchHandler 接口来处理超时的部分匹配,例如将这些超时的匹配输出到侧输出流中进一步处理,使用形式如下:

复制代码
//定义outputTag
OutputTag<String> outputTag = new OutputTag<String>("timeout"){};
​
//Pattern应用到数据流上
PatternStream<IN> patternStream = CEP.pattern(keyedStream, pattern);
patternStream.process(new MyPatternProcessFunction(outputTag));
​
//定义类实现PatternProcessFunction和TimedOutPartialMatchHandler
class MyPatternProcessFunction extends PatternProcessFunction<IN,OUT> implements TimedOutPartialMatchHandler<OrderInfo>{
    //定义outputTag
    private OutputTag<String> outputTag;
​
    //创建MyPatternProcessFunction构造器用于接收outputTag
    public MyPatternProcessFunction(OutputTag<String> outputTag) {
        this.outputTag = outputTag;
    }
​
    //处理匹配到的数据
    @Override
    public void processMatch(Map<String, List<IN>> match,
                             Context ctx,
                             Collector<String> out) throws Exception {
        List<IN> firstPatternInfo = match.get("first");
​
        //业务处理
        ... ...
​
        //输出
        out.collect(OUT(...));
    }
​
    //处理超时的数据
    @Override
    public void processTimedOutMatch(Map<String, List<IN>> match,
                                     Context ctx) throws Exception {
        List<IN> firstPatternInfo = match.get("first");
        //数据放入侧输出流
        ctx.output("...");
​
    }
}

以上自定义实现类中的processTimedOutMatch方法可以进行超时数据的处理,在指定时间内没有完全匹配的部分匹配事件可以通过该方法进行获取并处理输出到侧输出流中。

案例

下面通过一些案例来说明检测模式的使用,以下案例中全部基于EventTime设置Pattern进行复杂事件的匹配处理。

1. greedy使用

该案例主要演示greedy的使用,该模式是贪心的,会重复尽可能多的次数,只对量词适用,不支持模式组,在多个单独模式组合成组合模式中使用才有意义。该案例中读取用户登录日志,当用户登录成功后,输出用户登录前的所有操作,这里需要使用到LoginInfo实体对象,该对象中包含用户id(uid)、用户姓名(userName)、登录时间(loginTime)、登录状态(loginState)字段。

  • Java代码
复制代码
//准备环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
​
//1.定义事件流
SingleOutputStreamOperator<LoginInfo> ds = env.socketTextStream("node5", 9999)
        .map(new MapFunction<String, LoginInfo>() {
            @Override
            public LoginInfo map(String value) throws Exception {
                String[] arr = value.split(",");
                return new LoginInfo(arr[0], arr[1], Long.valueOf(arr[2]), arr[3]);
            }
        });
​
//设置watermark并设置自动推进watermark
SingleOutputStreamOperator<LoginInfo> dsWithWatermark = ds.assignTimestampsAndWatermarks(
        WatermarkStrategy.<LoginInfo>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                .withTimestampAssigner(new SerializableTimestampAssigner<LoginInfo>() {
                    @Override
                    public long extractTimestamp(LoginInfo element, long recordTimestamp) {
                        return element.getLoginTime();
                    }
                }).withIdleness(Duration.ofSeconds(5))
);
​
//获取每个用户的登录信息
KeyedStream<LoginInfo, String> keyedStream = dsWithWatermark.keyBy(new KeySelector<LoginInfo, String>() {
    @Override
    public String getKey(LoginInfo value) throws Exception {
        return value.getUid();
    }
});
​
//2.定义匹配规则。
Pattern<LoginInfo, LoginInfo> pattern = Pattern.<LoginInfo>begin("first")
        .where(new SimpleCondition<LoginInfo>() {
            @Override
            public boolean filter(LoginInfo value) throws Exception {
                return value.getLoginState().startsWith("fail");
            }
        }).oneOrMore().greedy()
        .followedBy("second")
        .where(new SimpleCondition<LoginInfo>() {
            @Override
            public boolean filter(LoginInfo value) throws Exception {
                return value.getLoginState().equals("success");
            }
        });
​
//3.将匹配规则应用到数据流上
PatternStream<LoginInfo> patternStream = CEP.pattern(keyedStream, pattern);
​
//4.获取符合规则的数据
patternStream.process(new PatternProcessFunction<LoginInfo, String>() {
    @Override
    public void processMatch(Map<String, List<LoginInfo>> match,
                             Context ctx,
                             Collector<String> out) throws Exception {
        List<LoginInfo> firstPatternInfo = match.get("first");
        List<LoginInfo> secondPatternInfo = match.get("second");
​
        //获取用户
        String uid = firstPatternInfo.get(0).getUid();
​
        //firstPatternInfo中登录状态拼接
        StringBuilder sb = new StringBuilder();
        for (LoginInfo loginInfo : firstPatternInfo) {
            sb.append(loginInfo.getLoginState()).append(",");
        }
​
        //secondPatternInfo中登录成功时间
        long loginTime = secondPatternInfo.get(0).getLoginTime();
​
        //输出
        out.collect("用户:" + uid + "在" + loginTime + "登录成功," +
                "登录成功前的操作为:" + sb.substring(0, sb.length() - 1));
    }
}).print();
​
env.execute();
  • Scala代码
复制代码
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
​
import org.apache.flink.streaming.api.scala._
​
val ds: DataStream[LoginInfo] = env.socketTextStream("node5", 9999)
  .map(line => {
    val arr: Array[String] = line.split(",")
    LoginInfo(arr(0), arr(1), arr(2).toLong, arr(3))
  })
​
val dsWithWatermark: DataStream[LoginInfo] = ds.assignTimestampsAndWatermarks(
  WatermarkStrategy.forBoundedOutOfOrderness[LoginInfo](Duration.ofSeconds(2))
    .withTimestampAssigner(new SerializableTimestampAssigner[LoginInfo] {
      override def extractTimestamp(element: LoginInfo, recordTimestamp: Long): Long = element.loginTime
    })
    .withIdleness(Duration.ofSeconds(5))
)
​
val keyedStream: KeyedStream[LoginInfo, String] = dsWithWatermark.keyBy(_.uid)
​
val pattern: Pattern[LoginInfo, LoginInfo] =
  Pattern.begin[LoginInfo]("first").where(_.loginState.startsWith("fail")).oneOrMore
  .followedBy("second").where(_.loginState.equals("success"))
​
val patternStream: PatternStream[LoginInfo] = CEP.pattern(keyedStream, pattern)
​
patternStream.process(new PatternProcessFunction[LoginInfo,String] {
  override def processMatch(`match`: util.Map[String, util.List[LoginInfo]],
                            ctx: PatternProcessFunction.Context,
                            out: Collector[String]): Unit = {
    val firstPatternInfo: util.List[LoginInfo] = `match`.get("first")
    val secondPatternInfo: util.List[LoginInfo] = `match`.get("second")
​
    //获取用户
    val uid: String = firstPatternInfo.get(0).uid
​
    //firstPatternInfo中登录状态拼接
    import scala.collection.JavaConverters._
    val end: String = firstPatternInfo.asScala.map(_.loginState).mkString(",")
​
    //获取成功登录的时间
    val loginTime: Long = secondPatternInfo.get(0).loginTime
​
    //输出
    out.collect(s"用户 $uid 在 $loginTime 登录成功,登录成功前的操作为:$end")
​
  }
}).print()
​
env.execute()

以上代码编写完成执行,向socket-9999中输入如下数据:

复制代码
#socket-9999中输入如下数据
uid1,zs,1000,fail1
uid2,ls,2000,fail2
uid1,zs,3000,fail3
uid2,ls,4000,fail4
uid1,zs,5000,fail5
uid1,zs,6000,success
#此条数据输入后会输出uid1匹配结果
uid2,ls,8001,success

控制台输出结果如下:

复制代码
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail1,fail3,fail5
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail3,fail5
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail5

如果不使用greedy贪婪模式,输出结果如下:

复制代码
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail1,fail3,fail5
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail1,fail3
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail1
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail3,fail5
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail3
5> 用户 uid1 在 6000 登录成功,登录成功前的操作为:fail5

2. 用户恶意登录检测

在用户登录场景中,如果一个用户在一定时间内出现连续登录失败情况,我们可以认为当前用户是恶意登录,如上个案例中用户在短时间内连续登录失败就可以看成是恶意登录,这时我们需要进行告警信息输出。下面这个案例进行用户登录日志数据读取,检测一个用户如果在20秒内连续三次登录失败,那么就输出报警信息。

  • Java代码
复制代码
//准备环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
​
//1.定义事件流
SingleOutputStreamOperator<LoginInfo> ds = env.socketTextStream("node5", 9999)
        .map(new MapFunction<String, LoginInfo>() {
            @Override
            public LoginInfo map(String value) throws Exception {
                String[] arr = value.split(",");
                return new LoginInfo(arr[0], arr[1], Long.valueOf(arr[2]), arr[3]);
            }
        });
​
//设置watermark并设置自动推进watermark
SingleOutputStreamOperator<LoginInfo> dsWithWatermark = ds.assignTimestampsAndWatermarks(
        WatermarkStrategy.<LoginInfo>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                .withTimestampAssigner(new SerializableTimestampAssigner<LoginInfo>() {
                    @Override
                    public long extractTimestamp(LoginInfo element, long recordTimestamp) {
                        return element.getLoginTime();
                    }
                }).withIdleness(Duration.ofSeconds(5))
);
​
//获取每个用户的登录信息
KeyedStream<LoginInfo, String> keyedStream = dsWithWatermark.keyBy(new KeySelector<LoginInfo, String>() {
    @Override
    public String getKey(LoginInfo value) throws Exception {
        return value.getUid();
    }
});
​
//2.定义匹配规则。
Pattern<LoginInfo, LoginInfo> pattern = Pattern.<LoginInfo>begin("first")
        .where(new SimpleCondition<LoginInfo>() {
            @Override
            public boolean filter(LoginInfo value) throws Exception {
                return value.getLoginState().startsWith("fail");
            }
        }).times(3).within(Time.seconds(20));
​
//3.将匹配规则应用到数据流上
PatternStream<LoginInfo> patternStream = CEP.pattern(keyedStream, pattern);
​
//4.获取符合规则的数据
patternStream.process(new PatternProcessFunction<LoginInfo, String>() {
    @Override
    public void processMatch(Map<String, List<LoginInfo>> match,
                             Context ctx,
                             Collector<String> out) throws Exception {
        List<LoginInfo> firstPatternInfo = match.get("first");
​
        //获取用户
        String uid = firstPatternInfo.get(0).getUid();
​
        //输出
        out.collect("用户:" + uid + "在20秒内连续3次登录失败!");
    }
}).print();
​
env.execute();
  • scala代码
复制代码
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
​
import org.apache.flink.streaming.api.scala._
​
val ds: DataStream[LoginInfo] = env.socketTextStream("node5", 9999)
  .map(line => {
    val arr: Array[String] = line.split(",")
    LoginInfo(arr(0), arr(1), arr(2).toLong, arr(3))
  })
​
val dsWithWatermark: DataStream[LoginInfo] = ds.assignTimestampsAndWatermarks(
  WatermarkStrategy.forBoundedOutOfOrderness[LoginInfo](Duration.ofSeconds(2))
    .withTimestampAssigner(new SerializableTimestampAssigner[LoginInfo] {
      override def extractTimestamp(element: LoginInfo, recordTimestamp: Long): Long = element.loginTime
    })
    .withIdleness(Duration.ofSeconds(5))
)
​
val keyedStream: KeyedStream[LoginInfo, String] = dsWithWatermark.keyBy(_.uid)
​
val pattern: Pattern[LoginInfo, LoginInfo] = Pattern.begin[LoginInfo]("first")
    .where(_.loginState.startsWith("fail"))
    .times(3)
    .within(Time.seconds(20))
​
val patternStream: PatternStream[LoginInfo] = CEP.pattern(keyedStream, pattern)
​
patternStream.process(new PatternProcessFunction[LoginInfo,String] {
  override def processMatch(`match`: util.Map[String, util.List[LoginInfo]],
                            ctx: PatternProcessFunction.Context,
                            out: Collector[String]): Unit = {
    val firstPatternInfo: util.List[LoginInfo] = `match`.get("first")
​
    //获取用户
    val uid: String = firstPatternInfo.get(0).uid
​
    //firstPatternInfo中登录状态拼接
    import scala.collection.JavaConverters._
    val end: String = firstPatternInfo.asScala.map(_.loginState).mkString(",")
​
    //输出
    out.collect("用户:" + uid + "在20秒内连续3次登录失败!")
​
  }
}).print()
​
env.execute()

以上代码编写完成执行,向socket-9999中输入如下数据:

复制代码
#socket-9999中输入如下数据
uid1,zs,1000,fail1
uid2,ls,2000,fail2
uid1,zs,3000,fail3
uid2,ls,4000,fail4
uid1,zs,5000,fail5
#此条数据输入后,uid1会匹配模式
uid2,ls,7001,success

控制台输出结果如下:

复制代码
5> 用户:uid1在20秒内连续3次登录失败!

3. 订单支付超时报警

在订单支付场景中,一个用户下订单后再一定时间内没有支付往往会进行提示,这也可以通过Flink CEP解决。下面这个案例中读取用户订单数据,实时监控用户订单支付是否超时,当用户在规定事件内支付成功会返回支付成功提示,否则会进行支付超时告警输出。

以下代码中使用到了OrderInfo实体对象,该对象中包含订单id(uid)、订单金额(orderAmount)、订单时间(orderTime)、支付状态(payState)字段。针对匹配后的数据流进行处理时自定义类继承了PatternProcessFunction抽象类和TimedOutPartialMatchHandler接口分别对匹配数据和超时数据进行处理。

  • Java代码
复制代码
public class PayCEPTest {
    public static void main(String[] args) throws Exception {
        //准备环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
​
        //1.定义事件流
        SingleOutputStreamOperator<OrderInfo> ds = env.socketTextStream("node5", 9999)
                .map(new MapFunction<String, OrderInfo>() {
                    @Override
                    public OrderInfo map(String value) throws Exception {
                        String[] arr = value.split(",");
                        return new OrderInfo(arr[0], Double.valueOf(arr[1]), Long.valueOf(arr[2]), arr[3]);
                    }
                });
​
        //设置watermark并设置自动推进watermark
        SingleOutputStreamOperator<OrderInfo> dsWithWatermark = ds.assignTimestampsAndWatermarks(
                WatermarkStrategy.<OrderInfo>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                        .withTimestampAssigner(new SerializableTimestampAssigner<OrderInfo>() {
                            @Override
                            public long extractTimestamp(OrderInfo element, long recordTimestamp) {
                                return element.getOrderTime();
                            }
                        }).withIdleness(Duration.ofSeconds(5))
        );
​
        //获取每个用户的登录信息
        KeyedStream<OrderInfo, String> keyedStream = dsWithWatermark.keyBy(new KeySelector<OrderInfo, String>() {
            @Override
            public String getKey(OrderInfo value) throws Exception {
                return value.getOrderId();
            }
        });
​
        //2.定义匹配规则。
        Pattern<OrderInfo, OrderInfo> pattern = Pattern.<OrderInfo>begin("first")
                .where(new SimpleCondition<OrderInfo>() {
                    @Override
                    public boolean filter(OrderInfo value) throws Exception {
                        return value.getPayState().equals("create");
                    }
                }).followedBy("second").where(new SimpleCondition<OrderInfo>() {
                    @Override
                    public boolean filter(OrderInfo value) throws Exception {
                        return value.getPayState().equals("pay");
                    }
                }).within(Time.seconds(20));
​
        //定义outputTag
        OutputTag<String> outputTag = new OutputTag<String>("pay-timeout"){};
​
        //3.将匹配规则应用到数据流上
        PatternStream<OrderInfo> patternStream = CEP.pattern(keyedStream, pattern);
​
        //4.获取符合规则的数据
        SingleOutputStreamOperator<String> result = patternStream.process(new MyPatternProcessFunction(outputTag));
​
        //打印结果
        result.print();
​
        //5.获取超时数据
        result.getSideOutput(outputTag).print();
​
        env.execute();
    }
}
​
class MyPatternProcessFunction extends PatternProcessFunction<OrderInfo,String> implements TimedOutPartialMatchHandler<OrderInfo>{
    //定义outputTag
    private OutputTag<String> outputTag;
​
    //创建MyPatternProcessFunction构造器用于接收outputTag
    public MyPatternProcessFunction(OutputTag<String> outputTag) {
        this.outputTag = outputTag;
    }
​
    //处理匹配到的数据
    @Override
    public void processMatch(Map<String, List<OrderInfo>> match,
                             Context ctx,
                             Collector<String> out) throws Exception {
        List<OrderInfo> firstPatternInfo = match.get("first");
​
        //获取订单
        String uid = firstPatternInfo.get(0).getOrderId();
​
        //输出
        out.collect("订单" + uid + "支付成功,待发货");
    }
​
    //处理超时的数据
    @Override
    public void processTimedOutMatch(Map<String, List<OrderInfo>> match,
                                     Context ctx) throws Exception {
        List<OrderInfo> firstPatternInfo = match.get("first");
        ctx.output(outputTag,"订单" + firstPatternInfo.get(0).getOrderId() + "支付超时");
​
    }
}
  • Scala代码
复制代码
case class OrderInfo(orderId: String, orderAmount: Double, orderTime: Long, payState:String)
object PayCEPTest {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
​
    import org.apache.flink.streaming.api.scala._
​
    val ds: DataStream[OrderInfo] = env.socketTextStream("node5", 9999)
      .map(line => {
        val arr: Array[String] = line.split(",")
        OrderInfo(arr(0), arr(1).toDouble, arr(2).toLong, arr(3))
      })
​
    val dsWithWatermark: DataStream[OrderInfo] = ds.assignTimestampsAndWatermarks(
      WatermarkStrategy.forBoundedOutOfOrderness[OrderInfo](Duration.ofSeconds(2))
        .withTimestampAssigner(new SerializableTimestampAssigner[OrderInfo] {
          override def extractTimestamp(element: OrderInfo, recordTimestamp: Long): Long = element.orderTime
        })
        .withIdleness(Duration.ofSeconds(5))
    )
​
    val keyedStream: KeyedStream[OrderInfo, String] = dsWithWatermark.keyBy(_.orderId)
​
    val pattern: Pattern[OrderInfo, OrderInfo] = Pattern.begin[OrderInfo]("first")
      .where(_.payState.equals("create"))
      .followedBy("second").where(_.payState.equals("pay")).within(Time.seconds(20))
​
    //定义outputTag
    val outputTag = new OutputTag[String]("pay-timeout")
​
    val patternStream: PatternStream[OrderInfo] = CEP.pattern(keyedStream, pattern)
​
    val result: DataStream[String] = patternStream.process(new MyPatternProcessFunction(outputTag))
​
    //打印结果
    result.print("订单支付:")
​
    //获取超时数据
    result.getSideOutput(outputTag).print("超时数据:")
​
    env.execute()
​
  }
​
}
​
class MyPatternProcessFunction(outputTag:OutputTag[String]) extends PatternProcessFunction[OrderInfo,String] with TimedOutPartialMatchHandler [OrderInfo]{
​
  //处理匹配到的数据
  override def processMatch(`match`: util.Map[String, util.List[OrderInfo]],
                            ctx: PatternProcessFunction.Context,
                            out: Collector[String]): Unit = {
    val firstPatternInfo: util.List[OrderInfo] = `match`.get("first")
​
    //获取订单
    val orderId = firstPatternInfo.get(0).orderId
​
    //输出
    out.collect("订单" + orderId + "支付成功,待发货")
​
  }
​
  //处理超时的数据
  override def processTimedOutMatch(`match`: util.Map[String, util.List[OrderInfo]],
                                    ctx: PatternProcessFunction.Context): Unit = {
​
    val firstPatternInfo: util.List[OrderInfo] = `match`.get("first")
​
    //获取订单
    val orderId = firstPatternInfo.get(0).orderId
​
    //输出到侧输出流
    ctx.output(outputTag,"订单" + orderId + "支付超时");
  }
}

以上代码编写完成执行,向socket-9999中输入如下数据:

复制代码
#socket-9999中输入如下数据
order1,100,1000,create
order2,200,2000,create
order1,100,20000,pay
#该条数据输入,wm达到22000,order2订单支付超时
order3,300,24001,pay

控制台输出结果如下:

复制代码
超时数据::7> 订单order2支付超时
订单支付::3> 订单order1支付成功,待发货

SQL中的CEP

前面讲解的Flink CEP主要应用在DataStream API中,同样,在Flink SQL API中Flink也支持复杂事件处理,可以通过"MATCH_RECOGNIZE"语句进行设置。目前在Tabel API还不支持复杂事件处理。本小节重点介绍Flink SQL API中如何使用CEP。

SQL CEP 语法

Flink SQL中使用CEP 需要在项目中导入如下依赖,该依赖与使用DataStream API导入的依赖一样。

复制代码
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-cep</artifactId>
  <version>${flink.version}</version>
</dependency>

在FlinkSQL中对数据流进行模式识别是通过"MATCH_RECOGNIZE"语句完成的,目前该子句只能针对Append数据流进行模式匹配,使用语法如下:

复制代码
SELECT T.aid, T.bid, T.cid
FROM MyTable
MATCH_RECOGNIZE (
      PARTITION BY userid  
      ORDER BY proctime
      MEASURES
        A.id AS aid,
        B.id AS bid,
        C.id AS cid
      ONE ROW PER MATCH 
      [AFTER MATCH SKIP ...]
      PATTERN (A B C)
      DEFINE
        A AS name = 'a',
        B AS name = 'b',
        C AS name = 'c'
) AS T

以上语法使用案例中读取MyTable表进行模式匹配,Pattern匹配"name为a、name为b、name为c"的事件序列,并进行各个事件信息的输出。以上语法使用的解释如下:

  • PARTITION BY

该子句表示表的逻辑分区,相当于group by操作,在每个分组内都会进行Pattern匹配,建议对传入的数据进行分区,否则"MATCH_RECOGNIZE"子句将被转换为单并行度执行。

  • ORDER BY

该子句指定传入行的排序方式。Pattern进行匹配时需要对事件进行排序,需要指定该值指定排序方式。对于"MATCH_RECOGNIZE"子句ORDER BY 后的第一列必须是EventTime/ProcessTime列且只能是升序。

  • MEASURES

该子句定义Pattern匹配成功后的输出,相当于SELECT 子句,通过该子句指定查询匹配数据的哪些列数据,查询的数据需要通过AS 指定别名。

  • ONE ROW PER MATCH

该子句定义每个匹配项应产生多少行结果,SQLAPI中只支持ONE ROW PER MATCH ,即:匹配结果只能输出一行。

  • AFTER MATCH SKIP

该子句指定下一个匹配开始的位置,这也是控制单个事件可以控制多少个不同匹配项的方法。

  • PATTERN

该子句是模式声明,使用类似正则表达式的语法来指定复杂事件匹配规则。

  • DEFINE

该子句定义每个单独模式必须满足的条件,类似通过Where指定条件。

关于以上部分子句后续还会进行详细介绍,下面通过一个案例来学习Flink SQL中CEP的使用。在该案例中,Flink SQL通过Kafka Connector读取Kafka中基站日志数据,我们需要统计每个基站中连续2次通话失败后通话成功的事件,由于Java代码和Scala代码类似,这里只给出Java代码实现。

复制代码
//创建TableEnvironment
EnvironmentSettings settings = EnvironmentSettings.newInstance()
        .inStreamingMode()
        .build();
TableEnvironment tableEnv = TableEnvironment.create(settings);
​
//当某个并行度5秒没有数据输入时,自动推进watermark
tableEnv.getConfig().set("table.exec.source.idle-timeout","5000");
​
​
//读取Kafka基站日志数据,通过SQL DDL方式定义表结构
tableEnv.executeSql("" +
        "create table stationlog_tbl (" +
        "   sid string," +
        "   call_out string," +
        "   call_in string," +
        "   call_type string," +
        "   call_time bigint," +
        "   duration bigint," +
        "   rowtime AS TO_TIMESTAMP_LTZ(call_time,3)," +
        "   WATERMARK FOR rowtime AS rowtime - INTERVAL '2' SECOND" +
        ") with (" +
        "   'connector' = 'kafka'," +
        "   'topic' = 'stationlog-topic'," +
        "   'properties.bootstrap.servers' = 'node1:9092,node2:9092,node3:9092'," +
        "   'properties.group.id' = 'testGroup'," +
        "   'scan.startup.mode' = 'latest-offset'," +
        "   'format' = 'csv'" +
        ")");
​
//SQL CEP - 输出连续2次通话失败通话又成功的基站信息
tableEnv.executeSql("" +
        "select " +
        "   T.sid,T.first_fail_dt,T.second_fail_dt,T.success_dt " +
        "from stationlog_tbl " +
        " MATCH_RECOGNIZE ( " +
        "   PARTITION BY sid " +
        "   ORDER BY rowtime " +
        "   MEASURES " +
        "       FIRST(A.rowtime) AS first_fail_dt," +
        "       FIRST(A.rowtime,1) AS second_fail_dt," +
        "       LAST(B.rowtime) AS success_dt " +
        "   ONE ROW PER MATCH " +
        "   AFTER MATCH SKIP TO LAST B " +
        "   PATTERN (A{2} B) " +
        "   DEFINE " +
        "       A as A.call_type = 'fail'," +
        "       B as B.call_type = 'success'" +
        ") T" +
        "").print();

以上代码中设置Flink SQL CEP是按照sid进行分组、事件时间排序,针对读取的Append实时数据进行模式匹配。通过PATTERN 指定了复杂事件匹配的Pattern为"A{2} B",A、B是通过DEFINE子句定义的模式,其中有对应模式的满足条件;A{2}表示符合A模式条件的事件严格出现2次;"A{2} B"表示的就是符合A模式条件的事件出现2次后紧跟符合B模式条件的事件。

当有符合Pattern的事件序列匹配上后,通过MEASURES子句进行匹配事件数据的输出,在MEASURES中查询了FIRST(A.rowtime)、FIRST(A.rowtime,1)、LAST(B.rowtime)三列,默认还有分组列sid。FIRST(A.rowtime)表示获取符合A模式条件的第一个事件中的rowtime列;FIRST(A.rowtime,1)表示获取符合A模式条件第一个事件后的一个事件中的rowtime列;LAST(B.rowtime)表示获取符合B模式条件最后一个事件的rowtime列。"AFTER MATCH SKIP"子句中指定的"TO LAST B"表示下一个匹配事件从B最后一个事件开始往后匹配。

在编写Flink SQL时指定了Watermark延时时间为2s,意味着小于等于watermark的事件才会被检测是否匹配定义的模式序列,此外,由于代码中默认有多个并行度,需要设置watermark自动推进机制。代码运行后向Kafka stationlog-topic中输入如下数据:

复制代码
#kafka stationlog-topic中输入数据
001,181,182,busy,1000,1
002,181,182,barring,2000,2
001,182,183,fail,3000,3
002,182,183,fail,4000,4
001,183,184,fail,5000,5
002,182,183,fail,6000,6
001,181,183,success,7000,9
002,181,183,success,8000,10
#此条数据输入后,wm达到8000,输出匹配结果
003,181,183,success,10001,10

根据指定的CEP复杂事件匹配模式,当输入"003,181,183,success,10001,10"这条数据后,watermark达到8000,基站001和002会有匹配事件输出,输出结果如下:

复制代码
+----+-----+-------------------------+-------------------------+-------------------------+
| op | sid |           first_fail_dt |          second_fail_dt |              success_dt |
+----+-----+-------------------------+-------------------------+-------------------------+
| +I | 002 | 1970-01-01 08:00:04.000 | 1970-01-01 08:00:06.000 | 1970-01-01 08:00:08.000 |
| +I | 001 | 1970-01-01 08:00:03.000 | 1970-01-01 08:00:05.000 | 1970-01-01 08:00:07.000 |

MESURES&DEFINE

在"MATCH_RECOGNIZE"语句中,"MEASURES"子句类似Select语句从匹配的事件序列中查询内容,"DEFINE"子句相当于Where指定Pattern中满足每个模式的条件,满足条件结果为true的事件会得到匹配,在MEASUERS和DEFINE子句中也可以使用聚合函数,对匹配的数据集基于聚合结果进行条件判断并输出结果。

案例:读取Kafka基站日志数据,匹配基站平均通话时长小于10s的事件。由于Java和Scala代码类似,这里只给出Java代码实现。

复制代码
//创建TableEnvironment
EnvironmentSettings settings = EnvironmentSettings.newInstance()
        .inStreamingMode()
        .build();
TableEnvironment tableEnv = TableEnvironment.create(settings);

//当某个并行度5秒没有数据输入时,自动推进watermark
tableEnv.getConfig().set("table.exec.source.idle-timeout","5000");

//读取Kafka基站日志数据,通过SQL DDL方式定义表结构
tableEnv.executeSql("" +
        "create table stationlog_tbl (" +
        "   sid string," +
        "   call_out string," +
        "   call_in string," +
        "   call_type string," +
        "   call_time bigint," +
        "   duration bigint," +
        "   rowtime AS TO_TIMESTAMP_LTZ(call_time,3)," +
        "   WATERMARK FOR rowtime AS rowtime - INTERVAL '2' SECOND" +
        ") with (" +
        "   'connector' = 'kafka'," +
        "   'topic' = 'stationlog-topic'," +
        "   'properties.bootstrap.servers' = 'node1:9092,node2:9092,node3:9092'," +
        "   'properties.group.id' = 'testGroup'," +
        "   'scan.startup.mode' = 'latest-offset'," +
        "   'format' = 'csv'" +
        ")");

//SQL CEP - 输出连续2次通话失败的基站信息
tableEnv.executeSql("" +
        "select " +
        "   T.sid,T.start_dt,T.end_dt,T.avgDuration,T.dt " +
        "from stationlog_tbl " +
        " MATCH_RECOGNIZE ( " +
        "   PARTITION BY sid " +
        "   ORDER BY rowtime " +
        "   MEASURES " +
        "       FIRST(A.rowtime) AS start_dt," +
        "       LAST(A.rowtime) AS end_dt," +
        "       AVG(A.duration) AS avgDuration," +
        "       B.rowtime AS dt " +
        "   ONE ROW PER MATCH " +
        "   AFTER MATCH SKIP PAST LAST ROW " +//在当前匹配的最后一行之后的下一行继续模式匹配
        "   PATTERN (A+ B) " +
        "   DEFINE " +
        "       A AS AVG(A.duration) < 10" +
        ") T" +
        "").print();

以上代码中设置Flink SQL CEP是按照sid分组、事件时间排序,针对读取的Append实时数据进行模式匹配。通过PATTERN指定了复杂事件匹配的Pattern为"A+ B","A+"表示匹配A模式的一到多个事件,类似DataStream API中的oneOrMore(),该模式在Flink SQL CEP中不能无限匹配下去,需要跟上另外的模式B。

通过DEFINE子句定了A模式,在该模式条件指定了AVG聚合函数,只要连续多个事件的平均通话时长小于10都会被匹配;虽然DEFINE子句中没有定义模式B,可以在PATTERN中指定B,表示不匹配A模式的事件。

当有符合Pattern的事件序列匹配上后,通过MEASURES子句进行匹配事件数据的输出,在MEASURES中查询了FIRST(A.rowtime)、LAST(A.rowtime)、AVG(A.duration)、B.rowtime四列,默认还有分组列sid。"AFTER MATCH SKIP"子句中指定的"PAST LAST ROW"表示在当前匹配的最后一行之后的下一行继续模式匹配。

在编写Flink SQL时指定了Watermark延时时间为2s,意味着小于等于watermark的事件才会被检测是否匹配定义的模式序列,此外,由于代码中默认有多个并行度,需要设置watermark自动推进机制。代码运行后向Kafka stationlog-topic中输入如下数据:

复制代码
#kafka stationlog-topic中输入数据
001,181,182,success,1000,7
001,182,183,success,3000,10
001,183,184,success,5000,10
001,182,183,success,6000,15
001,181,183,success,7000,8
#输入此数据,mw为6000,输出结果
001,181,183,success,8000,19
#输入此数据,wm为8000,输出结果
001,181,183,success,10000,30

根据指定的CEP复杂事件匹配模式,当输入"001,181,183,success,8000,19"这条数据后,会对事件时间6000之前的事件进行匹配,此时事件时间为1000、3000、5000、6000的事件符合"A+ B"模式序列。当输入"001,181,183,success,10000,30"这条数据后,会对事件时间8000之前的事件进行匹配,此时事件时间为7000、8000的事件符合"A+ B"模式序列。最终输出结果如下:

复制代码
+----+----+-------------------------+-------------------------+------------+-------------------------+
| op |sid |                start_dt |                  end_dt |avgDuration |                      dt |
+----+----+-------------------------+-------------------------+------------+-------------------------+
| +I |001 | 1970-01-01 08:00:01.000 | 1970-01-01 08:00:05.000 |          9 | 1970-01-01 08:00:06.000 |
| +I |001 | 1970-01-01 08:00:07.000 | 1970-01-01 08:00:07.000 |          9 | 1970-01-01 08:00:08.000 |

在MESURES和DEFINE子句中使用Aggregation函数时,需要注意如下两点:

  1. 聚合函数中可以引用多个列,但是这些列必须来自于同一个单独模式。例如SUM(A.price * A.tax) 是有效的,而 AVG(A.price * B.tax) 则是无效的。

  2. 目前在SQL CEP中设置聚合函数时,不支持DISTINCT aggregation。

Pattern定义

"MATCH_RECOGNIZE"语句中可以在DEFINE中指定模式名称和条件,然后通过PATTERN子句进行模式声明,整个模式声明需要使用括号括起来,多模式使用空格隔开,使用格式如下:

PATTERN (A B+ C* D)

"(A B)"这种模式声明意为A和B模式之间连接是严格的,两个模式之间不能存在没有映射到A模式和B模式的行。以上模式中"+"、"*"符号表示Pattern中的量词,有如下几种情况:

  • * :表示匹配该模式0或者多行。

  • +:表示匹配该模式1或者多行。

  • ? :表示匹配该模式0或者1行。

  • {n}:表示匹配该模式严格n行(n>0)。

  • {n,} : 表示匹配该模式n或者更多行(n≥0)。

  • {n,m} : 表示匹配该模式在n到m(包含)行之间(0≤n≤m,0<m)。

  • {,m}:表示匹配该模式在0到m(包含)行之间(m>0)。

贪婪模式和勉强模式

在Flink SQL CEP中使用"*、+、{n,}"量词进行匹配模式时默认是贪婪模式匹配,所谓贪婪模式匹配就是尽可能多的匹配数据,例如Pattern为"(A B* C)"匹配的事件序列为:匹配A模式一个事件,紧跟匹配B模式的多个事件,最后跟上匹配C模式的一个事件,"B*"默认贪婪模式会尽可能多的匹配符合B模式的事件。我们也可以设置"(A B*? C)","B*?"表示将匹配B模式尽可能少的行,这就是勉强模式,可见勉强模式与贪婪模式区别在与给量词后设置"?"符号。

下面通过一个案例来演示贪婪模式和勉强模式使用。该案例读取Kafka基站日志数据,设置Pattern(A B* C)匹配通话事件,A模式表示通话时间大于10秒事件,B模式表示通话时间小于15秒事件,C模式表示通话时长大于12秒事件。由于Java和Scala代码类似,这里只给出Java代码实现。

复制代码
//创建TableEnvironment
EnvironmentSettings settings = EnvironmentSettings.newInstance()
        .inStreamingMode()
        .build();
TableEnvironment tableEnv = TableEnvironment.create(settings);

//当某个并行度5秒没有数据输入时,自动推进watermark
tableEnv.getConfig().set("table.exec.source.idle-timeout","5000");

//读取Kafka基站日志数据,通过SQL DDL方式定义表结构
tableEnv.executeSql("" +
        "create table stationlog_tbl (" +
        "   sid string," +
        "   call_out string," +
        "   call_in string," +
        "   call_type string," +
        "   call_time bigint," +
        "   duration bigint," +
        "   rowtime AS TO_TIMESTAMP_LTZ(call_time,3)," +
        "   WATERMARK FOR rowtime AS rowtime - INTERVAL '2' SECOND" +
        ") with (" +
        "   'connector' = 'kafka'," +
        "   'topic' = 'stationlog-topic'," +
        "   'properties.bootstrap.servers' = 'node1:9092,node2:9092,node3:9092'," +
        "   'properties.group.id' = 'testGroup'," +
        "   'scan.startup.mode' = 'latest-offset'," +
        "   'format' = 'csv'" +
        ")");

//SQL CEP - 匹配符合(1个通话时长大于10的事件、多个通话时长小于15的事件、1个通话时长大于12的事件)模式序列
tableEnv.executeSql("" +
        "select " +
        "   T.sid,T.dt,T.duration " +
        "from stationlog_tbl " +
        " MATCH_RECOGNIZE ( " +
        "   PARTITION BY sid " +
        "   ORDER BY rowtime " +
        "   MEASURES " +
        "       C.rowtime AS dt, " +
        "       C.duration AS duration " +
        "   ONE ROW PER MATCH " +
        "   AFTER MATCH SKIP PAST LAST ROW " +
        "   PATTERN (A B* C) " +
        "   DEFINE " +
        "       A as A.duration > 10," +
        "       B as B.duration < 15," +
        "       C as C.duration > 12" +
        ") T" +
        "").print();

以上代码编写完成后,向Kafka stationlog-topic中输入如下数据:

复制代码
#kafka stationlog-topic中输入数据
001,181,182,success,1000,11
001,182,183,success,3000,12
001,183,184,success,5000,13
001,182,183,success,6000,14
001,181,183,success,7000,16
#输入此条数据,进行匹配结果输出
001,181,183,success,10000,30

由于Flink SQL CEP中设置的Pattern为(A B* C),"A"模式表示匹配通话时长大于10秒的事件;"B*"模式表示尽可能多的匹配通话时长小于15秒的事件(默认为贪婪模式);"C"模式表示通话时长大于12秒的事件。所以最终输出结果如下:

复制代码
+----+-----+-------------------------+---------+
| op | sid |                      dt |duration |
+----+-----+-------------------------+---------+
| +I | 001 | 1970-01-01 08:00:07.000 |      16 |

如果将Pattern设置为(A B*? C),匹配B模式为勉强模式,最终输出结果如下:

复制代码
+----+----+-------------------------+---------+
| op |sid |                      dt |duration |
+----+----+-------------------------+---------+
| +I |001 | 1970-01-01 08:00:05.000 |      13 |
| +I |001 | 1970-01-01 08:00:07.000 |      16 |

Flink SQL CEP 设置Pattern时,最后一个变量不能使用贪婪模式,例如:(A B*)这种使用方式不被允许,可以引入条件为非B的模式(例如C)来解决这种问题,使用形式如下:

复制代码
PATTERN (A B* C)DEFINE
A AS condA(),
B AS condB(),
    C AS NOT condB()
时间约束

Flink SQL CEP中也支持使用within来指定在一定时间内完成Pattern的匹配,当超过指定时间后,已经过期的数据状态就不需要维护,这样可以减少Flink内部维护状态的大小。Flink SQL CEP中使用within形式如下:

复制代码
SELECT T.aid, T.bid
FROM MyTable
MATCH_RECOGNIZE (
      PARTITION BY userid  
      ORDER BY proctime
      MEASURES
        FIRST(A.id) AS aid,
        B.id AS bid
      ONE ROW PER MATCH 
      [AFTER MATCH SKIP ...]
      PATTERN (A+ B) WITHIN INTERVAL '1' HOUR
      DEFINE
        A AS name = 'a',
        B AS name = 'b'
) AS T

可以在Pattern后跟上"WITHIN INTERVAL ..."子句来指定一定时间内匹配模式,当模式序列匹配的第一个事件和最后一个事件时间差在该限制时间内,那么就会输出匹配结果,否则不会进行匹配输出。建议在Flink SQL CEP中使用WITHIN 子句,这样可以有效控制状态防止Flink内存过大。此外由于WITHIN 子句不是标准SQL的一部分,Flink SQL中使用WITHIN 的语法后续Flink版本中可能还会改变。

下面通过一个案例来演示Flink SQL CEP中WITHIN 语法的使用。该案例读取Kafka基站日志数据,设置Pattern(A+ B)匹配通话事件,A模式表示通话状态为fail的事件,B模式表示通话状态为success的事件,限制该模式在5秒内发生才会进行匹配。由于Java和Scala代码类似,这里只给出Java代码实现。

复制代码
//创建TableEnvironment
EnvironmentSettings settings = EnvironmentSettings.newInstance()
        .inStreamingMode()
        .build();
TableEnvironment tableEnv = TableEnvironment.create(settings);

//当某个并行度5秒没有数据输入时,自动推进watermark
tableEnv.getConfig().set("table.exec.source.idle-timeout","5000");

//读取Kafka基站日志数据,通过SQL DDL方式定义表结构
tableEnv.executeSql("" +
        "create table stationlog_tbl (" +
        "   sid string," +
        "   call_out string," +
        "   call_in string," +
        "   call_type string," +
        "   call_time bigint," +
        "   duration bigint," +
        "   rowtime AS TO_TIMESTAMP_LTZ(call_time,3)," +
        "   WATERMARK FOR rowtime AS rowtime - INTERVAL '2' SECOND" +
        ") with (" +
        "   'connector' = 'kafka'," +
        "   'topic' = 'stationlog-topic'," +
        "   'properties.bootstrap.servers' = 'node1:9092,node2:9092,node3:9092'," +
        "   'properties.group.id' = 'testGroup'," +
        "   'scan.startup.mode' = 'latest-offset'," +
        "   'format' = 'csv'" +
        ")");

//SQL CEP - 基站通话失败后如果在5秒内没有通话成功信息就输出告警信息
tableEnv.executeSql("" +
        "select " +
        "   T.sid,T.fail_dt,T.success_dt " +
        "from stationlog_tbl " +
        " MATCH_RECOGNIZE ( " +
        "   PARTITION BY sid " +
        "   ORDER BY rowtime " +
        "   MEASURES " +
        "       FIRST(A.rowtime) AS fail_dt," +
        "       B.rowtime AS success_dt " +
        "   ONE ROW PER MATCH " +
        "   AFTER MATCH SKIP TO LAST B " +
        "   PATTERN (A+ B) WITHIN INTERVAL '5' SECOND " +
        "   DEFINE " +
        "       A as A.call_type = 'fail'," +
        "       B as B.call_type = 'success'" +
        ") T" +
        "").print();

以上代码完成后,向Kafka stationlog-topic中输入如下数据:

复制代码
#kafka stationlog-topic中数据
001,181,182,fail,1000,10
002,181,182,fail,2000,11
001,182,183,fail,3000,12
001,183,184,success,5000,14
#输入此条数据,输出基站001匹配结果
003,181,183,fail,7000,15
002,181,183,success,8000,16
#输入此条数据,不会输出基站002匹配结果
003,181,183,success,10000,17

当输入"003,181,183,fail,7000,15"数据后,此时watermark达到5000,基站001中事件时间为1000、3000、5000的事件匹配模式,进行结果输出。当"003,181,183,success,10000,17"数据输入后,watermark达到8000,基站002中事件时间为2000、8000的事件匹配模式,但是由于两者相差超过指定的5秒,所以该事件序列不会匹配。最终控制台输出结果如下:

复制代码
+----+----+-------------------------+-------------------------+
| op |sid |                 fail_dt |              success_dt |
+----+----+-------------------------+-------------------------+
| +I |001 | 1970-01-01 08:00:01.000 | 1970-01-01 08:00:05.000 |

匹配后的开始策略

在"MATCH_RECOGNIZE"语句中可以通过"AFTER MATCH SKIP ..."子句指定匹配后的开始策略,所谓匹配后开始策略指的是当一个Pattern被匹配后,从何处开始下一个Pattern匹配过程。有以下四种不同的开始策略:

  • SKIP PAST LAST ROW :匹配成功后,从匹配成功事件序列中最后一个事件的下一个事件继续模式匹配。

  • SKIP TO NEXT ROW :匹配成功后,从匹配成功事件序列中第一个事件的下一个事件开始搜索新匹配项。默认为该策略。

  • SKIP TO LAST variable :匹配成功后,跳转到当前匹配的指定模式,从该模式最后一个事件开始继续模式匹配。

  • SKIP TO FIRST variable :匹配成功后,跳转到当前匹配的指定模式,从该模式第一个事件继续模式匹配。

以上策略也可以决定一个事件出现在多少个匹配项中,如:使用SKIP PAST LAST ROW 策略,每个事件最多只能属于一个匹配项。为了更好的理解这些策略之间的差异,通过一个示例来解释各策略之间的区别。

以下是FlinkSQL实时表描述是股票交易价格数据:

复制代码
symbol   price      rowtime
股票代码 价格       事件时间
======  ======  =====================
  XYZ	  7 	 2018-09-17 10:00:01
  XYZ     9 	 2018-09-17 10:00:02
  XYZ     10	 2018-09-17 10:00:03
  XYZ     5 	 2018-09-17 10:00:04
  XYZ     10	 2018-09-17 10:00:05
  XYZ     7 	 2018-09-17 10:00:06
  XYZ     14	 2018-09-17 10:00:07

通过Flink SQL CEP匹配符合(A+ C)的Pattern,A模式表示股票总价格小于30的事件,C模式表示非A模式,即加上某事件price后股票总价格大于30的事件。如下:

复制代码
SELECT *
FROM Ticker
MATCH_RECOGNIZE(
        PARTITION BY symbol
        ORDER BY rowtime
        MEASURES
            SUM(A.price) AS sumPrice,
            FIRST(rowtime) AS startTime,
            LAST(rowtime) AS endTime
        ONE ROW PER MATCH
        [AFTER MATCH STRATEGY]
        PATTERN (A+ C)
        DEFINE
            A AS SUM(A.price) < 30
    )

以上CEP根据使用的"AFTER MATCH ...."策略不同产生不同结果。

1) 设置AFTER MATCH SKIP PAST LAST ROW 策略

这种策略会从匹配成功事件序列中最后一个事件的下一个事件继续模式匹配。结果如下:

复制代码
symbol   sumPrice        startTime              endTime
======== ========== ===================== =====================
 XYZ      26         2018-09-17 10:00:01   2018-09-17 10:00:04
 XYZ      17         2018-09-17 10:00:05   2018-09-17 10:00:07

第一个结果对应#1,#2,#3,#4 行匹配结果。第二行结果对应 #5,#6, #7 行匹配。

2) 设置AFTER MATCH SKIP TO NEXT ROW策略

这种策略会从匹配成功事件序列中第一个事件的下一个事件开始新匹配,结果如下:

复制代码
symbol   sumPrice        startTime              endTime
======== ========== ===================== =====================
 XYZ      26         2018-09-17 10:00:01   2018-09-17 10:00:04
 XYZ      24         2018-09-17 10:00:02   2018-09-17 10:00:05
 XYZ      25         2018-09-17 10:00:03   2018-09-17 10:00:06
 XYZ      22         2018-09-17 10:00:04   2018-09-17 10:00:07
 XYZ      17         2018-09-17 10:00:05   2018-09-17 10:00:07

第一个结果对应#1,#2,#3,#4 行匹配结果;第二个结果对应 #2,#3,#4,#5 行匹配结果;第三个结果对应 #3,#4,#5, #6 行匹配结果;第四个结果对应#4,#5,#6, #7 行匹配结果;最后一行结果对应#5,#6, #7 行匹配结果。

3) 设置AFTER MATCH SKIP TO LAST A

当Pattern匹配成功后,这种策略需要指定一个模式名称指定跳转到当前匹配的哪个模式,然后从该模式的最后一行开始新的匹配。这里指定跳转到A模式的最后一行开始新的匹配。结果如下:

复制代码
symbol   sumPrice        startTime              endTime
======== ========== ===================== =====================
 XYZ      26         2018-09-17 10:00:01   2018-09-17 10:00:04
 XYZ      25         2018-09-17 10:00:03   2018-09-17 10:00:06
 XYZ      17         2018-09-17 10:00:05   2018-09-17 10:00:07

第一个结果对应#1,#2,#3,#4 行匹配结果;第二个结果对应 #3,#4,#5, #6 行匹配结果;第三个结果对应 #5,#6, #7 行匹配 结果。

需要注意:对于"SKIP TO LAST variable"策略,可能跳转的模式无法匹配到对应的行,例如:A* ,这种情况会抛出运行异常,跳转的模式必须有对应的行进行匹配才可以。

4) 设置AFTER MATCH SKIP TO FIRST A

当Pattern匹配成功后,这种策略需要指定一个模式名称指定跳转到当前匹配的哪个模式,然后从该模式的第一行开始新的匹配。这里指定跳转到A模式的第一行开始新的匹配。该实例中"AFTER MATCH SKIP TO FIRST A"将会产生一个无限循环,所以会抛出异常.在多个模式组成的模式序列中,可以指定为跳转到其他中间某个模式的开始一行进行新的匹配。