简介
随着互联化的蔓延,各种项目都逐渐向分布式服务做转换。如今微服务已经普遍存在,本地事务已经无法满足分布式的要求,由此分布式事务问题诞生。 分布式事务被称为世界性的难题,目前分布式事务存在两大理论依据:CAP定律 BASE理论。
该文章之前如果还不是特别清除事务的ACID特性和隔离级别,可看下浅谈ACID事务管理及隔离级别
CAP定律
CAP 定律,又被叫作布鲁尔定律,在一个分布式系统中,、Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。
一致性(C)
分布式系统中同一时刻是否是同样的值,对于不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
换句话来说:A节点写操作后,B节点是否能读取到最新的数据。
可用性(A)
非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回 50,而不是返回 40。
换句话来说:集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容错性(P)
出现网络分区后,系统能够继续工作。比如集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。
熟悉 CAP 的人都知道,三者不能共有,分布式系统中,网络无法 100% 可靠,分区其实是一个必然现象。
如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是 A 又不允许,所以分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
对于 CP 来说,放弃可用性,追求一致性和分区容错性,我们的 ZooKeeper 其实就是追求的强一致。
对于 AP 来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的 BASE 也是根据 AP 来扩展。
顺便一提,CAP 理论中是忽略网络延迟,也就是当事务提交时,从节点 A 复制到节点 B 没有延迟,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。同时 CAP 中选择两个,比如你选择了 CP,并不是叫你放弃 A。因为 P 出现的概率实在是太小了,大部分的时间你仍然需要保证 CA。就算分区出现了你也要为后来的 A 做准备,比如通过一些日志的手段,是其他机器回复至可用。
BASE理论
BASE是Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。
BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。
BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
基本可用
分布式系统在出现故障时,允许部分功能不可用,保证核心功能可用
软状态
允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致。
最终一致
最终一致是指经过一段时间后,所有节点数据都将会达到一致。
BASE 解决了 CAP 中理论没有网络延迟,在 BASE 中用软状态和最终一致,保证了延迟后的一致性。
BASE 和 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。
分布式事务解决方案
两阶段提交协议(2PC)
说到 2PC ,不得不聊数据库分布式事务中的 XA Transactions.
一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。
优点:
优点尽量保证了数据的强一致,实现成本较低|
缺点
单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导据库无法使用
同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
总的来说,XA 协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。
三阶段提交协议(3pc)
顾名思义,算是2pc的改进版本
与两阶段提交不同的是,三阶段提交有两个改动点
1.引入超时机制。同时在协调者和参与者中都引入超时机制。
2.在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的
除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段
第一阶段(CanCommit)
协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应(基本与2pc第一阶段相同)
第二阶段(PreCommit)
发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
发送中断请求 协调者向所有参与者发送abort请求。
中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
第三阶段(doCommit)
执行提交
发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
响应反馈 事务提交完之后,向协调者发送Ack响应。
完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
中断事务
协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
发送中断请求 协调者向所有参与者发送abort请求
事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
优点
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。
缺点
由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
补偿事务(TCC)
TCC 其实就是采用的补偿机制,核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
它分为三个阶段:
第一、Try 阶段主要是对业务系统做检测及资源预留。
第二、Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功
第三、Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举例:A向B进行转账。
Try 阶段:调用远程接口把 A 和 B 的钱给冻结起来
Confirm阶段:执行远程调用的转账的操作,转账成功进行解冻。
Cancel阶段:第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法(或进行尝试)。
优点
跟2PC比起来,实现以及流程相对简单了一些
缺点:成本代价大
第一第二阶段可能会失败,需要通过大量的补偿代码保证数据的幂等。
本地消息表
该实现方式应该是业界使用最多的,核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试,这种思路是来源于ebay。
如图:
基本思路
A服务(消息生产)B服务(消息消费)
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
该方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
优点
种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点
消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理
MQ事务消息
市面上大部分MQ是不支持事务消息的,有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交。
基本流程
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务。
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
如果确认消息失败,在 RocketMQ Broker 中提供了定时扫描没有更新状态的消息。
如果有消息没有得到确认,会向消息发送者发送消息,来判断是否提交,在 RocketMQ 中是以 Listener 的形式给发送者,用来处理
如果消费超时,则一直重试,消息接收端需要保证幂等。如果消息消费失败,这时就需要人工进行处理,因为这个概率较低,如果为了这种小概率时间而设计这个复杂的流程反而得不偿失。
优点
优点: 实现了最终一致性,不需要依赖本地数据库事务
缺点:
实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。
Sagas 事务模型
Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题
该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。
例如我们买一瓶水
T1 = 扣 10 元,T2 = 给用户加一瓶水,T3 = 减库存一瓶水
C1 = 加10元,C2 = 给用户减一瓶水,C3 = 给库存加一瓶水
我们一次进行 T1,T2,T3 如果发生问题,就执行发生问题的 C 操作的反向。
这里要注意的是,在 Saga 模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或者影响当前事务。比如执行到 T3 这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题。
由此说明 Saga 模式没有隔离性的影响还是较大,可以参照华为的解决方案:从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。也可以在业务层面通过预先冻结资金的方式隔离这部分资源, 最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。(具体实例:可以参考华为的 Service Comb)
结束语
分布式事务的成本还是很大的,要不然不回尊称它为世界性难题。
不到万不得已,不建议使用。
目前阿里的全局事务服务 GTS和TX_LCN都在市面上用的比较多,具体可以看看