参考链接:
https://juejin.im/post/5b5a0bf9f265da0f6523913b
https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html
https://draveness.me/distributed-transaction-principle/
Table of Contents
3.1 CAP理论(Consisitency、Availability、Parition tolerance )
4.3 补偿事务(TCC)(对应try、confirm、cancel三种操作)
1、为什么会出现分布式事务?
说到事务,首先想到的是他的4个特性ACID。
原有单机中相对可靠的方法调用以及进程间通信方式已经没有办法使用,同时由于网络通信经常是不稳定的,所以服务之间信息的传递会出现障碍。
通过网络请求其他服务的接口时,往往会得到三种结果:正确、失败和超时,无论是成功还是失败,我们都能得到唯一确定的结果,超时代表请求的发起者不能确定接受者是否成功处理了请求,这也是造成诸多问题的诱因。
总结起来,分布式事务产生的原因:
- service存在多个节点
- resource存在多个节点
service存在多个节点:
比如这样一个场景,一个公司之内,用户的资产可能分为好多个部分,比如余额,积分,优惠券等等。在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护,这就相当于是多个service节点,传统的事务性,无法保证在一次的交易过程中,3个系统的数据都能正常的扣减。

resource存在多个节点:
随着数据量的增大,Mysql一般来说装千万级的数据就得进行分库分表,对于一个支付宝的转账业务来说,你给的朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,所以我们依然无法保证他们能同时成功。

2、什么是分布式事务?
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性(不同业务对应的数据库、同一业务分库导致的多个数据库)。
3、分布式事务的基础
3.1 CAP理论(Consisitency、Availability、Parition tolerance )
- C (一致性):对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
- A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。
- P (分区容错性):当出现网络分区后(系统如果不能在时限内达成数据一致性),系统能够继续工作。比如,一个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。
三者不能共存, 在分布式系统中,网络无法100%可靠,分区其实是一个必然现象,如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。
zookeeper : CP , 放弃可用性,追求一致性和分区容错性
Eureka 、BASE : AP , 放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性
3.2 BASE理论
在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:
- Basically Available(基本可用)
- Soft state(软状态)
- Eventually consistent(最终一致性)
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
4、常用的分布式事务的解决方案
前提说明:
思考业务是否真的需要分布式事务,需要使用分布式事务的原因主要是微服务过多,微服务过多就会出现分布式事务。
一种建议就是把需要事务的微服务聚合成一个单机服务,使用数据库的本地事务。因为不论任何一种方案都会增加你系统的复杂度,这样的成本实在是太高了,千万不要因为追求某些设计,而引入不必要的成本和复杂度。
思考:还可以从业务的角度考虑,是否业务可以容忍不同系统间的不一致性,是否可以从代码设计的角度取解决这种问题。
4.1 2PC(Two-Phase Commit)
4.1.1 是什么?
两阶段提交是一种使 分布式系统中 所有节点 在进行 事务提交 时 保持一致性 而设计的一种协议。
在一个分布式系统中,所有的节点虽然都可以知道自己执行操作后的状态,但是无法知道其他节点执行操作的状态,在一个事务跨越多个系统时,就需要引入一个作为协调者的组件来统一掌控全部的节点并指示这些节点是否把操作结果进行真正的提交。这个协调者叫做事务管理器,一个事务管理器管理多个资源。
想要在分布式系统中实现一致性的其他协议,都是在两阶段提交的基础上做的改进。
4.1.2 怎样的工作过程
两阶段提交的执行过程就跟它的名字一样分为两个阶段,投票阶段和提交阶段。
- 投票阶段:协调者(Coordinator)会向事务的参与者(Cohort)询问是否可以执行操作的请求,并等待其他参与者的响应,参与者会执行相对应的事务操作并记录重做和回滚日志,所有执行成功的参与者会向协调者发送
AGREEMENT或者ABORT表示执行操作的结果。 - 提交阶段:当所有的参与者都返回了确定的结果(同意或者终止)时,两阶段提交就进入了提交阶段,协调者会根据投票阶段的返回情况向所有的参与者发送提交或者回滚的指令。
- 当事务的所有参与者都决定提交事务时,协调者会向参与者发送
COMMIT请求;参与者在完成操作并释放资源之后,向协调者返回完成消息,协调者在收到所有参与者的完成消息时会结束整个事务; - 与之相反,当有参与者决定
ABORT当前事务时,协调者会向事务的参与者发送回滚请求;参与者会根据之前执行操作时的回滚日志对操作进行回滚,并向协调者发送完成的消息。
在提交阶段,无论当前事务被提交还是回滚,所有的资源都会被释放并且事务也一定会结束。
操作的资源都成功,事务最终成功提交:

存在操作失败的资源,事务最终回滚:

4.1.3 两阶段提交存在问题:
- 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
- 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
总的来说,XA协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。
自我分析: 协调者宕机、参与者宕机、网络通讯原因、同步阻塞
4.2 3PC(Three-Phase Commit)
4.2.1 是什么?
为了解决两阶段提交在协议的一些问题,三阶段提交引入了超时机制和准备阶段,如果协调者或者参与者在规定的之间内没有接受到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务,准备阶段的引入其实让事务的参与者有了除回滚之外的其他选择。

当参与者向协调者发送 ACK 后,如果长时间没有得到协调者的响应,在默认情况下,参与者会自动将超时的事务进行提交,不会像两阶段提交中被阻塞住。
上述的图片非常清楚地说明了在不同阶段,协调者或者参与者的超时会造成什么样的行为。
第一阶段(投票阶段):如果超时(比如协调者长时间没有响应),导致回滚事务
第二阶段(准备阶段):如果超时(比如协调者长时间没有响应),导致事务提交
第三阶段(最终阶段):
4.3 补偿事务(TCC)(对应try、confirm、cancel三种操作)
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
- Try 阶段主要是对业务系统做检测及资源预留
Confirm 阶段主要是对业务系统做确认提交。默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入 Bob 要向 Smith 转账,思路大概是:
我们有一个本地方法,里面依次调用
1、首先在 Try 阶段,执行远程调用接口把 Smith 和 Bob 的钱给冻结起来。(资源预留)
2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
4.4 本地消息表(异步保证一致性)
需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方(我理解也可以通过定时任务发送到消费方,或者说通过定时任务调用消费方)。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
总结:这种方案遵循BASE理论,采用的是最终一致性,比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
我们的业务场景:
文档标注完成之后,需要审核,审核需要调用第三方的审核系统,前提做一些逻辑的判断,如果可以去调的话,更新本地记录为待审核的状态,
4.5 MQ事务消息
在RocketMQ中实现了分布式事务,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部。
基本流程如下: 第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务。
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。消息接受者就能使用这个消息。
如果确认消息失败,在RocketMq Broker中提供了定时扫描没有更新状态的消息,如果有消息没有得到确认,会向消息发送者发送消息,来判断是否提交,在rocketmq中是以listener的形式给发送者,用来处理。
如果消费超时,则需要一直重试,消息接收端需要保证幂等。如果消息消费失败,这个就需要人工进行处理,因为这个概率较低,如果为了这种小概率时间而设计这个复杂的流程反而得不偿失