分布式学习-分布式事务

事务定义

事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。
事务

数据库事务特性(ACID)

A:原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

例如:银行转账,从A账户转100元至B账户,分为两个步骤:

(1)从A账户取100元
(2)存入100元至B账户。
这两步要么一起完成,要么一起不完成,如果只完成第一步,第二步失败,钱会莫名其妙少了100元。

C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

例如:现有完整性约束A+B=100,如果一个事务改变了A,那么必须得改变B,使得事务结束后依然满足A+B=100,否则事务失败。

I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

例如:现有有个交易是从A账户转100元至B账户,在这个交易事务还未完成的情况下,如果此时B查询自己的账户,是看不到新增加的100元的。

D:持久性(Durability)

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。

数据库事务实现原理

以MySQL的InnoDB为例,事务的ACID是通过InnoDB日志和锁来保证。

事务的隔离性是通过数据库锁的机制实现的,持久性通过redo log(重做日志)来实现,原子性和一致性通过Undo log来实现。

UndoLog的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。

和Undo Log相反,RedoLog记录的是新数据的备份。在事务提交前,只要将RedoLog持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是RedoLog已经持久化。系统可以根据RedoLog的内容,将所有数据恢复到最新的状态。

什么时候使用数据库事务

简单而言,就是业务上有一组数据操作,需要如果其中有任何一个操作执行失败,整组操作全部不执行并恢复到未执行状态,要么全部成功,要么全部失败。

在使用数据库事务时需要注意,尽可能短的保持事务,修改多个不同表的数据的冗长事务会严重妨碍系统中的所有其他用户,这很有可能导致一些性能问题。

分布式理论

CAP理论

  • 一致性(Consistency):分布式数据库的数据保持一致。
  • 可用性(Availability):任何一个节点挂了,其他节点可以继续对外提供服务。
  • 分区容错性(网络分区)Partition tolerance:一个数据库所在的机器坏了,如硬盘坏了,数据丢失了,可以新增一台机器,然后从其他正常的机器把备份的数据同步过来。

在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)3 个要素最多只能同时满足两个,不可兼得。其中,分区容忍性又是不可或缺的。

CAP理论

  • CA(放弃P):将所有的数据放在一个节点。满足一致性、可用性。比如传统的关系型数据库。
  • AP(放弃C):放弃强一致性,用最终一致性来保证。比如Zookeeper。
  • CP(放弃A):一旦系统遇到故障,受到影响的服务器需要等待一段时间,在恢复期间无法对外提供服务。比如Redis等。

CAP理论举例:

有3台机器分别有3个数据库分别有两张表,数据都是一样的
Machine1-db1-tbl_person、tbl_order
Machine2-db2-tbl_person、tbl_order
Machine3-db3-tbl_person、tbl_order

1)当向machine1的db1的表tbl_person、tbl_order插入数数据时,同时要把插入的数据同步到machine2、machine3,这就是一致性。
2)当其中的一台机器宕机了,可以继续对外提供服务,把宕机的机器重新启动起来可以继续服务,这就是可用性。
3)当machine1的机器坏了,数据全部丢失了,不会有任何问题,因为machine2和machine3上还有数据,重新加一台机器machine4,把machine2和machine3其中一台机器的备份数据同步过来就可以了,这就是分区容错性。

为什么三个不能同时满足?

虽然 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下来思考,会发现必须选择 P(分区容忍)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。

如果选择了 CA(一致性 + 可用性) 而放弃了 P(分区容忍性),那么当发生分区现象时,为了保证 C(一致性),系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A(可用性) 冲突了,因为 A(可用性)要求返回 no error 和 no timeout。

因此,分布式系统理论上不可能选择 CA (一致性 + 可用性)架构,只能选择 CP(一致性 + 分区容忍性) 或者 AP (可用性 + 分区容忍性)架构,在一致性和可用性做折中选择。

主要解释一下为什么在满足分区容错性的条件下,可用性与一致性不能同时满足。
一致性要求所有节点数据都一致时再响应客户端请求,那么一旦发生节点宕机或者网络隔离,就需要等待节点恢复或者网络恢复或者集群重新调整节点数目等等,才能满足一致性;在这段时间,服务是不可用的。相反如果想要在这种状态下,服务继续可用,便需要牺牲一致性。

顺便一提,CAP理论中是忽略网络延迟,也就是当事务提交时,从节点A复制到节点B,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。同时CAP中选择两个,比如你选择了CP,并不是叫你放弃A。因为P出现的概率实在是太小了,大部分的时间你仍然需要保证CA。就算分区出现了你也要为后来的A做准备,比如通过一些日志的手段,是其他机器回复至可用。

CAP理论告诉我们分布式系统只能选择AP或者CP,但实际上并不是说整个系统只能选择AP或者CP,在 CAP 理论落地实践时,我们需要将系统内的数据按照不同的应用场景和要求进行分类,每类数据选择不同的策略(CP 还是 AP),而不是直接限定整个系统所有数据都是同一策略。

另外,只能选择CP或者AP是指系统发生分区现象时无法同时保证C(一致性)和A(可用性),但不是意味着什么都不做,当分区故障解决后,系统还是要保持保证CA。

BASE理论

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展

  • 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。比如服务降级、页面降级。

  • 软状态:允许系统中存在中间状态,这个状态不影响系统可用性。

    • 这里的中间状态是指不同的data replication之间的数据更新可以出现延时的最终一致性。
    • 如CAP理论里面的示例,当向machine1的db1的表tbl_person、tbl_order插入数数据时,同时要把插入的数据同步到machine2、machine3,当machine3的网络有问题时,同步失败,但是过一会网络恢复了就同步成功了,这个同步失败的状态就称为软状态,因为最终还是同步成功了。
  • 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

数据一致性模型

数据的一致性模型可以分成以下 3 类:

  1. 强一致性:数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步的方式实现。
  2. 弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久之后可以读到。
  3. 最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。

分布式事务

什么是分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

举个互联网常用的交易业务为例:

互联网交易业务例子

上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。

交易处理事务说明

可以看到,如果多个数据库之间的数据更新没有保证事务,将会导致出现子系统数据不一致,业务出现问题。

分布式事务解决方案

2PC

2PC = Two Phase commit 二阶段提交(RDBMS(关系型数据库管理系统)经常就是这种机制,保证强一致性)。

在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交。

算法思路

参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

核心思想

对每一个事务都采用先尝试后提交的处理方式,处理后所有的读操作都要能获得最新的数据,因此也可以将二阶段提交看作是一个强一致性算法。

处理流程

阶段1:提交事务请求(投票阶段)询问是否可以提交事务

2PC阶段1

进一步将准备阶段分为以下三个步骤:

1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。

阶段2:执行事务提交(commit、rollback) 真正的提交事务

2PC阶段2

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

当协调者节点从所有参与者节点获得的响应消息都为”同意”时:
1)协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
3)参与者节点向协调者节点反馈ack(应答)”完成”消息。
4)协调者节点收到所有参与者节点反馈的ack应答”完成”消息后,完成事务。

2PC正常提交

如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
1)协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
3)参与者节点向协调者节点发送”回滚完成”消息。
4)协调者节点收到所有参与者节点反馈的”回滚完成”消息后,取消事务。

2PC异常中断

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

存在的问题

1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

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

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

需要注意的是:在全局事务决定回滚时,直接逐个发送rollback请求即可,不必分阶段。

2PC机制需要RM提供底层支持(一般是兼容XA)。如果使用Java,那么可以使用开源软件 atomikos 或者JOTM来快速实现,两者都支持spring事务整合。

3PC

3PC = Three Phase commit 三阶段提交

由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷,所以,在二阶段提交的基础上做了改进,提出了三阶段提交。

3PC

与两阶段提交不同的是,三阶段提交有两个改动点:

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

2、将二阶段的准备阶段拆分为2个阶段,插入了一个preCommit阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。保证了在最后提交阶段之前各参与节点的状态是一致的。

处理流程

阶段1:CanCommit阶段,是否提交-询问是否可以做事务提交

  1. 事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  2. 响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No。

阶段2:PreCommit阶段,预先提交-预先提交事务

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

  1. 发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
  2. 事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
  3. 响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

3PC阶段二预执行事务

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

1.发送中断请求:协调者向所有参与者发送abort请求。
2.中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

3PC阶段二中断事务

阶段3:doCommit阶段,执行事务提交(commit、rollback)真正的提交事务执行提交

  1. 发送提交请求:协调者接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
  2. 事务提交:参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  3. 响应反馈:事务提交完之后,向协调者发送Ack响应。
  4. 完成事务:协调者接收到所有参与者的ack响应之后,完成事务。

3PC阶段三正式执行事务

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

  1. 发送中断请求:协调者向所有参与者发送abort请求。
  2. 事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  3. 反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息。
  4. 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

3PC阶段三中断事务

在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。)

2PC与3PC的区别

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

目前两阶段提交、三阶段提交存在如下的局限性,并不适合在微服务架构体系下使用:

  • 所有的操作必须是事务性资源(比如数据库、消息队列、EJB组件等),存在使用局限性(微服务架构下多数使用HTTP协议),比较适合传统的单体应用;
  • 由于是强一致性,资源需要在事务内部等待,性能影响较大,吞吐率不高,不适合高并发与高性能的业务场景;

TCC事务

TCC事务机制相对于传统事务机制(X/Open XA),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。

解决了XA的几个缺点:

  1. 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
  2. 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  3. 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性 。

一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。

Try:完成所有业务检查,预留必须业务资源。

Confirm:真正执行业务,不作任何业务检查;只使用Try阶段预留的业务资源;Confirm操作满足幂等性。

Cancel:释放Try阶段预留的业务资源;Cancel操作满足幂等性。

整个TCC业务分成两个阶段完成:
TCC事务

第一阶段:
主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。

第二阶段:
活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。

与2PC比较:

  • 位于业务服务层而非资源层。
  • 没有单独的准备(prepare)阶段,Try操作兼备资源操作与准备能力。
  • Try操作可以灵活选择业务资源的锁定粒度。
  • 开发成本较高。

缺点:

  • Canfirm和Cancel的幂等性很难保证。
  • 这种方式缺点比较多,通常在复杂场景下是不推荐使用的,除非是非常简单的场景,非常容易提供回滚Cancel,而且依赖的服务也非常少的情况。
  • 这种实现方式会造成代码量庞大,耦合性高。而且非常有局限性,因为有很多的业务是无法很简单的实现回滚的,如果串行的服务很多,回滚的成本实在太高。

本地消息表(异步确保最终一致性)

本地消息表的方案最初是由ebay提出,核心思路是将分布式事务拆分成本地事务进行处理。通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

简单来说就是:本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。

处理流程

把分布式事务最先开始处理的事务方成为事务主动方,在事务主动方之后处理的业务内的其他事务成为事务被动方。流程如下图:

本地消息表方案

  1. 事务主动方处理本地事务。

事务主动发在本地事务中处理业务更新操作和写消息表操作。(如上图中操作1、2)

  1. 事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。

消息中间件可以基于Kafka、RocketMQ消息队列,事务主动方法主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。(如上图中操作3、4、5)

  1. 事务被动方通过消息中间件,通知事务主动方事务已处理的消息。(如上图中6、7、8)

为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。具体保存一致性的容错处理如下:

1、当步骤1处理出错,事务回滚,相当于什么都没发生。
2、当步骤2、步骤3处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询为超时消息数据,再次发送的消息中间件进行处理。事务被动方消费事务消息重试处理。
3、如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
4、如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。

优点

  • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对MQ中间件特性的依赖。
  • 方案轻量,容易实现。

缺点

  • 与具体的业务场景绑定,耦合性强,不可公用。
  • 消息数据与业务数据同库,占用业务系统资源。
  • 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。

结合MQ实现的可靠消息最终一致性

基于MQ的分布式事务方案其实是对本地消息表的封装,将本地消息表基于MQ 内部,其他方面的协议基本与本地消息表一致。

处理流程

以阿里的 RocketMQ 中间件为例,在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ的事务消息相对于普通MQ,相对于提供了2PC的提交接口,方案如下:

正常情况——事务主动方发消息

这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:

MQ事务-正常情况-事务主动方发消息

1、发送方向 MQ服务端(MQ Server)发送half消息。
2、MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。
3、发送方开始执行本地事务逻辑。
4、发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
5、MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。

异常情况——事务主动方消息恢复

在断网或者应用重启等异常情况下,发送方提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

MQ事务-异常情况——事务主动方消息恢复

5、MQ Server 对该消息发起消息回查。
6、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
7、发送方根据检查得到的本地事务的最终状态再次提交二次确认。
8、MQ Server基于commit / rollback 对消息进行投递或者删除。

MQ分布式事务

MQ分布式事务

事务主动方基于MQ通信通知事务被动方处理事务,事务被动方基于MQ返回处理结果。
如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。
如果是事务被动方业务上的处理失败,可以通过MQ通知事务主动方进行补偿或者事务回滚。

优点

  • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
  • 吞吐量由于使用本地消息表方案。

缺点

  • 一次消息发送需要两次网络请求(half消息、commit/rollback消息)。
  • 业务处理服务需要实现消息状态回查接口。

尽最大努力通知

这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。

处理流程

  1. 业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失。
  2. 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知。
  3. 主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。
  4. 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。
  5. 如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息

方案特点

  1. 用到的服务模式:可查询操作、幂等操作。
  2. 被动方的处理结果不影响主动方的处理结果;
  3. 适用于对业务最终一致性的时间敏感度低的系统;
  4. 适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作,比如银行通知、商户通知等;

Saga事务——最终一致性

简介

Saga事务源于1987年普林斯顿大学的Hecto和Kenneth发表的如何处理long lived transaction(长活事务)论文,Saga事务核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

Saga事务基本协议

  • 每个Saga事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
  • 每个Ti 都有对应的幂等补偿动作Ci,补偿动作用于撤销Ti造成的结果。

可以看到,和TCC相比,Saga没有“预留”动作,它的Ti就是直接提交到库。

Saga事务执行顺序

下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分
Saga的执行顺序有两种:

Saga事务执行顺序

事务正常执行完成

T1, T2, T3, ..., Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。

事务回滚

T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。

Saga恢复策略:

  1. 向前恢复(forward recovery)

Saga事务向前恢复

对应于上面第一种执行顺序,适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的子事务(sub-transaction)。该情况下不需要Ci。

  1. 向后恢复(backward recovery)

Saga事务向后恢复

对应于上面提到的第二种执行顺序,其中j是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个Saga的执行结果撤销。

Saga事务实现方式

1、命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。

中央协调器(Orchestrator,简称OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。

Saga命令协调模式

以电商订单的例子为例:

1、事务发起方的主业务逻辑请求OSO服务开启订单事务。
2、OSO向库存服务请求扣减库存,库存服务回复处理结果。
3、OSO向订单服务请求创建订单,订单服务回复创建结果。
4、OSO向支付服务请求支付,支付服务回复处理结果。
5、主业务逻辑接收并处理OSO事务处理结果回复。

中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。

2、事件编排 (Event Choreography0:没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。

在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。

当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何Saga参与者听到都意味着事务结束。

Saga事件编排模式

以电商订单的例子为例:

1、事务发起方的主业务逻辑发布开始订单事件。
2、库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
3、订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
4、支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
5、主业务逻辑监听订单已支付事件并处理。

事件/编排是实现Saga模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及2至4个步骤,则可能是非常合适的。

命令协调设计的优点

1、服务之间关系简单,避免服务之间的循环依赖关系,因为Saga协调器会调用Saga参与者,但参与者不会调用协调器
2、程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
3、易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试

命令协调设计的缺点

1、中央协调器容易处理逻辑容易过于复杂,导致难以维护。
2、存在协调器单点故障风险。

事件/编排设计的优点

1、避免中央协调器单点故障风险。
2、当涉及的步骤较少服务开发简单,容易实现。

事件/编排设计的缺点

1、服务之间存在循环依赖的风险。
2、当涉及的步骤较多,服务间关系混乱,难以追踪调测。

值得补充的是,由于Saga模型中没有Prepare阶段,因此事务间不能保证隔离性,当多个Saga事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。

分布式事务解决方案比较

分布式事务解决方案比较

2PC/3PC
依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。

TCC
适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。

本地消息表/MQ事务
都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。

Saga事务
由于Saga事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。
Saga相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga事务较适用于补偿动作容易处理的场景。

本文标题:分布式学习-分布式事务

文章作者:王洪博

发布时间:2019年09月10日 - 18:09

最后更新:2019年09月12日 - 10:09

原始链接:http://whb1990.github.io/posts/464cb56d.html

▄︻┻═┳一如果你喜欢这篇文章,请点击下方"打赏"按钮请我喝杯 ☕
0%