分布式原理-05丨分布式事务

Posted by jiefang on January 4, 2020

分布式事务

分布式事务,就是在分布式系统中运行的事务,由多个本地事务组合而成。 实现分布式事务有以下 3 种基本方法:

  • 基于 XA 协议的二阶段提交协议方法;
  • 三阶段提交协议方法;
  • 基于消息的最终一致性方法;
  • TCC事务;

基于 XA 协议的二阶段提交方法

XA 是一个分布式事务协议,规定了事务管理器和资源管理器接口。因此,XA 协议可以分为两部分,即事务管理器和本地资源管理器。

XA 实现分布式事务的原理,类似于集中式算法:事务管理器作为协调者,负责各个本地资源的提交和回滚;而资源管理器就是分布式事务的参与者,通常由数据库实现,比如 Oracle、DB2 等商业数据库都实现了 XA 接口。

基于 XA 协议的二阶段提交方法中,二阶段提交协议(The two-phase commit protocol,2PC),用于保证分布式系统中事务提交时的数据一致性,是 XA 在全局事务中用于协调多个资源的机制。

执行过程

两阶段提交协议的执行过程,分为投票voting)和提交commit)两个阶段。 投票为第一阶段,协调者(Coordinator,即事务管理器)会向事务的参与者(Cohort,即本地资源管理器)发起执行操作的CanCommit请求,并等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,记录日志信息但不提交,待参与者执行成功,则向协调者发送“Yes”消息,表示同意操作;若不成功,则发送“No”消息,表示终止操作。

当所有的参与者都返回了操作结果(Yes或No消息)后,系统进入了提交阶段。在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommitDoAbort指令:

  • 若协调者收到的都是“Yes”消息,则向参与者发送“DoCommit”消息,参与者会完成剩余的操作并释放资源,然后向协调者返回“HaveCommitted”消息;
  • 如果协调者收到的消息中包含“No”消息,则向所有参与者发送“DoAbort”消息,此时发送“Yes”的参与者则会根据之前执行操作时的回滚日志对操作进行回滚,然后所有参与者会向协调者发送“HaveCommitted”消息;协调者接收到“HaveCommitted”消息,就意味着整个事务结束了。

二阶段提交的算法思路可以概括为:协调者下发请求事务操作,参与 者将操作结果通知协调者,协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作。

基于 XA 的二阶段提交算法基本满足了事务的 ACID 特性,但依然有些不足:

  • 同步阻塞问题:二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态。
  • 单点故障问题:基于XA的二阶段提交算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
  • 数据不一致问题:在提交阶段,当协调者向参与者发送 DoCommit 请求之后,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。

三阶段提交方法

三阶段提交协议(Three-phase commit protocol,3PC),是对二阶段提交(2PC)的改进。为了解决两阶段提交的同步阻塞和数据不一致问题,三阶段提交引入了超时机制和准备阶段

同时在协调者和参与者中引入超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务。

在第一阶段和第二阶段中间引入了一个准备阶段,也就是在提交阶段之前,加入了一个预提交阶段。在预提交阶段排除一些不一致的情况,保证在最后提交之前各参与节点的状态是一致的。也就是说,除了引入超时机制之外,3PC 把 2PC 的提交阶段一分为二,这样三阶段提交协 议就有 CanCommitPreCommitDoCommit 三个阶段。

CanCommit 阶段

CanCommit 阶段与 2PC的投票阶段类似:协调者向参与者发送请求操作(CanCommit请求),询问参与者是否可以执行事务提交操作,然后等待参与者的响应;参与者收到CanCommit请求之后,回复Yes,表示可以顺利执行事务;否则回复 No。

image

PreCommit 阶段

协调者根据参与者的回复情况,来决定是否可以进行 PreCommit 操作。 如果所有参与者回复的都是“Yes”,那么协调者就会执行事务的预执行:

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

假如任何一个参与者向协调者发送了“No”消息,或者等待超时之后,协调者都没有收到参与者的响应,就执行中断事务的操作:

  • 发送中断请求:协调者向所有参与者发送“Abort”消息。
  • 终断事务:参与者收到“Abort”消息之后,或超时后仍未收到协调者的消息,执行事务的终断操作。

image

DoCommit 阶段

DoCmmit阶段进行真正的事务提交,根据PreCommit阶段协调者发送的消息,进入执行提交阶段或事务中断阶段。

执行提交阶段

  • 发送提交请求:协调者接收到所有参与者发送的Ack响应,从预提交状态进入到提交状态,并向所有参与者发送 DoCommit 消息。
  • 事务提交:参与者接收到DoCommit消息之后,正式提交事务。完成事务提交之后,释放所有锁住的资源。
  • 响应反馈:参与者提交完事务之后,向协调者发送 Ack 响应。
  • 完成事务:协调者接收到所有参与者的 Ack 响应之后,完成事务。

事务中断阶段

  • 发送中断请求:协调者向所有参与者发送 Abort 请求。
  • 事务回滚:参与者接收到 Abort 消息之后,利用其在 PreCommit 阶段记录的Undo信息执行事务的回滚操作,并释放所有锁住的资源。
  • 反馈结果:参与者完成事务回滚之后,向协调者发送 Ack 消息。
  • 终断事务:协调者接收到参与者反馈的Ack消息之后,执行事务的终断,并结束事务。

image

基于分布式消息的最终一致性方案

2PC 和 3PC 这两种方法,有两个共同的缺点:

  • 一是都需要锁定资源,降低系统性能;
  • 二是,没有解决数据不一致的问题。 因此,便有了通过分布式消息来确保事务最终一致性的方案。

将需要分布式处理的事务通过消息或者日志的方式异步执行,消息或日志可以存到本地文件、数据库或消息队列中,再通过业务规则进行失败重试。这就是使用基于分布式消息的最终一致性方案

基于分布式消息的最终一致性方案的事务处理,引入了一个消息中间件(Message Queue,MQ),用于在多个应用之间进行消息传递。

流程

image

image

  1. 订单系统把订单消息发给消息中间件,消息状态标记为“待确认”;
  2. 消息中间件收到消息后,进行消息持久化操作,即在消息存储系统中新增一条状态为“待发送”的消息;
  3. 消息中间件返回消息持久化结果(成功/失败),订单系统根据返回结果判断如何进行业务操作。失败,放弃订单,结束(必要时向上层返回失败结果);成功,则创建订单;
  4. 订单操作完成后,把操作结果(成功 / 失败)发送给消息中间件;
  5. 消息中间件收到业务操作结果后,根据结果进行处理:失败,删除消息存储中的消息,结束;成功,则更新消息存储中的消息状态为“待发送(可发送)”,并执行消息投递;
  6. 如果消息状态为“可发送”,则MQ会将消息发送给支付系统,表示已经创建好订单,需要对订单进行支付。支付系统也按照上述方式进行订单支付操作;
  7. 订单系统支付完成后,会将支付消息返回给消息中间件,中间件将消息传送给订单系统。订单系统再调用库存系统,进行出货操作;

分布式事务中,当且仅当所有的事务均成功时整个流程才成功。所以,分布式事务的一致性是实现分布式事务的关键问题,目前来看还没有一种很简单、完美的方案可以应对所有场景。

TCC事务

TCC是柔性事务的一种实现,TCC是三个首字母,Try-Confirm-Cancel,具体描述是将整个操作分为上面这三步。TCC事务处理流程和2PC二阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质就是一个应用层面的2PC。

在TCC协议里,参与的主体分为两种:

  • 发起方:发起事务的应用;
  • 参与方:执行事务请求,手上握有资源的服务;

三种动作:TryConfirmCancel

  • Try:负责预留资源(比如新建一条状态=PENDING的订单),同时也做业务检查(比如看看余额是否足够),简单来说就是不能预留已经被占用的资源;
  • Confirm:负责落地所预留的资源(比如扣费、把订单状态变成COMPLETED);
  • Cancel:负责撤销所预留的资源(比如把订单状态变成CANCELED);

ACID保证:

  • A,正常情况下保证;
  • C,在某个时间点,会出现A库和B库的数据违反一致性要求的情况,但是最终是一致的;
  • I,在某个时间点,A事务能够读到B事务部分提交的结果;
  • D,和本地事务一样,只要commit则数据被持久;

TCC时的注意事项:

  • TCC模式在于服务层面而非数据库层面;
  • TCC模式依赖于各服务正确实现Try、Confirm、Cancel和timeout处理机制;
  • TCC模式最少通信次数为2n(n=服务数量);
  • 不是所有业务模型都适合使用TCC,比如发邮件业务根本就不需要预留资源;
  • 需要良好地设计服务的日志、人工处理流程/机制,便于异常情况的处理;

TCC优缺点:

  • TCC优点:让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能;
  • TCC不足之处:
    • 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高;
    • 实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等;

流程

image

  1. 【发起方】发送Try到所有参与方;
  2. 每个【参与方】执行Try,预留资源;
  3. 【发起方】收到所有【参与方】的Try结果;
  4. 【发起方】发送Confirm/Cancel到所有参与房;
  5. 每个【参与方】执行Confirm/Cancel;
  6. 【发起方】收到所有【参与方】的Confirm/Cancel结果;

总结

image