分享

Saga模式和事件驱动

 KILLKISS 2017-11-21

在领域驱动设计的实践中,我一直有个问题没想明白 – 领域聚合对象对于某一类事物的抽象是很准确的,比如库存、商品、会员,这个和现实生活中的能一一对应上。通过充血模型将数据和行为统一起来,很容易理解。
但是类似订单、审批流程,这类逻辑一直没有很好的抽象方式。从内部状态看,他纯粹是个有限状态机,没啥特别的商业逻辑;从行为看,却发现他的所有状态变更,都是需要调用外部模块,并根据外部模块的返回结果,再确定迁越到哪个状态。这和我们以前充血模型中将行为控制在只修改本身状态的做法是相抵触的。
比如库存实体,不管是出库、入库、下单、发货、上架、下架,改变的只是库存商品的状态和数量,不涉及外部模块。
但是对比订单,它的下单、支付、发货、签收等需要支付、库存、配送模块一起配合返回结果,还要处理其他模块的各种各样的失败情况(库存不足,用户没钱支付、用户拒收等等)。
这就尴尬了,这样的实体聚合的方法必须依赖外部模块,耦合拆不开,系统不能自洽,它的开发、测试、部署上线都是麻烦的事情。
这个疑惑直到我了解Saga

Saga是什么

Saga是什么?Saga的定义是“长时间活动的事务”(Long Lived Transaction,后文简称为LLT)。他是普林斯顿大学HECTOR GARCIA-MOLINA教授在1987年的一篇关于分布式数据库的论文中提出来的概念。
Sagas论文

Long Lived从字面意义上不清晰,Long到底意味着多长?事务持续时间是一个小时、一天甚至一周吗?其实都不是,有时候Saga事务时长甚至只有几秒钟,时间跨度并不重要。重要的是什么?关键的是跨系统的多次“事务”,Saga往往由多个外部子事务构成,需要通过多次外部系统的消息交互,才能将整体事务从开始迁移到结束状态,这和我们原来常见的在一个数据库的短事务不一样。比如一个旅行的订单,是由机票、旅馆、租车三个子订单构成,都需要外部的确认,缺任何一个步骤,不能成行,这就是一个典型的LLT。

ACID和BASE事务

相对于一般事务我们习惯ACID - 要么一起成功,要么一起失败。
但是LLT的话,直接ACID,消耗就太大了,想象一下在酒店人员确认房间信息前,我们前一个机票必须一直都处于占位锁定但不能确认出票的状态,机票供应商可不允许你这样玩。
所以Saga采用了BASE(Basic Availability, Soft, Eventual consistency)事务的方式,来避免消耗大量资源的同步、锁定。
举例:电商下单,需要冻结商品库存(将可销售库存-1,已销售库存+1),然后扣取用户的帐户余额 ,最终完成订单。 库存不足、帐户余额不足都将导致订单失败。这里库存和支付模块都是单独的服务,有自己的数据存储。

  • 如果要实现ACID,需要依赖分布式事务2PC提交,2PC在JavaEE的JTA规范中有实现。

    jta

JTA控制器统一向三个服务发起prepare的请求,如果都确认,就可以同时事务提交(请注意这里订单一旦生成就已经是commited状态了)。提交过程中所有的资源是锁定等待中的,如果账务扣款慢了(比如银行接口反应慢了),资源就会一直等待锁定,直到事务超时为止,这样对系统资源的消耗是巨大的。

  • 如果采用BASE事务

    BASE


    BASE事务采用多段本地事务提交,将大事务拆为5个local transaction(T1-T5),并为这些事务准备了冲正操作

    • T1:Order服务先完成开启订单,订单状态Opened

    • T2:Inventory服务完成锁定库存

    • T3:Order服务变更订单状态为Prepared

    • T4:Account服务扣款

    • T5:Order服务变更订单状态为Committed

      其中每步完成都会提交事务。请注意这里的订单多了两个中间状态Opened和Prepared,最后才到Committed,对比ACID直接一步上Committed,BASE冗长了很多。
      这样的优势是每段只锁定本地资源,对其他服务无影响,应付那种超时的情况需要的资源少。缺点是需要自己处理每个步骤失败的情况。和ACID相比,BASE不能轻易的回滚,只能通过补偿(Compensating)操作达到最终一致性。比如在T4扣款阶段,如果发现用户账户余额不足,交易失败。那么就要执行前三步的冲正操作(C3、C2、C1)

    • C3:将订单状态改为Opened

    • C2:将库存解锁

    • C1:将订单置为废弃状态 Cancelled

      注:这里的冲正的概念,英文术语是Compensating(补偿),和原来的事务回滚Roll back不一样,回滚是可以撤销变更回到原有的状态的,但是冲正类似发邮件,你发错了一封邮件,别人收到了错误邮件,你是无法撤回的。只能重新发一封邮件告诉别人:“对不起,刚才发错了,你就当没看见”。
      ACID下如果库存或者余额不足,订单不会生成,直接回滚。BASE下即使最终订单失败取消,订单还是会生成,对世界的改变已经不可撤销了。

ls

Saga特点

  • Saga Execution Coordinator(SEC)
    我们有两种方式来处理Saga中的业务流程和失败补偿
    做法一:让每个服务给后续步骤发送消息
    做法二:通过一个统一的执行协调器来完成

    如果采用做法一,一旦有其中一个步骤失败,补偿操作非常麻烦,还会给模块带来不必要的耦合性,所以我们倾向于有一个统一的协调器来完成这个操作。
    SEC
    从严格意义上来说,Saga本身是不包含业务逻辑的,Saga更倾向于“Process”而不是“Logic”。这里有人会不同意,我们订单流程里面那么多if else判断难道不是业务逻辑吗?其实这些if else正是需要根据业务步骤的处理结果来进行下一步,或者发起冲正操作。如果你用有限状态机来抽象,也不需要用If else了。Saga Execution Coordinator就是一个基于事件驱动的状态机的协调器。

  • 多实例
    有了状态就需要保存状态,SEC的每个实例只包含单个Saga的状态,这就意味着如果你有多个订单,就会启动多个SEC。
  • Event Handling
    SEC需要用到Event Handling机制。我们有两个选择,一个是创建一个上帝Event Handler,来分发执行所有的Event。另外一个就是让Saga的SEC实例自己来订阅属于自身的Event。第二个方案比较合适一些。

  • 持久化
    SEC的事务可能会持续很长时间,为了考虑停机重启等场景,我们会将Saga持久化

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多