分布式事务解决的核心问题是数据一致性的问题,根据 cap 理论可知一致性、可用性、分区容错性三者只能选其二,所以在实践中需要对此做出权衡。目前解决分布式事务的模型有 M/S、MM、2PC/3PC、paxos。

M/S 指的是 master/slave,master 作为可读写的节点,slave 作为只读节点,slave 定时去 master 上拉取最新数据,当 master 挂掉的时候集群便不能写,必须要等 master 恢复过来,否则便可能出现数据丢失。

MM 指的是 master/master,有两个以及两个以上的节点作为写节点,写节点之间相互拉取最新的数据,这种模式在极端情况下同样可能出现数据丢失的问题。

2PC 指的是两阶段提交,该模型有两个角色,分别是事务管理器和资源管理器,事务管理器首先向所有的资源管理器发出预提交命令,此时资源管理器需要写 redo log、undo log,当执行成功后向事务管理器回复确认,当事务管理器收到所有的资源管理器的确认后,然后向所有的资源管理器发出提交事务命令。这个方式有一些缺陷,其中一个很严重的问题是当整个流程执行到向资源管理器发送提交事务命令时,如果事务管理器挂掉了,那么资源管理器便不知道该怎么办了,同时还锁死了资源。

3PC 是对 2PC 的改进,总共分为三个阶段,第一阶段先询问一下,但是不锁资源,后面两个阶段和 2PC 的阶段相同,这种改进的好处是如果第一阶段所有节点返回成功,那么有理由相信成功提交的概率很大。如果有节点返回失败,那么也不会锁资源,能提高吞吐量。

paxos 算法也分为两个阶段,在第一阶段 (Proposer) 提案者向所有的节点发出 prepare request,关于所修改的数据有一个提案号,(Acceptor 提案的表决者)节点在接收到提案后会比较提案号,如果该提案号是最新的,那么便接受该请求,并向提案者回复确认,在第二阶段当提案者收到超过半数节点的确认后,便会向所有的节点发布 accept request,同样需要带上提案号,当节点收到 accept request 后,如果提案号是最大的,那么节点便会修改这个值,如果发现自己有一个更大的提案号那么,节点便会拒绝修改。

paxos 的这种方式要求节点数目必须是奇数才能满足超过半数节点的确认这样的要求,这里的确认是包含发出请求的节点的,这样才能满足奇数的要求。

下图是对于这几种方式的一个汇总,分别比较了一致性、事务、延迟、吞吐量、是否有数据丢失、失败时是否可读写。

基于可靠消息服务的分布式事务

这种实现分布式事务的方式需要通过消息中间件来实现。假设有 A 和 B 两个系统,分别可以处理任务 A 和任务 B。此时系统 A 中存在一个业务流程,需要将任务 A 和任务 B 在同一个事务中处理。

  • 在系统 A 处理任务 A 前,先向消息中间件发一条消息
  • 消息中间件收到消息后将消息持久化,但不投递消息。此时下游系统 B 任然不知道该条消息的存在。
  • 消息中间件持久化成功后,便向系统 A 返回一个确认应答
  • 系统 A 收到确认应答后则可以开始处理任务 A
  • 任务 A 处理完成后向详细中间件发送 Commit 请求。该请求发送完成后,对系统 A 而言,该事务的处理过程就结束了,此时它可以处理别的任务了。但 commit 消息可能会在传输途中丢失,从而消息中间件并不会向系统 B 投递这条消息,从而造成数据不一致。这个问题由消息中间件的事务会查机制完成。
  • 消息中间件收到 Commit 指令后,便向系统 B 投递该消息,从而触发任务 B 的执行
  • 当任务 B 执行完成后,系统 B 向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时这个分布式事务完成。

上述过程可以得出如下几个结论:

  1. 消息中间件扮演着分布式事务协调者的角色。
  2. 系统 A 完成任务 A 后,到任务 B 执行完成之间,会存在一定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是可以接受的,因为系统最终是一致的。

在上述过程中,如果任务 A 处理失败,那么需要进入回滚流程,如下图所示:

  • 若系统 A 在处理任务 A 时失败,那么就会向消息中间件发送 Rollback 请求,和发送 Commit 请求一样,系统 A 发完之后便可以任务回滚已经完成,它便可以去做其他事情。
  • 消息中间件收到回滚请求后,直接讲消息丢弃,而不投递给系统 B,从而不会触发系统 B 的任务 B。

上面所介绍的 Commit 和 Rollback 都属于理想情况,但在实际系统中,Commit 和 Rollback 指令都有可能在传输中丢失。那么当出现这种情况的时候,消息中间件是如何保证数据一致性的呢?– 答案是超时询问机制

系统 A 除了实现正常的业务流程外,还需要提供一个事务询问的接口,供消息中间件调用。当消息中间件收到一条事务型消息后便开始计时,如果到了超时时间也没收到系统 A 发送过来的 Commit 或 Rollback 指令的话,就会主动调用系统 A 提供的事务询问接口询问该消息目前的状况。该接口会返回三种结果:

  • 提交:若获得的状态是提交,则将消息投递给系统 B
  • 回滚:若获得的状态是回滚,则直接讲该条消息丢弃
  • 处理中:若获得的状态时处理中,则继续等待

当上有系统执行完任务并向消息中间件提交了 Commit 指令后,便可以处理其他任务了,此时他可以任务事务已经完成,消息中间件会保证下游系统一定会消费掉消息。

消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成之后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕。

如果消息在投递过程中丢失,或消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功为止。重试的次数可以设置,比如说重试三次失败后将其放入死信队列,进行人工干预。

参考文章

  1. 分布式系统的事务处理
  2. 常用的分布式事务解决方案
  3. 消息事务的 demo