分布式事务Saga模式
两阶段提交2PC是分布式事务中最强大的事务类型之一,两段提交就是分两个阶段提交,第一阶段询问各个事务数据源是否准备好,第二阶段才真正将数据提交给事务数据源,当需要同时更新多个数据源实体时,例如确认订单并立即更新库存时,它非常有用。
但是,当您使用微服务时,情况会变得复杂。每个服务都拥有自己的数据库系统,每个服务不能再越过别的服务直接访问那个服务的数据库了。
比如服务A有自己的数据库O1,服务B有自己的数据库O2,服务B如果想同时更新数据库O1和O2,就不能越过服务B直接操作数据库O1,而两段提交正是适合这样的场合,服务B可以在一个JTA事务中同时调用数据库O1的XA数据源JNDI,再调用数据库O2的XA数据源JNDI,那么O1和O2的数据就会实现两段提交,O1数据库执行修改或插入操作后其实没有保存到数据库,只有等O2数据库执行修改或插入操作后,才在第二阶段提交确认保存,这个过程如果有任何出错,数据库O1和数据库O2如同没有执行修改或插入一样,两者数据状态是一致的。
因此,在微服务架构中,因为一个服务不能越过其他服务直接访问它们的数据库,两段提交可能不适用,当然EJB提供了基于容器的跨服务分布式事务,虽然听起来很容易,但是因为是同步操作,对网络硬件要求比较高,一旦发生事务出错,需要手工介入数据库进行强制回滚,如果跨N个服务调用出错,出错定位是非常困难的,很难判断问题出在哪个服务器或哪段通讯上,不可能进行时间和空间的同时定位。
基于以上原因,本文介绍Saga模式是一种分布式异步事务,一种最终一致性事务,是一种柔性事务,当然从传统ACID同步事务过渡到异步事务需要很多思维方式切换和步骤证明,可见本站其他文章,这里。
下面以电子商务案例说明Saga模式实现:
API网关 后面是四个微服务:OrderService(订购) StockService(库存) PaymentService(支付) 和DeliveryService(货运)
在这个例子中,我们不能将“下订单,向客户收费,更新库存”这几个动作放入一个ACID事务中。但是又必须一致地执行这整个流程,这就需要创建一个分布式事务。
我们都知道,实施分布式任务是多么困难,而且不幸的是,事务也不例外。处理瞬态状态,服务之间的最终一致性,隔离和回滚是应该在设计阶段考虑的情况。
幸运的是,我们已经为它提出了一些很好的模式,因为我们已经实施了二十多年的分布式事务。今天我想谈论的那个叫做Saga模式。
Saga是最着名的分布式事务模式之一。关于它的第一篇文章早在1987年就已经发表了, 从那以后它一直是一个受欢迎的解决方案。
Saga是一系列本地交易,每笔事务都会更新单个服务中的数据。第一个事务由系统外部请求启动,然后每个后续步骤由前一个事件完成而触发。
对于我们这个电子商务示例,非常高层次级的Saga设计实现如下所示:
现在有两种不同的方式来实现saga事务,最流行的两种方式是:
事件/编排Choreography:没有中央协调器(没有单点风险)时,每个服务产生并聆听其他服务的事件,并决定是否应采取行动。
命令/协调orchestrator:中央协调器负责集中处理事件的决策和业务逻辑排序。
让我们对这两个实现进行更深入的了解,以了解它们的工作方式。
事件/编排Events/Choreography
在Events/Choreography方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何Saga参与者听到都意味着事务结束。
让我们看看它在我们的电子商务示例中的外观:
步骤如下: 1. 订单服务保存新订单,将状态设置为pengding挂起状态,并发布名为ORDER_CREATED_EVENT的事件。
支付服务监听ORDER_CREATED_EVENT,并公布事件BILLED_ORDER_EVENT。
库存服务监听BILLED_ORDER_EVENT,更新库存,并发布ORDER_PREPARED_EVENT。
货运服务监听ORDER_PREPARED_EVENT,然后交付产品。最后,它发布ORDER_DELIVERED_EVENT
最后,订单服务侦听ORDER_DELIVERED_EVENT并设置订单的状态为concluded完成。
在上面的情况下,如果需要跟踪订单的状态,订单服务可以简单地监听所有事件并更新其状态。 在这个案例中,除了订单服务以外的其他服务都是订单服务的子服务,也就是说,为完成一个订单服务,需要经过这些步骤,订单服务与这些服务是包含与被包含关系,因此,订单服务在业务上天然是一个协调器。
回滚分布式事务并不是免费的。通常情况下,您必须实施额外操作才能弥补以前所做的工作。
假设库存服务在事务过程中失败了。让我们看看回滚是什么样子的:
1.库存服务产生PRODUCT_OUT_OF_STOCK_EVENT ;
2.订购服务和支付服务会监听到上面库存服务的这一事件: 1. 支付服务会退款给客户。 2. 订单服务将订单状态设置为失败。
请注意,为每个事务定义一个公共共享ID非常重要,因此每当您抛出一个事件时,所有侦听器都可以立即知道它引用的是哪个事务。
saga事件/编排设计的优点和缺点
事件/编排是实现Saga模式的自然方式; 它很简单,容易理解,不需要太多的努力来构建,所有参与者都是松散耦合的,因为他们彼此之间没有直接的耦合。如果您的事务涉及2至4个步骤,则可能是非常合适的。
但是,如果您在事务中不断添加额外步骤,则此方法可能会很快变得混乱,因为很难跟踪哪些服务监听哪些事件。此外,它还可能在服务之间添加循环依赖,因为它们必须订阅彼此的事件。
最后,使用这种设计来实现测试将会非常棘手。为了模拟交易行为,您应该运行所有服务。
Saga的命令/协调模式
这里我们定义了一项新服务,全权负责告诉每个参与者该做什么以及什么时候该做什么。saga协调器orchestrator以命令/回复的方式与每项服务进行通信,告诉他们应该执行哪些操作。
订单服务保存pending状态,并要求订单Saga协调器(简称OSO)开始启动订单事务。
OSO向收款服务发送执行收款命令,收款服务回复Payment Executed消息
OSO向库存服务发送准备订单命令,库存服务将回复OrderPrepared消息
OSO向货运服务发送订单发货命令,货运服务将回复Order Delivered消息。
OSO订单Saga协调器必须事先知道执行“创建订单”事务所需的流程(通过读取BPM业务流程XML配置获得)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。当你有一个中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
类似saga协调器的标准模式是状态机,其中每个转换对应于命令或消息。状态机是构建定义明确的行为的极好模式,因为它们易于实现,特别适用于测试。
命令/协调器设计的优点和缺点
基于协调器的Saga有很多好处:
避免服务之间的循环依赖关系,因为saga协调器会调用saga参与者,但参与者不会调用协调器
集中分布式事务的编排
只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
更容易实施和测试
在添加新步骤时,事务复杂性保持线性,回滚更容易管理
如果在第一笔交易还没有执行完,想改变有第二笔事务的目标对象,则可以轻松地将其暂停在协调器上,直到第一笔交易结束。
然而,这种方法仍然有一些缺点,其中之一是有在协调器中集中太多逻辑的风险,并最终导致智能协调器会告诉愚蠢的服务该做什么的架构,这不符合Martinfowler定义微服务应该是聪明的服务+哑巴或愚蠢的管道。
Saga协调器的另一个缺点是它会稍微增加基础设施的复杂性,因为您需要管理额外的服务。同时增加单点风险,协调器一旦出问题,全局影响。
Saga模式提示
为每个事务创建一个唯一的ID 为每项事务设置一个唯一的标识符是追踪后续处理步骤的常用技术,但它也有助于参与者以标准方式向对方请求数据。例如,通过使用事务ID,送货服务可以要求库存服务在哪里提取产品,如果订单已付款,请与支付服务进行双重检查。
在命令Command中添加回复地址 可以考虑像在消息中发送回复地址,而不是让参与者回复固定地址,这样您可以让参与者回复多个协调人。
幂等操作 如果您使用队列进行服务之间的通信(如SQS,Kafka,RabbitMQ等),我个人建议您将您的操作设置为幂等。这些队列中的大多数可能会传递相同的消息两次。(Kafak 0.10以后已经支持正好一次消息传递,消除了重复消息传递)
它也可能会增加服务的容错能力。通常,客户端中的错误可能会触发/重放不需要的消息,并与数据库混淆。
避免同步通信 随着事务的进行,不要忘记在消息中添加每个要执行的操作所需的所有数据。整个目标是避免服务之间再进行同步调用,以请求更多的数据。它将使您的服务能够在其他服务脱机时执行其本地事务。很多人错误地使用消息系统,先使用消息系统发送一个提醒通知,然后再让消息接受者通过服务接口过来取数据,这等同于没有使用消息系统,因为同步操作会堵塞,而消息系统是非堵塞的,大数据读取时同步经常会堵塞,这是无法通过事前评估数据量大小来主观以为这么小数据量不会造成堵塞的。