当前位置: 首页 > 科技观察

状态机在马蜂窝机票订单交易系统中的应用与优化实践

时间:2023-03-17 22:03:41 科技观察

在设计交易系统时,稳定性、可扩展性和可维护性是我们需要关注的重点。本文将探讨如何通过状态机在交易系统中的应用来解决上述问题。关于马蜂窝机票订单交易系统交易系统往往具有订单维度多、状态多、交易环节长、流程复杂等特点。以马蜂窝运输业务中的机票交易为例,用户提交的订单中可能包含很多机票信息之外的信息,比如保险或者其他附加产品。其中,保险又分为飞行意外险、航班延误险、组合险等多种类型。从用户的角度来看,一个订单是由购买的主产品机票和附加产品组成的。支付时,整体支付,如需退票或退票,也可部分操作;从供应商的角度看,一个订单中每个产品背后都有一个独立的供应商,有机票的供应商,有保险的供应商,有保险的供应商。每个供应商的订单都需要单独出票并独立结算。用户的购买和支付流程与供应商的出票和担保流程构成一个有机的整体,穿插在票务交易系统中,密不可分。状态机在机票交易系统中的应用与优化有限状态机(以下简称状态机)的概念是用来对事物或对象的行为进行建模的工具。状态机将复杂的逻辑简化为有限数量的稳定状态,构建这些状态之间的转换、动作等行为的数学模型,并在稳定状态下判断事件。向状态机输入一个事件,状态机根据当前状态和触发的事件唯一确定一次状态转移。图1:FSM工作原理业务系统的本质是描述现实世界,所以几乎所有的业务系统都会有状态机的影子。订单交易流程自然适合状态机模型的应用。以用户的支付流程为例,如果不使用状态机,则在收到支付成功回调时需要进行一系列动作:查询支付流水号,记录支付时间,修改main状态订单支付,并通知供应商出票,记录通知出票时间,修改出票子订单状态为出票……逻辑非常繁琐,代码耦合严重。为了使交易系统的订单状态按照设计流程正确向下流动,例如当前用户已经支付,不允许再次支付;当前订单已经关闭,不能再通知工单等,我们通过应用状态机对工单进行优化交易系统抽取所有状态、事件、动作,统一管理复杂的状态转换逻辑取代冗长的ifelse判断,让门票交易系统中复杂的问题解耦,变得直观方便。易于操作,使系统更易于维护和管理。状态机设计在数据库设计层面,我们将整个订单视为主订单,将供应商的订单视为子订单。假设一个用户同时购买了机票和保险,因为机票和保险对应的是不同的供应商,即一个主订单对应两个子订单。主订单order记录用户的信息(UID、联系方式、订单总价等),子订单sub_order记录产品类型、供应商订单号、结算价等。同时,我们分离forwardticket发行和反向退票和重新预订到不同的子系统。这样各个子系统完全独立,有利于系统的维护和扩展。对于ticketforward子系统,有两套状态机:主订单状态机负责管理订单的状态,包括订单创建成功、支付成功、交易成功、订单关闭等;sub-order状态机负责管理sub_order的状态,维护从预订成功到出票的过程。同样,对于反向退款和重新预订子系统,将有它们自己的状态机。图2:主票单状态机状态转换示例框架选择目前业界常用的状态机引擎框架主要有SpringStatemachine、Stateless4j、Squirrel-Foundation等,经过结合实际业务进行横向比较,我们最终决定使用Squirrel-Foundation,主要是因为:代码量适中,扩展和维护都比较容易;StateMachine轻量级,实例创建成本小;丰富的入口点支持状态入口、状态完成、异常等节点的监控,使得转换过程中有足够的入口点;支持使用注解定义状态转换,使用方便;在设计上不支持单例复用,只能和New一起使用,所以状态机自身的生命流管理非常清晰,不会因为状态机单例复用的问题而带来麻烦。MSM的设计和实现结合大流量的业务逻辑,我们在Squirrel-Foundation的基础上抽取并重新封装了Action的概念,结合状态迁移和异步消息,封装到MSM框架(MFWStateMachine),用于实现业务订单状态定义、事件定义、状态机定义,并以注解的形式描述状态转换。我们认为状态转换必然伴随着异步消息,所以把一个流程中必须成功的数据库操作放到事务中,而把允许失败重试、对实时性要求不高的操作放到事务中。异步消息消费中间的过程。以机票订单支付成功为例,当机票订单支付成功时,会涉及修改订单状态为已支付、更新支付流水号等,在一笔交易中;通知供应商开票放在异步消息消费处理中。异步消息的实现使用RocketMQ,主要是考虑到RocketMQ支持两阶段提交,保证消息可靠性,支持重试,支持多消费者组。具体说明如下:1、对于每一个需要在状态转换中执行的动作,都会抽取一个Action类继承自AbstractAction,以支持多种不同的状态转换来执行同一个动作。这主要依赖于publicListmatchConditions()的实现,所以matchConditions只需要返回多个初始状态事件匹配条件键值对即可。每个Action都有一个对应的context类继承自MFWContext类,用于进程saveDB等方法中的通信。2.注册所有Actions,并为每个状态转换的完成或失败添加一个监听器。3、由于对RocketMQ异步消息的依赖,需要一个SpringBean继承BaseMessageSender,它会生成一个异步消息提供者。如果要使用两阶段提交,需要一个类继承BaseMsgTransactionListener,这里可以参考ticket的OrderChangeMes??sageSender和OrderChangeMsgTransactionListener。4.最后实现一个事件触发类。这个类包含一个Apply方法,传入订单PO对象、事件和相应的上下文。每次执行都会实例化一个状态机实例,初始化当前状态,调用Fire方法。5.实例化一个状态机对象,将当前状态设置为数据库对应的状态,调用Fire方法后,最终会执行OrderStateMachine类中注解描述的callMethod方法。我们配置的是callMethod="action",会反映并执行当前类的Action方法。我们对Action方法的实现是通过super.action(from,to,event,context),会执行MFWStateMachine的Action方法。首先会根据当前状态和事件获取对应的Action。这里使用的是“工厂模式”,然后执行Process方法。成功则执行MFWStateMachine类中初始化的TransitionCompleteListener,执行Action的afterProcess方法修改数据库记录并发送消息;如果失败,则执行TransitionExceptionListener,并执行Action的onException方法进行相应处理。综上所述,MSM可以根据Action类的声明和配置动态生成Squirrel-Foundation状态机定义,无需用户重新定义,使MSM的使用更加方便。图3:UML涉水的陷阱1.事务不生效最初我们使用Spring注解进行事务管理,即在Action类的数据库操作方法上添加@Transactional注解,但在实践中发现并没有起作用.经过排查,发现Spring的事务注解是通过AOP切面来实现的。如果在对象内部的方法中调用对象的其他使用AOP注解的方法,则被调用方法的AOP注解将失效。因为在同一个类的内部代码调用中,不会用到代理类。后来我们通过手动开启交易解决了这个问题。2.匹配Actions最初,我们有两种匹配Actions的方式:精确匹配和非精确匹配。精确匹配是指只有当状态转换的初始状态与触发的事件一致时,才能匹配动作;非精确匹配是指只要触发的事件一致就可以匹配到动作。后来发现在某些情况下非精确匹配会出问题,所以改成多条件精确匹配。即在执行状态机被触发时执行的Action方法时,为了准确匹配Action,可以将多个状态转换执行方法匹配到同一个Action,这样Action代码可以复用没有问题。3.有一些情况是绝对不能出现异步消息一致性的,比如没有成功修改数据库就发送消息;或者修改数据库成功,但是发送消息失败;或者在提交数据库事务之前,消息已经发送成功。为了解决这个问题,我们使用了RocketMQ的事务消息功能,它支持两阶段提交。它会先发送预处理消息,然后回调执行本地事务,最后提交或回滚,有助于保证修改数据库信息和发送异步消息的一致性。4、同一个订单数据并发执行不同的事件。在某些情况下,相同的订单数据可能会同时触发不同的事件(毫秒级别)。例如机票主订单处于待支付状态,可以收到支付中心的回调,触发支付成功事件;用户也可以点击取消订单,或者超时支付失败的定时任务可以触发关闭订单事件。如果不进行控制,订单可能看起来已成功支付,然后被取消。我们使用数据库乐观锁来避免这个问题:在执行修改数据库的事务时,更新顺序的语句有对原始状态的条件判断。通过判断更新的行数是否为1,判断是否抛出异常,即生成这样的SQL语句:updateorderwhereorder_id='1234'andorder_status='tobepaid'。这样,如果同时触发并执行了两个事件,谁先提交事务成功,谁就能执行成功;事务中后面提交的事件会因为更新行数为0而执行失败,最终回滚事务,就好像没有发生过一样。使用悲观锁也可以解决这个问题。这样谁先抢到锁就可以执行成功。但是考虑到脚本可能会批量修改数据库,悲观锁存在死锁的潜在问题,我们最终还是采用了乐观锁的方式。基于Squirrel-Foundation总结MSM状态机的定义和声明,提取Action的概念,为Action类配置初始状态、目标状态、触发事件、上下文定义等,使MSM可以基于在Action类的声明和配置上动态生成Squirrel-Foundation状态机的定义,无需用户重新定义,更易操作,更易维护。通过使用状态机,轻松解决了票务订单交易系统的流程复杂性,系统在稳定性、可扩展性和可维护性方面也得到了显着的改善和增强。状态机的使用场景不限于订单交易系统,其他涉及状态变化复杂过程的系统同样适用。希望通过本文的介绍,能够让有了解和使用状态机需求的读者朋友有所收获。本文作者:马蜂窝大运研发团队机票交易系统研发工程师田冬。【本文为专栏作者马蜂窝科技原创文章,作者微信公众号马蜂窝科技(ID:mfwtech)】点此查看作者更多好文