在服务端应用程序中,我们往往会通过事务处理来保证数据一致性(Data Consistency),例如:当用户从库存中取走了一定数量的物品,这些物品会体现在用户的提货单上,与此同时,库存中物品的数量也应该减少。如果在这个过程中无法保证数据的一致性,那么就有可能出现用户没有成功取走物品,而库存中的物品数量却减少了;或者用户成功取走了物品,而库存中的物品数量却没有变化。前者导致物品总量比实际少了一些,而后者又导致物品总量比实际多了,这样的问题就是数据一致性问题。为了保证应用程序不会出现这类问题,我们通常会使用数据库事务。单说数据库事务就有很多相关的知识,但这都不是这里打算深入讨论的内容。我们会着重介绍一下微服务架构下跨服务事务的实现以及与之相关的Saga体系结构模式,不过在此之前,还是有必要回顾一下事务处理的一些解决方案。
本地事务
本地事务通常会在同一个资源管理器(Resource Manager)上完成,最为常见的例子就是在同一个数据库上操作多张数据表,在这些操作完成之后,数据表的变更同时成功或者同时失败。例如,下面的C#代码会在一个本地事务中同时更新两张数据表:
|
代码中SqlTransaction能够保证,对于Users表和Inventory表的更新要么同时成功,要么同时失败。这个本地事务是在SQL Server的资源管理器上执行,因此,本地事务的效率是比较高的。
在微服务架构中,我们往往会选择Database-per-Service的设计,这样做的好处是能够获得比较好的数据隔离性,而且不同的服务可以根据本身的特点选择不同的数据存储方案。因此,就单个服务而言,实现本地事务是比较容易的事情,它能够很好地满足服务本身的业务需求,也能够很好地保证数据的一致性。然而很明显,本地事务无法保证跨服务的数据一致性。
分布式事务
分布式事务往往会横跨多个资源管理器(Resource Manager,RM),并由分布式事务协调器(Distributed Transaction Coordinator,DTC)负责事务协调。分布式事务通常基于两段提交协议(Two-phase Commit, 2PC)实现:事务提交分两个阶段进行,在第一阶段(准备阶段)中,2PC协议需要确保DTC已经获得了所有来自RM的提交反馈信息,对于每个RM,DTC都需要明确知道它是否可以成功完成其本地事务,或者无法完成。就RM而言,在这一阶段会尝试提交其本地事务,如果能够成功提交,则向DTC报告“可以提交”的状态,否则报告“无法提交”的状态。DTC在收集了所有参与者RM的状态后,如果全部为“可以提交”,则启动第二阶段(提交阶段),通知所有RM完成正式提交;但只要有一个RM报告“无法提交”,则DTC会通知其它的RM取消提交操作。
一个成功的2PC提交的过程大致可以用下面的顺序图来表示:
此外,三段提交协议(Three-phase commit, 3PC)也是实现分布式事务的一种模式,与2PC相比,3PC主要是为了解决DTC或者RM出现故障的情形,它将2PC中的第一阶段(准备阶段)进行了细分,将RM分为了Awaiting和pre-commit两种状态。总的来说,3PC和2PC过程大致相同,可以参考这篇文章进一步了解,在此就不多说了。
从 2.0版本开始,.NET Framework引入了一个非常方便的类:TransactionScope,这个类能够辅助完成分布式事务处理,比如,下面的伪代码能够实现跨SQL Server服务器的事务处理: