在领域驱动设计的实践中,我一直有个问题没想明白 – 领域聚合对象对于某一类事物的抽象是很准确的,比如库存、商品、会员,这个和现实生活中的能一一对应上。通过充血模型将数据和行为统一起来,很容易理解。 但是类似订单、审批流程,这类逻辑一直没有很好的抽象方式。从内部状态看,他纯粹是个有限状态机,没啥特别的商业逻辑;从行为看,却发现他的所有状态变更,都是需要调用外部模块,并根据外部模块的返回结果,再确定迁越到哪个状态。这和我们以前充血模型中将行为控制在只修改本身状态的做法是相抵触的。 比如库存实体,不管是出库、入库、下单、发货、上架、下架,改变的只是库存商品的状态和数量,不涉及外部模块。 但是对比订单,它的下单、支付、发货、签收等需要支付、库存、配送模块一起配合返回结果,还要处理其他模块的各种各样的失败情况(库存不足,用户没钱支付、用户拒收等等)。 这就尴尬了,这样的实体聚合的方法必须依赖外部模块,耦合拆不开,系统不能自洽,它的开发、测试、部署上线都是麻烦的事情。 这个疑惑直到我了解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),然后扣取用户的帐户余额 ,最终完成订单。 库存不足、帐户余额不足都将导致订单失败。这里库存和支付模块都是单独的服务,有自己的数据存储。
JTA控制器统一向三个服务发起prepare的请求,如果都确认,就可以同时事务提交(请注意这里订单一旦生成就已经是commited状态了)。提交过程中所有的资源是锁定等待中的,如果账务扣款慢了(比如银行接口反应慢了),资源就会一直等待锁定,直到事务超时为止,这样对系统资源的消耗是巨大的。
ls
Saga特点
Saga Execution Coordinator(SEC) 我们有两种方式来处理Saga中的业务流程和失败补偿 做法一:让每个服务给后续步骤发送消息 做法二:通过一个统一的执行协调器来完成
如果采用做法一,一旦有其中一个步骤失败,补偿操作非常麻烦,还会给模块带来不必要的耦合性,所以我们倾向于有一个统一的协调器来完成这个操作。
 从严格意义上来说,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持久化
|