分布式事务是指事务的参与者、支持事务的提供者、资源提供者者以及事务管理器分别部署于不同的分布式系统的不同节点上, 并且用于在分布式系统中保证不同节点之间的数据一致性。分布式事务的实现方式有很多种,比较具有代表性的是由Oracle Tuxedo系统提出的XA分布式事务协议。
为什么要用分布式事务?因为我们的系统需要让分布式数据节点保证数据的一致性
可以去考虑下这样一个场景,如果现在有一个电商平台,包含的功能有订单,支付,库存。当去电商平台下一笔订单时,下单成功后电商平台会跳转到支付平台,支付成功后会更新库存的数据,然后电商平台就可以给我们发货了。
假设在支付的时候,电商平台向银行平台发起扣款,如果银行扣款成功了,但是给电商平台返回结果的时候网络出现了问题,没能返回正确的支付结果信息。那电商平台会认为支付失败,即不会给客户发货了。但实际上客户的钱已经被减掉了。
针对上述情况,我们希望能把电商平台和银行这一整个支付流程能够放到一个事务里面。
这里就出现了两个问题:
1.作为电商平台,银行的系统是不由我控制的,我怎样才可以把它的系统和我的代码放到一个事物里面呢?
2.目前的事务都是基于单个数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务,如何能做到多数据一致的事务呢?
基于这个情况,分布式事务理论就出现了,着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立的数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现支持跨数据库的事务支持,这也就是大家常说的“分布式事务”。
两阶段提交(2PC)
两阶段提交2pc就是使用XA协议的原理,我们可以从这个图的流程来很容易的看出中间的一些比如commit和abort的细节。
两阶段提交这种解决方案属于牺牲了一部分可用性来换取的一致性。两阶段提交是一个理论,目前没有对应落地方案
另外说一句,TransactionScop 默认不能用于异步方法之间事务一致,因为事务上下文是存储于当前线程中的,所以如果是在异步方法,需要显式的传递事务上下文。
补偿事务(TCC)
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
举个例子,假入 鲍勃 要向 史密斯 转账,思路大概是:
我们有一个本地方法,里面依次调用
1、首先在 Try 阶段,要先调用远程接口把 史密斯 和 鲍勃 的钱给冻结起来。
2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
三阶段提交(3PC)
三阶段提交又称3PC,其在2PC提交的基础上增加了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题,提高系统可用性。
但是性能问题和不一致问题仍然没有得到解决。下面我们还是一起看下三阶段流程的是什么样的?
第一阶段:CanCommit阶段
这个阶段类似于2PC中的第二个阶段中的Ready阶段,是一种事务询问操作,事务的协调者向所有参与者询问“你们是否可以完成本次事务?”,如果参与者节点认为自身可以完成事务就返回“YES”,否则“NO”。而在实际的场景中参与者节点会对自身逻辑进行事务尝试,其实说白了就是检查下自身状态的健康性,看有没有能力进行事务操作。
第二阶段:PreCommit阶段
在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。此时分布式事务协调者会向所有的参与者节点发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
否则,如果阶段一中有任何一个参与者节点返回的结果是No响应,或者协调者在等待参与者节点反馈的过程中超时(2PC中只有协调者可以超时,参与者没有超时机制)。整个分布式事务就会中断,协调者就会向所有的参与者发送“abort”请求。
第三阶段:DoCommit阶段
在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”->“提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。
3PC : CanCommit -> PreCommit -> Commit
TCC : Try -> Commit -> Cancel
其中3pc是不能取消掉,pre提交之后,如果没有收到commit请求,也会进行提交。而TCC是可以取消的,这点来看tcc比3pc有优势
本地消息表(异步确保)
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
基本思路就是:
消息生产方:需要额外维护一个本地消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方 : 需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。在 .NET中 有现成的解决方案。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
MQ 事务消息
部分MQ是支持事务消息的,比如RocketMQ,RabbitMq,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的, Kafka ,pulsar都不支持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会返回消息的地址。 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。