1分布式事务问题的由来
在传统的单体应用中,我们所有的功能都能在同一个数据库中完成。一致性自然也能由同一个本地事务保证。以一个交易系统为例,如果有个客户消费了,首先我们需要在余额中扣钱,然后增加这个客户的积分。
但是数据库往往会成为系统的瓶颈,因此我们引入微服务后,很可能会对数据库做垂直拆分。
此时如果不使用分布式事务,就会出现数据一致性问题,因为两个服务只能保证自己本地事务的一致性。余额事务成功,可能积分事务会失败。因此,在分布式环境下,我们需要一个分布式的事务管理器来保证不同服务之间的数据一致性。
2常见分布式事务的解决方案
我们常见的分布式事务解决方案有:XA方案、TCC方案、基于可靠消息方案、Saga方案。我们下面来分别介绍每种方案的实现方式。
2.1XA方案
XA是由X/Open组织提出的分布式事务规范,该规范主要定义了全局事务管理器™和局部资源管理器(RM)之间的接口。主流的关系型数据库产品都实现了XA接口。
在了解XA方案之前,我们先了解一些关键的名词:
Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,接收TC的指令,控制本地事务的提交和回滚。
整个XA的流程如下:
XA是二阶段协议的一种实现,步骤如下:
一阶段:
TM保证所有的RM都开启事务,此时会生成一个全局的事务ID,后续每个组件交互时都会根据这个ID来识别不同的事务。每个RM接到TM的请求后开启自己的本地事务,RM执行本地事务无论成功或失败,都会向TM反馈。
二阶段:
TM根据RM们返回的分支事务状况判断,如果全部都是成功,则向所有RM发起提交请求(commit)。如果有任何一个RM的分支事务失败,则发起回滚请求(rollback)。
XA方案中, RM 本质上就是数据库,通过提供支持 XA 的驱动程序来供应用使用。TM则有多种实现方式,可以是独立的服务,也可以是融入服务层的SDK。当然XA方案并不是100%可靠,该方案还需要关注异常情形,如:TM如果挂掉事务该如何处理;二阶段如果出现部分RM回滚成功、部分失败该如何处理;如果部分RM长时间未回复结果等等。
XA方案的优点:
- XA方案是这么多种方案中,少有的可以不对业务产生入侵的方案。
- 该方案简单,不影响研发人员的开发。
XA方案的缺点:
- 该方案重度依赖不同的数据库。如果数据库不支持,或支持得不够好,则不能使用。如:Mysql5.7之前的版本就支持的不够好。
- XA由于是同步事务,资源锁定时间会很长,因此性能往往不会很好,并且无法优化。
- 已经落地的XA方案通常都需要使用重量级的服务器,如:WebLogic。
2.2TCC方案
TCC是“Try”、“Confirm”、“Cancel”的缩写。
Try:尝试执行业务,这个阶段会完成所有业务的检查,预留必须的业务资源。
Confirm:确认执行业务。这个阶段不需要做任何检查,真正执行业务,并且执使用Try阶段预留的资源。Confirm阶段的操作需要满足幂等性。
Cancel:取消执行业务。如果Try阶段有任何一个业务执行失败,这会进入Cancel流程。本阶段主要回滚Try阶段锁定的资源。
TCC的工作流程如下:
TCC的所有操作都在业务层面完成,不依赖于底层的RPC框架,这是优点。但是,也因此会对业务造成很大的入侵性。对现有业务逻辑的改造成本会比较大,全部需要改造为TCC对应的阶段模式、幂等。特别是Cancel阶段,需要业务编写额外的回滚代码。
举一个实际业务场景,以我们文章开头的支付为例:
该系统拥有3种状态:paying、pay-fail、pay-success。
Try阶段:
当用户消费订单提交后,系统开始进行业务逻辑,订单状态被改为paying状态,同时系统会调用扣款服务和积分服务,将账户中的余额扣除或预扣。此处就是预留资源。
Confirm阶段:
系统调用扣款服务和积分服务的Confirm接口,把钱打入收款方账户并增加积分。订单的状态会被改为“pay-success”。
Cancel阶段:
如果在Try阶段任何一个服务失败,则TCC框架会调用Cancel方法。此时会把扣除或预扣的余额加回去,进行数据的回滚,并把订单状态变为“pay-fail”。
同样,TCC方案也不是100%可靠,并且对业务的改造成本很大。但是,近年来也有新的框架,在TCC的基础上实现了业务无感知的方案,我们会在后续文章中介绍。
2.3基于可靠消息的方案
基于可靠消息的分布式事务方案是一种异步柔性事务,使用的是最终一致性,因此不能保证数据的强一致性,但是在吞吐量、可用性和容错方面有更好的表现。
该方案主要是通过一个中间消息管理服务,来保证消息往下一个服务传递,从而让事务继续传递。可靠消息只能保证消息不丢失,但是无法保证消息只传递一次,因此下游服务必须要实现幂等。
流程如下:
- 步骤1-2是在本地事务执行之前,在消息管理服务上注册一个消息。这一步如果失败,整个事务被判定失败。步骤1、2主要是为了容错,当步骤4如果没有执行,消息管理服务会定时捞出步骤1中注册,但是未提交的事务,反查服务A的接口,确认步骤3是成功还是失败,进而确定消息是提交(继续传递)还是取消。
- 步骤3是执行本地事务。这一步如果失败,事务判定失败,继续调用步骤4取消事务。
- 步骤4是提交/取消事务,告诉消息管理服务,消息是否可以继续往下传播。这一步如果失败,会有步骤1-2中的操作进行补偿。
- 步骤5是消息继续往下一个节点传递。如果消息没有被ACK,会定时重发。因此下游节点需要实现幂等。
- 步骤6、7用于确认服务B的事务也执行完毕。如果没有执行,步骤5会重发直到得到反馈。
可靠消息的的吞吐量明显会优于同步事务。但是缺点也十分明显,首先是没有资源的锁定,当消息到达某个服务可能业务无法执行下去。其次,当下游事务失败,需要整体事务回滚时,有可能前面的数据已经被改变,无法回滚。
还是以一开始的业务场景为例,服务A先增加了积分,然后服务B进行扣款,发现余额不足,但是此时积分已经被用户使用,导致无法回滚。
该事务模型适用于流水线式的业务模型,并且各数据之间不会有关联,避免出现脏数据。
2.4Saga方案
Saga也是最终一致性的柔性事务,由1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表的论文演化而来。Saga的实现方式是,把一个长事务,拆分成多个子事务执行,每个子事务都有相应的执行模块和回滚模块。其实现方式如下:
一个长事务会被拆分成一个个的小事务:T1、T2、T3…
当事务正常执行时,会T1、T2、T3…顺序执行下去。当出现异常的时候,则每个子事务分别调用自己的回滚模块,直到第一个子事务也回滚完成,从而达到最终一致性。由于是最终一致,因此也会要求每个服务的幂等。
Saga方案和TCC比较相似,对代码也有很强的入侵性。由于是柔性事务,也会存在和可靠消息方案中的问题,如:同时操作一个资源会有数据一致性问题;同一个事务中如果数据被其他事务修改,会有回滚失败的风险。
面对这些问题,通常有以下的一些解决方案来规避:
- 业务层面的资源冻结,防止被其他事务影响。
- 应用层面使用逻辑锁防止并发。
- 事务流转过程中及时更新资源状态。
- …
目前业界主要由集中式和分布式两种实现方式:
集中式的Saga协调器和调用者在同一个进程,通过传递一个Saga对象来追踪子事务的执行状况,如果成功则调用提交方法,失败则调用对应的补偿方法。业务耦合较高,通常会提供DSL让用户自定义调用链路。
分布式的Saga则通常使用事件驱动的方式,下游服务只需要订阅相关的事件即可。这种方式降低了耦合,但是由于引入了消息管理器,链路很长,对于代码调试会比较痛苦。
3总结
本期我们介绍了常用的四种分布式事务方案,但是方案用于具体项目工程中就需要框架化了。许多方案中都有业务入侵的缺点,但是方案如果框架化后,通过不同的优化方式,是可以实现无业务入侵的。
下一期我们会介绍具体框架的实现方式。
欢迎加入我的知识星球,一起讨论相关技术问题,获取海量中间件、框架原理解析文章