Flink CEP (九) CEP的状态机实现

文章目录

我们分析过 CEP 检测处理的流程,可以认为检测匹配事件的过程中会有“初始(没有任何匹配)”“检测中(部分匹配成功)”“匹配成功”“匹配失败”等不同的“状态”。随着每个事件的到来,都会改变当前检测的“状态”;而这种改变跟当前事件的特性有关、也跟当前所处的状态有关。这样的系统,其实就是一个 “状态机”(state machine) 。这也正是正则表达式底层引擎的实现原理。

所以 Flink CEP 的底层工作原理其实与正则表达式是一致的,是一个“非确定有限状态自动机”(Nondeterministic Finite Automaton,NFA)。NFA 的原理涉及到较多数学知识,我们这里不做详细展开,而是用一个具体的例子来说明一下状态机的工作方式,以更好地理解 CEP的原理。

我们回顾一下快速上手应用案例,检测用户连续三次登录失败的复杂事件。用 Flink CEP 中的 Pattern API 可以很方便地把它定义出来;如果我们现在不用 CEP,而是用 DataStream API 和处理函数来实现,应该怎么做呢?

这需要设置状态,并根据输入的事件不断更新状态。当然因为这个需求不是很复杂,我们也可以用嵌套的 if-else 条件判断将它实现,不过这样做的代码可读性和扩展性都会很差。更好的方式,就是实现一个状态机。

在这里插入图片描述

上图所示即为状态转移的过程,从初始状态(INITIAL)出发,遇到一个类型为fail 的登录失败事件,就开始进入部分匹配的状态;目前只有一个 fail 事件,我们把当前状态记作 S1。基于 S1 状态,如果继续遇到 fail 事件,那么就有两个 fail 事件,记作 S2。基于 S2状态如果再次遇到 fail 事件,那么就找到了一组匹配的复杂事件,把当前状态记作 Matched,就可以输出报警信息了。需要注意的是,报警完毕,需要立即重置状态回 S2;因为如果接下来再遇到 fail 事件,就又满足了新的连续三次登录失败,需要再次报警。

而不论是初始状态,还是 S1、S2 状态,只要遇到类型为 success 的登录成功事件,就会跳转到结束状态,记作 Terminal。此时当前检测完毕,之前的部分匹配应该全部清空,所以需要立即重置状态到 Initial,重新开始下一轮检测。所以这里我们真正参与状态转移的,其实只有 Initial、S1、S2 三个状态,Matched 和 Terminal 是为了方便我们做其他操作(比如输出报警、清空状态)的“临时标记状态”,不等新事件到来马上就会跳转。

完整代码如下:

public class NFAExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        //1.获取登录数据流
        KeyedStream<LoginEvent, String> loginEventStream = 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)
                )
                .keyBy(event -> event.userId);

        //2.数据按照顺序依次输入,用状态机进行处理,状态跳转
        SingleOutputStreamOperator<String> warningStream = loginEventStream.flatMap(new StateMachineMapper());

        //3.输出
        warningStream.print();

        env.execute();

    }

    //自定义实现RichFlatMapFunction
    public static class StateMachineMapper extends RichFlatMapFunction<LoginEvent, String> {

        //声明状态机当前的状态
        private ValueState<State> currentState;

        @Override
        public void open(Configuration parameters) throws Exception {
            currentState = getRuntimeContext().getState(new ValueStateDescriptor<State>("state", State.class));
        }

        @Override
        public void flatMap(LoginEvent value, Collector<String> out) throws Exception {
            //如果状态为空,进行初始化
            State state = currentState.value();
            if (state == null){
                state = State.Initial;
            }

            //基于当前状态跳转到下一秒
            State nextState = state.transition(value.eventType);

            //判断当前状态的特殊情况,直接进行跳转
            if(nextState == State.Matched){
                //检测到了匹配,输出报警信息; 不更新状态,就是跳转回S2
                out.collect(value.userId + "连续三次登陆失败");
            }else if(nextState == State.Terminal){
                //直接将状态更新为初始状态,重新开始检测
                currentState.update(State.Initial);
            }else{
                //状态覆盖跳转
                currentState.update(nextState);
            }
        }
    }

    //状态机实现
    public enum State {
        Terminal, //匹配失败,终止状态
        Matched,  //匹配成功

        // S2 , 传入基于S2 状态可以进行的一系列状态转移
        S2(new Transition("fail", Matched), new Transition("success", Terminal)),

        // S1
        S1(new Transition("fail", S2), new Transition("success", Terminal)),

        // 初始状态
        Initial(new Transition("fail", S1), new Transition("success", Terminal));

        private Transition[] transitions; // 当前状态的转移机制

        // 状态的构造方法,可以传入一组状态转移规则来定义状态
        State(Transition... transitions) {
            this.transitions = transitions;
        }

        // 状态的转移方法,根据当前输入事件类型,从定义好的转移规则中找到下一个状态
        public State transition(String eventType) {
            for (Transition transition : transitions) {
                if (transition.getEventType().equals(eventType)) {
                    return transition.getTargetState();
                }
            }
            // 如果没有找到转移规则,说明已经结束,回到初始状态
            return Initial;
        }
    }

    //定义一个状态转移类,包含当前引起状态转移的事件类型,以及转移的目标状态
    public static class Transition {
        // 触发状态转移的当前事件类型
        private String eventType;

        // 转移的目标状态
        private State targetState;

        public Transition(String eventType, State targetState) {
            this.eventType = eventType;
            this.targetState = targetState;
        }

        public String getEventType() {
            return eventType;
        }

        public State getTargetState() {
            return targetState;
        }
    }
}

Gitee上的源代码

运行代码,可以看到输出与之前 CEP 的实现是完全一样的。显然,如果所有的复杂事件处理都自己设计状态机来实现是非常繁琐的,而且中间逻辑非常容易出错;所以 Flink CEP 将底层 NFA 全部实现好并封装起来,这样我们处理复杂事件时只要调上层的 Pattern API 就可以,无疑大大降低了代码的复杂度,提高了编程的效率。
在这里插入图片描述
Flink CEP 是 Flink 对复杂事件处理提供的强大而高效的应用库。

CEP 在实际生产中有非常广泛的应用。对于大数据分析而言,应用场景主要可以分为统计分析和逻辑分析。企业的报表统计、商业决策都离不开统计分析,这部分需求在目前企业的分析指标中占了很大的比重,实时的流数据统计可以通过 Flink SQL 方便地实现;而逻辑分析可以进一步细分为风险控制、数据挖掘、用户画像、精准推荐等各个应用场景,如今对实时性要求也越来越高,Flink CEP 就可以作为对流数据进行逻辑分析、进行实时风控和推荐的有力工具。

所以 DataStream API 和处理函数是 Flink 应用的基石,而 SQL 和 CEP 就是 Flink 大厦顶层扩展的两大工具。Flink SQL 也提供了与 CEP 相结合的“模式识别”(Pattern Recognition)语句——MATCH_RECOGNIZE,可以支持在 SQL 语句中进行复杂事件处理。尽管目前还不完善,不过相信随着 Flink 的进一步发展,Flink SQL 和 CEP 将对程序员更加友好,功能也将更加强大,全方位实现大数据实时流处理的各种应用需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值