当我们向微服务架构迁移时,如何处理好分布式事务是必须考虑的问题。这篇文章介绍了分布式事务处理的两种方案,可以结合实际采用合适的解决方案。原文:Handling Distributed Transactions in the Microservice world[1]
如今每个人(包括我)都在思考、构建微服务,分布式系统是微服务的核心原则和一切实现的上下文。
什么是分布式事务?
跨越网络上多个物理系统或计算机的事务被简单的称为分布式事务。在微服务世界中,事务被分割到多个服务中,需要按顺序调用这些服务以完成整个事务。
下面是一个单体电子商务系统使用事务的例子:
图 1: 单体中的事务
在上面的系统中,如果用户向平台发送 Checkout 请求,平台将创建一个本地数据库事务,该事务操作多个数据库表,以处理订单并从库存中保留商品。如果有任何步骤失败,事务(包括订单和保留的商品)可以回滚。这被称为 ACID(原子性 Atomicity、一致性 Consistency、隔离性 Isolation、持久性 Durability),由数据库系统保证。
下面是电子商务系统分解为微服务的情况:
图 2: 微服务中的事务
当我们解耦这个系统时,创建了微服务OrderMicroservice
和InventoryMicroservice
,各自有独立的数据库。当用户发起 Checkout 请求时,这两个微服务都将被调用从而将更改应用到各自的数据库中。因为事务是通过多个系统跨多个数据库的,所以现在这是一个分布式事务。
微服务中的分布式事务有什么问题?
随着微服务体系架构的出现,事务可以跨越多个微服务,从而跨越数据库,因此我们现在无法利用数据库的 ACID 特性,从而面临以下关键问题:
如何保持事务的原子性?
原子性意味着事务要么完成所有步骤,要么没有完成任何步骤。在上面的例子中,如果InventoryMicroservice
方法中的“保留商品”失败,如何回滚OrderMicroservice
应用的“处理订单”?
如何处理并发请求?
如果某个微服务的对象被持久化到数据库中,同时有另一个请求读取相同的对象。服务应该返回旧数据还是新数据?在上面的例子中,一旦OrderMicroservice
已经完成,那么InventoryMicroservice
在执行更新的过程时,客户下单的请求中应该包括当前的订单吗?
如今,系统应该为失败而设计,其中主要的问题就是处理分布式事务。下面引用 Pat Helland 的话:
一般来说,应用程序开发人员不会简单的就能实现支持分布式事务的大型可伸缩应用系统。—— Pat Helland
可能的解决方案
在设计和构建基于微服务的应用时,上述两个问题非常关键。为了解决这些问题,下面列举几种方法:
- 两阶段提交(Two-Phase Commit)
- 最终一致性和补偿(Eventual Consistency and Compensation )/ SAGA
1. 两阶段提交
顾名思义,这种处理事务的方式有两个阶段,准备阶段和提交阶段,其中起到重要作用的是事务协调器(Transaction Coordinator),负责维护事务的生命周期。
工作方式:
在准备阶段,所有涉及到的微服务都准备提交,并通知协调器已经准备好完成事务。然后在提交阶段,事务协调器向所有微服务发出提交或回滚命令。
以电子商务系统为例:
图 3: 在微服务上成功的两阶段提交
在上面的示例中(图 3),当用户发送 Checkout 请求时,TransactionCoordinator
将发起一个带有所有上下文信息的全局事务。首先,向OrderMicroservice
发送 prepare 命令创建订单。然后,向InventoryMicroservice
发送 prepare 命令保留商品。当两个服务都可以执行更改时,它们将锁定对象,不再接受其他更改,并通知TransactionCoordinator
。一旦TransactionCoordinator
确认所有微服务都已准备好应用更改,就会通过请求事务 commit 来要求这些微服务持久化所作的更改,然后所有对象才能被解锁。
图 4: 在微服务上失败的两阶段提交
在失败的场景中(图 4)——如果在任何时候有某个微服务没有做好准备,TransactionCoordinator
将中止事务并发起回滚流程。图中由于某种原因,OrderMicroservice
未能创建订单,但是InventoryMicroservice
已经回复说它准备创建订单。TransactionCoordinator
将请求InventoryMicroservice
中止创建订单,并回滚所做的任何更改、解锁数据库对象。
优点
- 该方法保证事务是原子的。交易结束时,要么所有微服务都成功,要么所有微服务都没有改变。
- 其次,允许读写分离,在事务协调器提交更改之前,对象上的更改是不可见的。
- 这种方法通过同步调用通知客户端成功或失败。
缺点
- 没什么事情是完美的,两阶段提交与单个微服务的处理时间比起来慢很多,并且高度依赖于事务协调器,在高负载期间,事务协调器确实会降低系统的速度。
- 另一个主要缺点是数据库行锁定,该锁可能成为性能瓶颈,并且可能出现两个事务相互锁定造成的死锁。
2. 最终一致性和补偿/SAGA
最终一致性的最佳定义之一是 microservices.io[2]描述的:每个服务在更新数据时发布一个事件。其他服务订阅事件,当接收到事件时,更新其数据。
在这种方法中,分布式事务由相关微服务上的异步本地事务来完成,微服务通过事件总线相互通信。
工作方式:
再以电子商务系统为例:
图 5: 最终的一致性/SAGA,成功的场景
在上面的例子中(图 5),客户端请求系统处理订单。在处理过程中,Choreographer
发出一个 Create Order 事件,表示开始一个事务。OrderMicroservice
监听到这个事件并创建一个订单,如果成功,发出一个 Order Created 事件。Choreographer
侦听此事件,并通过发出 Reserve items 事件继续保留商品。InventoryMicroservice
侦听此事件并保留商品,如果成功,发出 Items Reserved 事件。在这个例子中,这意味着事务的结束。
微服务之间所有基于事件的通信都是通过事件总线进行的,并由另一个系统编排以解决复杂性问题。
图 6: 最终的一致性/SAGA,失败场景
如果由于任何原因InventoryMicroservice
未能保留商品(图 6),它会发出 Failed to Reserve Items 事件。Choreographer
侦听此事件,并通过发出 Delete Order 事件启动补偿事务。OrderMicroservice
侦听此事件并删除所创建的订单。
优点
这种方法的一大优点是每个微服务只关注自己的原子事务。如果某个服务花费了更长的时间,其他微服务不会被阻塞,这也意味着不需要数据库锁。由于其基于异步事件的解决方案,这种方法可以使系统在高负载下具有高度的可伸缩性。
缺点
该方法的主要缺点是没有读取隔离。这意味着在上面的示例中,客户端可以看到已创建的订单,但在下一秒中,由于补偿事务,订单会被删除。此外,当微服务的数量增加时,调试和维护就变得更加困难。
结论
首先尽量避免分布式事务,如果正在构建新应用,那么就从单体开始,如 Martin Fowler 在 MonolithFirst[3]中所描述的那样:
更常见的方法是从单体开始,逐渐剥离边缘的微服务。这种方法可以在微服务体系架构的核心留下一个巨大的单体,大多数新的开发都发生在微服务中,而这个单体相对来说变化不大。— Martin Fowler
当一个事件需要在两个地方更新数据时,与两阶段提交相比,最终一致性/SAGA 方案是处理分布式事务的更好的方式,主要原因是两阶段提交在分布式环境中不能伸缩。不过最终一致性方案引入了新问题,例如如何以原子方式更新数据库和发出事件,因此采用这种方案需要开发和测试团队改变思维方式。
References:
[1] Handling Distributed Transactions in the Microservice world: https://medium.com/swlh/handling-transactions-in-the-microservice-world-c77b275813e0
[2] Event Driven Architecture: https://microservices.io/patterns/data/event-driven-architecture.html[3] MonolithFirst: https://martinfowler.com/bliki/MonolithFirst.html