以下单出售股票为例,更好地了解如何在微服务中应用Saga模式。Saga中的动作都是编排过的:每个动作TX的执行都是在回应另一个动作,但是这个过程并不需要一个总指挥或者总协调人。将这个下单出售股票的任务拆分成5个子任务:T1—创建订单;T2—fee服务计算和收取相应的费用;T3—account transaction服务预留股票仓位;T4—market服务将购买订单提交到市场;T5—更新所提交的订单的状态。
![](https://img.haomeiwen.com/i7749898/6cbca689bccca675.jpeg)
(1)order服务执行T1,发出OrderCreated事件消息。
(2)fee服务、account transaction服务以及market服务消费这个OrderCreated事件消息。
(3)fee服务和account transaction服务执行对应的动作(T2和T3),然后分别发出事件消息,market服务会消费这两个事件消息。
(4)当订单的前提条件都满足以后,market服务将订单发布到市场上(T4),然后发出OrderPlaced事件消息。
(5)order服务消费OrderPlaced事件消息然后更新订单的状态(T5)。
每个任务都可能会失败——在这种情况下,应用需要回滚到一个合理且一致的状态。每个服务都有一个补偿动作:C1—取消客户创建的订单;C2—撤销手续费并退还给客户;C3—撤销预留的股票仓位;C4——取消发布到市场上的订单;C5——撤销订单的状态。
如何触发这些动作呢?猜对了,是事件!比如,假设将订单发布到市场上时出现了故障,market服务会发送一个OrderFailed事件来取消这个订单,然后Saga中的其他所有服务都会消费这个事件消息。在收到这个事件后,每个服务会执行相应的行动:order服务会取消客户的订单;transaction服务会取消预留的股票;而fee服务会将撤销收取的费用,依次执行C1、C2、C3的动作。
![](https://img.haomeiwen.com/i7749898/d9bf2f0be6651fe8.jpeg)
这种回滚形式的目的是让系统在语义上达到一致,而非数学意义上的一致。系统将一个操作回滚后并不一定能恢复到和之前完全一样的状态。假设计算手续费的任务会发送一封邮件,但我们并不能将邮件撤回,所以就需要重新发送一封确认错误的邮件来代替,并告诉客户fee服务收取的手续费会退回到账户中。
一个流程中的每个动作可能有多个对应的补偿动作。这种方式会增加系统复杂度,这种复杂度不仅存在于预测失败场景并提前做好准备上,还体现在编码和测试上。尤其是因为交互牵涉的服务越多,回滚的复杂度可能就越高。
在构建反映真实世界环境的微服务时,预见到失败场景并做相应准备是很重要的一块内容,操作的隔离性反而没那么至关重要。在设计微服务时,我们需要把补偿考虑在内,以确保整个应用有足够的恢复能力。
优势
这种编排式的交互很有用,因为参与交互的各个服务之间不需要明确知道对方的存在,这也就确保了它们之间是松耦合的。相应地,这也提高了每个服务的自治性。
劣势
没有哪个代码片段能完整体现下单流程的整个执行过程。这会增加验证和测试的难度,因为这些验证工作会被分摊到不同的服务上。它同时还增加了状态管理的复杂度:每个服务需要在处理订单的过程中反映出不同的状态,比如,order服务必须跟踪订单是否被创建、发布、取消、拒绝等。这些额外的复杂度增加了分析理解整个系统的难度。
编排同样还会引入服务的循环依赖问题:order服务会发出事件消息供market服务消费,但是,反过来,它也会消费market服务发出的事件消息。这种循环依赖会导致在发布阶段服务之间是相互耦合在一起的。
一般来说,当选择异步的通信方式时,开发者必须在监控和跟踪技术方面上投入较多资源来确保能够跟踪系统的执行流程。在出现错误或者需要调试一个分布式系统时,监控和追踪能力就能够发挥飞行记录仪的职责。开发者应该将所有发生的事情都保存下来,这样就可以在事后调查每条事件消息,以搞清在这些系统中当时到底发生了什么。对于编排型的交互来说,这种监控和跟踪的能力是至关重要的。
摘取自 摩根·布鲁斯和保罗·A.佩雷拉的《微服务实战》
网友评论