java分布式系统常见的事务处理机制

为保障系统的可用性、可靠性以及性能,在分布式系统中,往往会设置数据冗余,即对数据进行复制。举例来说,当一个数据库的副本被破环以后,那么系统只需要转换到其他数据副本就能继续运行下去。另外一个例子,当访问单一服务器管理的数据的进程数不断增加时,系统就需要对服务器的数量进行扩充,此时,对服务器进行复制,随后让它们分担工作负荷,就可以提高性能。但同时,如何保障多个数据节点之间数据的一致以及如何处理分布式事务,将成为为一个复杂的话题。本文将介绍常用的事务处理机制。

一、理论

CAP定理

CAP定理(也称为 Brewer 定理),是由计算机科学家 Eric Brewer 提出的,即在分布式计算机系统不可能同时提供以下全部三个保证:

一致性(Consistency):所有节点同一时间看到是相同的数据

可用性(Availability):不管是否成功,确保每一个请求都能接收到响应

分区容错性(Partition tolerance):系统任意分区后,在网络故障时,仍能操作

·

显然,为了保障性能和可靠性,我们将数据复制多份,分布到多个节点上,同时也带来了一个难点,那就是如何保持各个副本数据的一致性。换句话说,我们选择了AP,则必须要牺牲掉 C 了。

但是,在实际的应用场景中,数据的一致性往往也是需要保证的。那么这是否违背了CAP定理呢?


一致性模型

其实,数据的一致性也分几种情况,大致可以分为:

(1)Weak弱一致性:当你写入一个新值后,读操作在数据副本上可能读出来,也可能读不出来。比如:某些存储系统,搜索引擎,实时游戏,语音聊天等,这些数据本文对完整性要求不高,数据是否一致关系也不大。

(2)Eventually最终一致性:当你写入一个新值后,并不一定能马上读出来,但在某个时间窗口之后保证最终能读出来。比如:DNS,电子邮件,消息中间件等系统,大部分分布式系统技术都采用这类模式。

(3)Strong强一致性:新的数据一旦写入,在任意副本任意时刻都能读到新值。比如:文件系统,RDBMS都是强一致性的。

·也就是说,在设计分布式系统时,我们并不一定要求是强一致性的,根据应用场景可以选择弱一致性或者是最终一致性。

事务的作用

保证执行结果的正确性

·

保证数据的一致性

ACID

常见的事务处理机制-Master-Slave复制

Slave一般是 Master 的备份。在这样的系统中,一般是如下设计的:

(1)读写请求都由Master负责。

(2)写请求写到Master上后,由 Master 同步到 Slave 上。

这种机制的特点是:

(1)数据同步通常是异步的

(2)有良好的吞吐量,低延迟* 在大多数 RDBMS 中支持,比如 MySQL二进制日志

(3)弱/最终一致性

这种机制的缺点是,如果Master挂了,Slave 只能提供读服务,而没有写服务。

常见的事务处理机制-Master-Master多主复制

指一个系统存在两个或多个Master,每个Master都提供读写服务。这个机制是Master-Slave的加强版,数据间同步一般是通过Master间的异步完成,所以是最终一致性。 Master-Master的好处是,一台Master挂了,别的Master可以正常做读写服务,他和Master-Slave一样,当数据没有被复制到别的Master上时,数据会丢失。很多数据库都支持Master-Master的Replication的机制。

这种机制的特点是:

(1)异步

(2)最终的一致性

(3)多个节点间需要序列化协议


Paxos算法

Paxos算法是 Leslie Lamport 于1990年提出的一种基于消息传递且具有高度容错特性的一致性算法。Paxos 算法目前在 Google 的 Chubby、MegaStore、Spanner 等系统中得到了应用,Hadoop 中的 ZooKeeper 也使用了 Paxos 算法。


在Paxos算法中,分为4种角色:

Proposer:提议者

Acceptor:决策者

Client:产生议题者

Learner:最终决策学习者

·

算法可以分为两个阶段来执行:

阶段1

Proposer选择一个议案编号 n,向 acceptor 的多数派发送编号也为 n 的 prepare 请求。

Acceptor:如果接收到的 prepare 请求的编号 n 大于它已经回应的任何prepare 请求,它就回应已经批准的编号最高的议案(如果有的话),并承诺不再回应任何编号小于 n 的议案;


阶段2

Proposer:如果收到了多数 acceptor 对 prepare 请求(编号为 n)的回应,它就向这些 acceptor 发送议案{n, v}的 accept 请求,其中 v 是所有回应中编号最高的议案的决议,或者是 proposer 选择的值,如果回应说还没有议案。

·

Acceptor:如果收到了议案{n, v}的 accept 请求,它就批准该议案,除非它已经回应了一个编号大于 n 的议案。

·

Proposer可以提出多个议案,只要它遵循上面的算法。它可以在任何时刻放弃一个议案。(这不会破坏正确性,即使在议案被放弃后,议案的请求或者回应消息才到达目标)如果其它的 proposer 已经开始提出更高编号的议案,那么最好能放弃当前的议案。因此,如果 acceptor 忽略一个 prepare 或者 accept 请求(因为已经收到了更高编号的 prepare 请求),它应该告知 proposer 放弃议案。这是一个性能优化,而不影响正确性。


二、两阶段提交

两阶段提交协议(Two-phase commit protocol,2PC)的过程涉及到协调者和参与者。协调者可以看做成事务的发起者,同时也是事务的一个参与者。对于一个分布式事务来说,一个事务是涉及到多个参与者的。具体的两阶段提交的过程如下:


java分布式系统常见的事务处理机制_第1张图片

第一阶段(准备阶段)

·协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。

参与者节点执行询问发起为止的所有事务操作,并将Undo信息和 Redo 信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)

·各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个“同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个“中止”消息。

第二阶段(提交阶段)

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

当协调者节点从所有参与者节点获得的相应消息都为“同意”时:

(1)协调者节点向所有参与者节点发出“正式提交(commit)”的请求。

(2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。

(3)参与者节点向协调者节点发送“完成”消息。

如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

(1)协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。

(2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。

(3)参与者节点向协调者节点发送”回滚完成”消息。

(4)协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。

(5)协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务

不管最后结果如何,第二阶段都会结束当前事务。

二段式提交协议的优缺点:

优点:原理简单,实现方便;

缺点:

(1)同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。

(2)单点故障。由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。

(3)数据不一致。在阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了 commit 请求。而在这部分参与者接到 commit 请求之后就会执行 commit 操作。但是其他部分未接到 commit 请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

(4)二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

(5)为了解决两阶段提交协议的种种问题,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。


三、三阶段提交

三阶段提交协议(Three-phase commit protocol,3PC),是二阶段提交(2PC)的改进版本。与两阶段提交不同的是,三阶段提交有两个改动点:

(1)引入超时机制。同时在协调者和参与者中都引入超时机制。

(2)在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

即3PC把 2PC 的准备阶段再次一分为二,这样三阶段提交就有 CanCommit、PreCommit、DoCommit 三个阶段。

CanCommit阶段

CanCommit阶段其实和 2PC 的准备阶段很像。协调者向参与者发送 commit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No 响应。

事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。

响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回 Yes 响应,并进入预备状态。否则反馈 No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared 阶段。

事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo 和 redo 信息记录到事务日志中。

响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

发送中断请求:协调者向所有参与者发送abort请求。

中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交

发送提交请求:协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送 doCommit 请求。

事务提交:参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。

响应反馈:事务提交完之后,向协调者发送ACK响应。

完成事务:协调者接收到所有参与者的ACK响应之后,完成事务。

中断事务:协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是 ACK 响应,也可能响应超时),那么就会执行中断事务。

发送中断请求:协调者向所有参与者发送abort请求

事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo 信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息

中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

在doCommit阶段,如果参与者无法及时接收到来自协调者的 doCommit 或者 rebort 请求时,会在等待超时之后,会继续进行事务的提交。即当进入第三阶段时,由于网络超时等原因,虽然参与者没有收 到 commit 或者 abort 响应,事务仍然会提交。

三阶段提交不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了 commit 操作,这样就和其他接到 abort 命令并执行回滚的参与者之间存在数据不一致的情况。


四、TCC

关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。 TCC事务机制相比于上面介绍的XA,解决了其几个缺点: 1.解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。 2.同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。 3.数据一致性,有了补偿机制之后,由业务活动管理器控制一致性


java分布式系统常见的事务处理机制_第2张图片

对于TCC的解释:

(1)Try阶段:尝试执行,完成所有业务检查(一致性),预留必须业务资源(准隔离性)

(2)Confirm阶段:确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性。要求具备幂等设计,Confirm失败后需要进行重试。

(3)Cancel阶段:取消执行,释放Try阶段预留的业务资源 Cancel操作满足幂等性Cancel阶段的异常和Confirm阶段异常处理方案基本上一致。

举个简单的例子如果你用100元买了一瓶水, Try阶段:你需要向你的钱包检查是否够100元并锁住这100元,水也是一样的。

如果有一个失败,则进行cancel(释放这100元和这一瓶水),如果cancel失败不论什么失败都进行重试cancel,所以需要保持幂等。

如果都成功,则进行confirm,确认这100元扣,和这一瓶水被卖,如果confirm失败无论什么失败则重试(会依靠活动日志进行重试)

对于TCC来说适合一些:

强隔离性,严格一致性要求的活动业务。

执行时间较短的业务


实现参考:ByteTCC:https://github.com/liuyangming/ByteTCC/


优点

对比与前面提到的两阶段提交法,有两大优势:

(1)TCC能够对分布式事务中的各个资源进行分别锁定, 分别提交与释放, 例如, 假设有AB两个操作, 假设A操作耗时短, 那么A就能较快的完成自身的try-confirm-cancel流程, 释放资源. 无需等待B操作. 如果事后出现问题, 追加执行补偿性事务即可.

(2)TCC是绑定在各个子业务上的(除了cancle中的全局回滚操作), 也就是各服务之间可以在一定程度上”异步并行”执行.

注意事项

(1)事务管理器(协调器)这个节点必须以带同步复制语义的高可用集群(HAC)方式部署.

(2)事务管理器(协调器)还需要使用多数派算法来避免集群发生脑裂问题.

适用场景

(1)严格一致性

(2)执行时间短

(3)实时性要求高

举例:红包, 收付款业务.

具体实现

1、需要在提供分布式事务的接口添加@Compensable

2、在对应的接口实现添加@Compensable

3、在接口实现上添加confirmMethod、cancelMethod、

transcationContextEditor

4、实现confirmMethod方法和cancelMethod方法

注意:confirm方法和cancel方法一定要和try方法在同一类中

5、主事务的业务都实现的差不多才调用子事务


五、本地消息表

本地消息表这个方案最初是ebay提出的 ebay的完整方案https://queue.acm.org/detail.cfm?id=1394128。

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。


java分布式系统常见的事务处理机制_第3张图片

对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用100元去买一瓶水的例子。

1.当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。

2.这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫他减去水的库存,到达商品服务器之后这个时候得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。

3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器本地消息表进行状态更新。

4.针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的,如果已经接收,在判断是否执行,如果执行在马上又进行通知事务,如果未执行,需要重新执行需要由业务保证幂等,也就是不会多扣一瓶水。

本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。


六、MQ事务RocketMQ)

在RocketMQ中实现了分布式事务,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,下面简单介绍一下MQ事务,如果想对其详细了解可以参考: https://www.jianshu.com/p/453c6e7ff81c


java分布式系统常见的事务处理机制_第4张图片

程如下: 第一阶段Prepared消息,会拿到消息的地址。

第二阶段执行本地事务。

第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。消息接受者就能使用这个消息。

如果确认消息失败,在RocketMq Broker中提供了定时扫描没有更新状态的消息,如果有消息没有得到确认,会向消息发送者发送消息,来判断是否提交,在rocketmq中是以listener的形式给发送者,用来处理


java分布式系统常见的事务处理机制_第5张图片

费超时,则需要一直重试,消息接收端需要保证幂等。如果消息消费失败,这个就需要人工进行处理,因为这个概率较低,如果为了这种小概率时间而设计这个复杂的流程反而得不偿失


七、Saga事务

Saga是30年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。 Saga的组成:

每个Saga由一系列sub-transaction Ti 组成 每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果,这里的每个T,都是一个本地事务。 可以看到,和TCC相比,Saga没有“预留 try”动作,它的Ti就是直接提交到库。

Saga的执行顺序有两种:

T1, T2, T3, ..., Tn

T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n Saga定义了两种恢复策略:

向后恢复,即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。 向前恢复,适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci。

这里要注意的是,在saga模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或者影响当前事务。

还是拿100元买一瓶水的例子来说,这里定义

T1=扣100元 T2=给用户加一瓶水 T3=减库存一瓶水

C1=加100元 C2=给用户减一瓶水 C3=给库存加一瓶水

我们一次进行T1,T2,T3如果发生问题,就执行发生问题的C操作的反向。 上面说到的隔离性的问题会出现在,如果执行到T3这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题

可以看见saga模式没有隔离性的影响还是较大,可以参照华为的解决方案:从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。也可以在业务层面通过预先冻结资金的方式隔离这部分资源, 最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。

具体实例:可以参考华为的servicecomb


八、总结

TCC和MQ事务的比较

(1)基于消息事务是强一致性,造成资源浪费

(2)TCC是柔性事务,在Try阶段要对资源进行预留

(3)TCC事务在确认或者取消阶段释放资源

(4)TCC的时效性更好


分布式事务的框架

1、2pc、3pc:基于数据库的XA协议(效率低)\基于应用层的Seata

2、tcc框架:tcc-transcation\hmily\ByteTCC

3、消息最终一致性:

本地消息队列\rocketmq\ribbitmq

(https://www.cnblogs.com/hhhshct/p/11496536.html)

4、saga事务

你可能感兴趣的:(java分布式系统常见的事务处理机制)