分布式事务是在分布式系统中解决的最困难的问题之一,没有一种适用于所有情况的解决方案。然而,有几种模式和方法可以使其更容易。
分布式事务可以使用两阶段提交协议来实现。由于本文不涉及两阶段提交协议,让我们简要讨论一下它的机制,并只考虑成功的情况。
在第一阶段中,协调器要求所有事务参与者准备事务。只有在收到所有参与者的确认后,它才开始第二阶段并提交事务。两阶段提交有几个显著的缺点:
?它很慢。?协调器是单点故障。?它是一种阻塞式协议。
这就是为什么在现代分布式系统中很少使用两阶段提交的原因。相反,有一种另外的方式可以实现分布式事务(或替代它),那就是“Saga模式”。Saga是一系列步骤,达到了与一个事务相同的目标。每个步骤是在本地服务接收到相应消息或事件后触发的本地事务。在失败的情况下,会有补偿步骤应用于将所有参与者恢复到初始状态。Saga的一个重要优点是它是非阻塞的。即使整个事务可能需要相当长的时间(甚至可能比两阶段提交更长),所有参与者也可以使用异步消息进行通信,而无需等待彼此。
不幸的是,正如我之前所说,Saga不是分布式事务,而是它的替代品。这意味着Saga并不满足事务所应用的一些要求(我将省略对ACID的解释)。隔离性(Isolation)的缺失是主要的问题,这意味着一个Saga所做的所有中间更改对其他Saga(通常甚至对最终用户)都是可见的,尽管它们在Saga失败时可能被回滚。同时,几个Saga可以在彼此毫不知情的情况下操作同一资源。这种行为的后果可能对整个系统的数据完整性造成不可预测和甚至危险的影响。
那么,“预定模式”是什么呢?
让我们为事务的好处编号。第一个是前面提到的隔离性(Isolation)。第二个是能够锁定资源并阻止其他事务对其进行更改。但是,这主要是因为本地事务很短暂。在Saga中,这种方式不起作用,因为不可能在这么长的时间内对行进行数据库锁定。
幸运的是,有一种替代方案,称为“预定模式”。其思想是在Saga完成成功或失败后,对资源或其部分进行应用级别的锁定。一般而言,步骤的顺序如下:
1.本地服务接收到一条消息,触发预定的创建。2.如果接收到类似的消息,则检查现有的预定。3.在收到确认消息后,服务释放预定并对相关实体应用必要的更改。4.如果没有收到确认消息,则预定根据超时或其他内部条件过期。
预定模块应具备以下功能:
1.预定(Reserve):能够为某个资源创建预定。它还可以设置过期条件,例如有效时间。如果已经创建了相同的预定,它将返回错误消息。2.验证(Validate):能够检查创建的预定及其有效性。通常需要最终化过程。3.过期(Expire):能够将预定标记为过期。可以基于某些内部条件或接收到相应消息时发生。4.确认(Confirm):此功能完成过程并更改相关实体的状态。确认后,预定不能被标记为过期或以其他任何方式更改。
预定也可以是显式的或隐式的。在显式方法中,服务接收到命令以显式预定资源、使预定失效或确认预定。在隐式方法中,服务根据接收到的事件自行决定如何创建和管理预定。通常,显式预定适用于基于编排的Saga,而隐式预定则适用于基于协同的Saga,但它们也可以相反。这里的关键是确定预定逻辑应该放在本地服务(隐式)还是外部服务或编排器(显式)中。
预定模式类似于应用级别的锁定,因此您可能会遇到与数据库锁定相同的问题,以及一些特定的与逻辑相关的问题:
?由于服务可能同时接收多个针对同一资源的并发请求,仍然可能存在竞态条件,因此应在本地使用数据库锁定以避免这种情况。?预定和过期
逻辑应该经过垃圾邮件和欺诈案例的验证。例如,如果在将产品添加到购物车时对其进行预定并且在删除或购买产品之前不将其过期,那么有人可能会预定您商店中的所有产品并且永远不支付。
?可能会发生死锁:如果Saga在处理预定时等待预定的资源,您可能会得到几个无限相互锁定的Saga。
以下是此类死锁的一个示例:
Saga预定死锁
假设我们有一个旅行旅游应用程序。它创建一个订单,其中包括交通和住宿。有两个不同的服务负责一个预定:Tickets服务预订机票;Rooms服务预订酒店客房。Payments服务等待Rooms和Tickets都预订完成以完成订单。当Tickets为一个订单预订了最后可用的机票,并且Rooms为另一个订单预订了最后一个客房时,死锁发生。在这种情况下,这些订单都无法完成。
示例
让我们看一个具体的示例。假设我们有一个允许用户使用礼品卡支付商品的应用程序。礼品卡可以用来支付订单的一部分金额(如果金额小于订单金额),但也可以多次使用同一礼品卡支付多个订单(如果价值超过订单价值)。
礼品卡应用程序服务
该系统有3个服务:Payments(支付)、Gift Cards(礼品卡)和Orders(订单),在处理支付时应满足以下条件:
?使用一个礼品卡支付的订单总金额不能超过礼品卡本身的价值。?如果支付失败,礼品卡可以用于支付另一个订单。
此外,还可能有多个并发请求尝试使用同一礼品卡进行支付。这就是为什么在此处应用预定模式的原因。如果没有在礼品卡中预定订单金额,它可能会被多次使用。另一方面,我们不能立即将礼品卡标记为“已使用”或减少其金额,因为整个Saga可能会在最后失败。
解决这个问题的基于协同的方法如下:
预定模式消息流
?订单服务启动一个Saga并发布OrderCreated事件。?Payments服务接收到OrderCreated事件,如果支付需要使用礼品卡,就会创建GiftCardPaymentCreated事件。?Gift Cards服务接收到GiftCardPaymentCreated事件,并尝试在礼品卡上创建预定。如果礼品卡已过期或余额不足,它将发送GiftCardPaymentRejected事件。?如果Gift Cards服务成功创建了预定,它将发送GiftCardPaymentReserved事件。?Payments服务接收到GiftCardPaymentReserved事件,继续处理支付。?如果支付成功,Payments服务将发送PaymentSucceeded事件。?如果Gift Cards服务在某个步骤失败或超时,它将发送GiftCardPaymentExpired事件。?Payments服务接收到GiftCardPaymentExpired事件,并尝试使用其他支付方式完成支付。
预定模式在这里的关键是,一旦预定被创建,其他并发请求将无法使用相同的礼品卡进行支付。预定有效时间的设置取决于Saga的需求,可以是一段固定的时间或直到Saga完成或失败。
总结起来,预定模式是一种用于处理分布式事务的替代方法。它在分布式系统中使用Saga模式和应用级别的锁定来确保资源的一致性和可靠性。尽管它并不是完美的解决方案,并且仍然存在一些挑战和潜在的问题,但预定模式提供了一种有效的方法来处理分布式事务。
原创文章,作者:小技术君,如若转载,请注明出处:https://www.sudun.com/ask/34025.html