单库不涉及网络交互,所以实现多表间事务比较简单。这种交易称为本地交易。但是,当单个数据库性能达到瓶颈时,需要对数据库进行拆分,就会有跨数据库(数据库实例)的事务需求;随着企业应用规模越来越大,企业将进一步进行服务转型以满足业务增长的需求;现在的微服务架构越来越流行,跨服务的事务场景也会越来越多。这些都是分布式事务。分布式事务是指事务的发起者、参与者、数据资源服务器和事务管理者分别位于分布式系统的不同节点上。总结一下,分布式事务分为三种场景:跨库分布式事务跨服务分布式事务混合分布式事务本文将介绍分布式事务常见的解决方案:2PC3PCTCCSagatransactionsbasedonlocalmessagetablemechanismBasedontransactionmessagemechanism尽力而为通知机制常见解决方案分布式事务由多个本地事务组成。分布式事务跨越多个设备,并在它们之间经历复杂的网络。可想而知,严格交易之路艰难而漫长。2PC两阶段提交(Two-phaseCommit,简称2PC),是指一种算法(Algorithm),旨在让基于分布式系统架构的所有节点在提交事务时保持一致性。整体分为两个阶段,如图。2PC的优点和缺点2PC的优点是可以使用参与者(RM)的功能来提交和回滚本地事务,并且对业务逻辑零侵入(相对于TCC方案)。但是2PC也有三大缺点:同步阻塞、单点故障和数据不一致。同步阻塞因为参与者(RM)在执行操作时是同步阻塞的,所以在2PC过程中其他节点访问锁定资源时必须处于阻塞状态。单点故障协调器(TM)是单点的,一旦协调器出现故障,参与者将一直处于阻塞状态。如果一个热点资源被阻塞,可能会引起整个系统的雪崩。数据不一致在第二阶段,由于网络原因,部分参与者(RM)没有收到协调者(TM)的信息,或者部分参与者在执行commit/rollback操作时出现异常。这样就会导致数据不一致的问题。2PC优化Percolator是Google上一代分布式事务解决方案,构建于BigTable之上,用于Google内部网页索引更新业务,原论文参见参考文献4。TiDB的事务模型沿用了Percolator的事务模型,然后讲解分布式数据库的相关博客。3PC3PC与2PC相比,增加了三阶段模式和超时机制。超时机制:第三阶段,当参与者长时间没有得到协调器的响应时,默认情况下,参与者会自动提交超时的事务(即使协调器可能会发送回滚命令,这会导致数据不一致)。解决2PC同步阻塞的情况。同时,3PC增加的第一阶段查询通知,降低了2PC出现数据不一致的概率。但是2PC中的单点故障问题是3PC无法解决的。3PC的三个阶段如图:Phase1,CanCommitPhase2,PreCommitPhase3,DoCommit总结一下,2PC和3PC都是协议,是一种指导思想,和真正的实现方案还是有区别的的项目。但是2PC和3PC在大型分布式系统中很少使用,因为在事务处理过程中,协调器需要同时连接多个数据库(RM)。通常,微服务连接到各自域中的数据库。微服务如果要修改另一个域的数据,必须通过RPC接口实现,无法访问多个数据源。如果链接多个数据源的协调器过多,势必会增加服务治理的难度,并可能导致数据混乱。TCC是2PC还是3PC,取决于数据库的事务提交和回滚。但有时有些业务不仅仅涉及到数据库,比如发送短信、上传图片等业务层逻辑。因此,事务的提交和回滚必须升级到业务层面,而不是数据库层面,而TCC是业务层面的两阶段提交。TCC(Try、Commit、Cancel)是一种补偿性事务。该模型要求应用的每个服务提供三个接口:try、confirm、cancel。核心思想是先评估资源的预留情况。如果交易可以提交,则预留资源确认完成;如果事务需要回滚,则释放保留的资源。TCC的本质是应用层面的2PC,也分为两个阶段,如下图所示:比如扣款服务使用TCC,需要写Try方法扣款;您还需要一个Confirm方法来执行真正的扣除;最后,需要提供Cancel方法来回滚扣除操作。可以看出,原来的一个方法需要扩展成三个方法,所以TCC对业务的侵入很大。虽然对业务有入侵,但TCC并不阻塞资源。每个方法直接提交交易。如果有错误,在业务层面通过Cancel进行补偿,所以TCC是一个补偿事务。对于2PC中单点失败或者超时的问题,TCC的解决方案是不断重试:不断重试没有收到响应的Confirm/Cancel接口,直到成功,如果重试策略失败,则通过记录和报警进行人工干预。但是这种重试机制造成了TCC的幂等问题和空回滚问题。TCC中需要注意的问题幂等性问题由于重调机制,Try、Confirm、Cancel这三个方法需要幂等地实现,避免重复执行导致的错误。空回滚问题Coordinator(TM)由于网络问题没有成功接收到参与者(RM)的Try接口响应,此时Coordinator(TM)会发出Cancel命令。那么Cancel接口需要在不执行Try的情况下能够正常取消。挂起问题事务协调器调用TCC服务第一阶段Try操作时,可能会因为网络拥塞而超时。此时事务协调器会触发两阶段回滚,调用TCC服务的Cancel操作。Cancel调用不会超时;之后,网络拥塞的第一阶段Try数据包被TCC服务接收,第二阶段Cancel请求在第一阶段Try请求之前执行。延迟Try后,TCC服务将一直不会再收到第二阶段的Confirm或Cancel,导致TCC服务被挂起。所以在实现TCC服务的时候,既要允许空回滚,又要在执行完空回滚后拒绝Try请求,避免挂掉。上面解释的补偿TCC是一个通用的TCC,它需要控制所有与分布式事务相关的业务。但是有的时候(比如调用别的公司的接口),通用的TCC就不行了。所以就有了compensatedTCC,可以理解为没有Try的TCC形式。由于没有提供Try接口,可以认为是Saga机制的另一种形式。比如你需要坐飞机中转,你中转的飞机是不同的航空公司,比如从A飞B,再从B飞C。只有A-B和B-C都买票才有意义。这时候直接调用航空公司的购票操作。当两家航空公司都购买成功时,它就直接成功了。如果某家公司购买失败,则需要调用取消预订接口。相当于直接执行TCC的第二阶段,需要重点关注回滚操作。如果回滚失败,必须有告警记录和人工干预。综上所述,TCC事务实现了从资源层到业务层的分布式事务,允许业务灵活选择资源的加锁粒度,在全局事务执行过程中不会一直持有锁,因此系统的吞吐量比2PC模式高很多。由于TCC交易带来的工程复杂性、网络延迟、服务治理难度,除非是与支付交易相关的核心业务场景(需要高一致性),否则其他业务场景不应使用TCC交易。SagatransactionSagatransaction也是补偿交易。与补偿式TCC一样,没有Try阶段,而是将分布式事务看成是一组本地事务组成的事务链。一个Saga交易的基本协议如下:每个Saga交易由一系列幂等有序的子交易(sub-transactions,充当参与者)Ti组成。每个Ti都有一个对应的幂等补偿动作Ci,用于撤销Ti造成的结果。如果事务链中包含业务时序逻辑,则必须合理安排事务链的顺序(以项目利益优先,有错可人工补的原则,比如先扣款再发货;先退货再退款)由于Saga模型中没有Prepare阶段,所以无法保证事务之间的隔离。当多个Saga事务操作同一个资源时,会出现更新丢失、脏数据读取等问题。这时候就需要在业务层控制并发,比如:在应用层加锁,或者在应用层预冻结资源。以下是订购流程的示例。整个操作包括:扣除库存(inventoryservice)、创建订单(orderservice)、支付(paymentservice)等。事务正常执行完成T1,T2,T3,...,Tn,例如:扣除库存(T1),创建订单(T2),支付(T3),按顺序,但是支付服务报错报错,此时Saga有两种策略可以使用。Saga事务的恢复策略Saga定义了两种恢复策略。前向恢复(forwardrecovery)适用于必须成功的场景。发生故障并执行重试。执行顺序类似这样:T1,T2,...,Tj(失败),Tj(重试),..,Tn,其中j为发生错误的子事务。显然,没有必要为远期恢复提供补偿交易。如果业务中的子事务会(最终)成功,或者补偿事务难以定义或不可能,前向恢复更适合需求。过于激进的重试策略(如间隔时间过短或重试次数过多)都会对下游业务造成不利影响,因此需要采用合理的重试机制。后向恢复(backwardrecovery)该方法的作用是撤销之前所有成功的子事务,从而撤销整个Saga的执行结果。下面解释的例子都是后向恢复策略。Saga事务协调模式Saga执行事务的顺序称为Saga的协调逻辑。这种协调逻辑有两种模式,Orchestration和EventChoreography如下:Orchestration:Saga提供了一个控制类来方便协调子事务。事务执行的命令从控制类发起,按照逻辑顺序请求Saga的子事务。控制类收到子事务的反馈后,发起对其他子事务的调用。Saga的所有子事务都是围绕这个控制类进行通信和协调的。以电商订单为例:交易发起方的主要业务逻辑请求控制类发起订单交易。控制类向库存服务请求扣减库存,库存服务回复处理结果。控件类请求订单服务创建订单,订单服务回复创建结果。控制类向支付服务请求支付,支付服务回复处理结果。主要业务逻辑接收并处理交易结果回复。控制类必须事先知道执行整个订单交易所需的流程。它负责通过向每个子事务发送命令来协调分布式回滚,以在任何失败时撤消先前的操作。当一切都基于控件类来协调时,回滚就容易多了,因为控件类默认执行正向过程,回滚时只需要执行反向过程即可。EventChoreography:子事务之间的调用、分配、决策和排序都是通过交换事件来进行的。是一种去中心化模式,子事务通过消息机制进行通信,通过监听器监听其他子事务发送的消息,从而进行后续的逻辑处理。由于没有中间协调点,相互间的协调都是通过子事务来进行的。以电商订单为例:交易发起方的主要业务逻辑发布开始订单事件。库存服务监听开始订单事件,扣减库存,发布扣减库存事件。订单服务监听库存扣除事件并创建订单。并发布订单已创建事件支付服务监听订单已创建事件,支付,并发布订单已支付事件主要业务逻辑监听并处理订单已支付事件。该实现方式与基于协调的Saga相比的优势在于:服务之间的关系简单,避免了子事务之间的循环依赖,因为Saga控制类会调用Saga子事务,但是子事务不会调用控制类。程序开发简单,子交易只需要完成自己的任务即可,无需考虑处理消息的方式,降低了子交易接入的复杂度。易于维护和扩展。如果事务需要增加新的步骤,只需要修改控制类,保持事务复杂度线性,回滚更容易管理。易于测试,测试工作集中在控制类,其他服务可以单独测试。基于协调的Saga的缺点如下:存在将过多逻辑集中在控制类中的风险,难以维护。控制类存在单点故障风险。基于事件编排的Saga的优势如下:避免控制单点故障的风险。子交易通过订阅时间进行通信,组合起来会更加灵活。基于事件编排的Saga缺点如下:存在服务间循环依赖的风险。当涉及的步骤很多时,服务之间的关系就很混乱。没有完善的文档支持,理解整个交易的执行过程只能通过阅读代码来完成。增加了开发者理解和维护代码的难度。基于本地消息表机制,本地消息表机制会在数据库中存储一个本地事务消息表,在执行本地事务操作的同时,将操作状态插入到本地事务消息表中。消息插入成功后,调用其他服务。如果调用成功,则修改本地消息的状态;如果调用失败,它将继续重试。下游接口需要保证幂等性。本地消息表机制是一种尽力而为的通知思想。这里以支付服务和记账服务为例,介绍本地消息表的解决方案。大致流程:用户在支付服务中完成支付订单支付后,会调用记账服务的接口生成原始记账凭证存入数据库。整体流程如图所示:完整流程:在支付库中添加一个消息表,用于记录支付消息,即在用户支付成功后,向该消息表中插入一条支付成功的消息,状态为“发送”。这里必须保证本地事务的强一致性(支付逻辑和消息插入消息表形成一个强一致性事务,要么同时成功,要么同时失败)。完成第1步的逻辑后,向mq的PAY_QUEUE队列post一个支付消息。该支付消息内容与支付库消息表中存储的消息内容一致。记账服务听到这个消息,记账服务处理消费逻辑开始生成记账凭证。记账凭证生成后,向mq反方向向ACC_QUEUE队列传递消费成功的消息。支付服务监听计费服务消费成功的消息,将本地消息表的消息状态更改为“已发送”。消息回收系统每隔一段时间从本地消息表中取出状态为“发送中”的消息,然后重新投递到mq的PAY_QUEUE队列中。如果报文恢复系统再次投递相同的报文并达到一定阈值,则会记录告警并通知人工处理。总结本地消息表机制的优点是构建成本比较低,但也有两个缺点:本地消息表与业务耦合,难以实现通用性,无法单独运维.本地消息表基于数据库,高并发下存在性能瓶颈。基于事务消息机制,无论是2PC&3PC还是TCC,本地消息事务,基本上都遵循XA协议的思想。也就是说,这些方案本质上都是事务协调器,协调各个事务参与者的本地事务的进程,使所有本地事务一起提交或回滚,最终达到全局执行的特性。在协调过程中,协调者需要收集每个本地事务的当前状态,并根据这些状态下达下一阶段的操作指令。但是由于这些全局事务方案操作繁琐,时间跨度大,或者在全局事务过程中对相关资源进行独占锁定,导致整个分布式系统的全局事务并发度不会太高。难以满足电子商务等高并发场景的事务吞吐量需求,因此互联网服务商探索了很多与XA协议背道而驰的分布式事务解决方案。其中消息中间件实现的最终一致的全局事务(transactionmessagetransaction)是一个经典的方案。基于事务消息机制的普通消息无法解决本地事务执行和消息发送的一致性问题。因为消息发送是一个网络通信的过程,发送消息的过程可能会发送失败或者超时。如果超时,可能发送成功,也可能发送失败。无法确定消息的发送者,所以此时消息的发送者是提交事务还是回滚事务,都可能存在不一致。要解决这个问题,就需要引入事务性消息。事务性消息与普通消息的区别在于,事务性消息发送成功后,处于准备状态,不能被订阅者消费。事务性消息状态变为可消费状态后,下游订阅者可以监听该消息。发送交易消息的过程如下:交易发起方预先发送交易消息。MQ系统收到交易消息后,将消息持久化,消息状态为“待发送”,并向发送方发送ACK消息。如果事务发起方没有收到ACK报文,则取消本地事务的执行;如果它收到ACK消息,它会执行本地事务。本地事务执行完成后,发送给MQ系统根据结果提交或回滚请求。本地事务执行后,发送给MQ的通知消息可能会丢失。因此,支持事务性消息的MQ系统有定时扫描逻辑,扫描出状态仍为“待发送”的消息,并向消息的发送者发起查询,询问事务性消息的最终状态并更新基于结果状态的事务消息。因此,交易发起方需要为MQ系统提供交易消息状态查询接口。MQ系统收到消息通知后,如果提交了请求,则将消息改为“可消费”,供订阅者消费;如果事务被回滚,事务消息将被删除。如果交易消息的状态为“可发送”,MQ系统会将消息推送给下游参与者,如果推送失败,会不断重试。下游参与者收到消息后,执行本地事务。如果本地事务执行成功,会向MQ系统发送ACK消息;如果执行失败或者给MQ发送ACK的消息丢失,MQ系统会继续推送消息。总结基于事务消息机制实现最终一致性,适用于异步更新场景,对数据实时性要求不高的场景。与本地消息表实现方案相比,不需要创建本地消息表,也不需要依赖本地数据库事务,因此该方案更适合高并发场景。RocketMQ可以直接支持生产环境使用基于事务的消息机制。其他消息中间件(如Kafka、RabbitMQ等)需要自行开发封装可靠的消息服务。RocketMQ事务消息解决了本地事务执行和发送消息两个动作,满足事务的约束。Kafka事务消息用于在一个事务中需要发送多条消息时,保证多条消息之间的事务约束,即多条消息要么发送成功,要么发送失败。BestEffort通知机制BestEffort通知机制的本质是引入一个周期性的验证机制来覆盖最终的一致性。对业务的侵入性小,对MQ系统要求低,实现起来也比较简单。适用于对最终一致性的敏感性。比较低级、业务链接较短的场景,比如跨平台、跨企业系统的业务交互。适用场景小明通过联通网上营业厅给手机充值。整个操作流程如下:小明选择充值金额“50元”,支付方式“支付宝”。联通网上营业厅创建充值订单,状态为“支付中”,跳转到支付宝支付页面。支付宝审核确认小明付款后,从小明账户中扣除50元,并向联通账户充值50元。执行完成后,向MQ系统发送消息,消息内容标识支付是否成功,允许消息发送失败。如果消息发送成功,支付宝通知服务会订阅消息,并调用联通接口通知支付结果。如果此时联通服务挂掉,通知失败,将每隔5min、10min、30min、1h、...、24h等重复调用联通接口,直到调用成功或到达在预定的时间窗口上限之后,将不再发出通知。这就是尽力而为通知的含义。如果联通业务恢复正常,收到支付宝通知,请为账户充值(联通充值接口需要保证幂等性)。支付”订单,向支付宝发起请求,验证订单的支付结果。技术选择2PC&3PC2PC&3PC强依赖数据库,可以提供强一致性和强事务性,但是相对来说延迟比较高,更适合传统的单体应用。同一个方法中存在跨库操作。适用于大分布式、高并发、高性能要求的场景。TCCTCC适用于一定且执行时间短、实时性要求高、数据一致性要求高的场景,例如互联网金融公司的三大核心业务:交易、支付、记账。SagatransactionsSagatransactions适用于长期的业务流程,业务流程多,对同一资源的并发操作较少。广泛应用于银行业金融机构,如互联网小额贷款、渠道融合场景、金融机构对接系统(需要对接外部系统)等。基于本地消息表机制和基于交易的消息机制,两者都是适用于交易各方支持运营权等,对一致性的要求不高,业务上可以容忍数据不一致,直到通过自下而上的机制完成最终一致性。交易涉及的参与者和环节较少,业务有对账/核销制度。Best-effort通知机制Best-effort通知机制适用于对最终一致性敏感度不高,服务链路较短的场景。课前知识对文中提到的名词进行补充说明。DTP模型DTP(DistributedTransactionProcess)是一种分布式事务模型。在这个模型中,一共有三个角色:AP:Application,事务的发起者,即业务层。哪些操作属于事务由AP定义。TM:TransactionManager,事务管理器,又称协调器。接收AP的事务请求,管理全局事务,管理事务分支状态,协调RM的处理,通知RM哪些操作属于哪些全局事务和事务分支等,是整个事务调度模型的核心部分。RM:ResourceManager,资源管理器,也称为参与者。一般是数据库,但也可以是其他的资源管理器,比如消息队列(比如JMS数据源)、文件系统等等。DTP模型上定义了三个角色,但在实践中,一个角色可以同时执行两个功能。比如:AP和TM合并,TM不需要单独部署组件。XA协议(XA规范)XA是一种分布式事务处理规范。XA规范了TM与RM之间的通信接口(功能如下图),形成了TM与多个RM之间的双向通信桥梁,从而保证了多数据库资源下的强一致性。目前知名的数据库,如Oracle、DB2、MySQL等,都实现了XA接口,都可以作为RM使用。在整个事务处理过程中,数据始终处于锁定状态,即从准备到提交再回滚,TM一直持有数据库的锁。如果有其他事务要修改数据库中的数据,则必须等待锁释放。
