分布式事务解决方案概览


在分布式系统中,很容易产生分布式事务问题。事务指的就是一个操作单元,在一个操作单元中要使所有操作保持一致,简言之就是要么是全部成功,要么是全部失败。

本来在单体应用、单个数据库的系统中,依靠像MySQL这种数据库强大的事务机制是很容易保证一个操作单元中的操作都保持一致的。

不过在分布式系统下,要完成一项业务功能,一般会调用多个服务并且还可能操作多个数据库,在这样的情况下要保证本次操作下的行为全部都成功或者全部失败,显得有些困难。

幸而,在无数先贤前辈的不懈探索过程中,也产生了一些关于分布式系统中保证数据一致性的解决方案。下面就粗略来看看业内主流的解决方案。

二阶段提交(2PC)

二阶段提交(Two-Phase commit),即2PC。顾名思义,是把整个事务单元分为两个阶段处理,二阶段提交流程如图1和图2所示。
1.png

图1 正常的二阶段提交流程

2.png

图2 二阶段提交出现异常的情况
  • 阶段一:表决阶段,协调者将事务信息发送给各参与者,然后各参与者接受到事务请求后向协调者反馈自身是否有能力执行事务提交并记录undo和redo日志。
  • 阶段二:执行阶段,协调者接受到各参与的反馈后,再通知各参与进行真正事务提交或者回滚,如果各参与者收到回滚,则会根据undo日志执行回滚操作。


下面来看看二阶段提交的优缺点。

二阶段提交的优点:
  • 提高了达到数据一致性的可能性,原理简单,实现成本低。


二阶段提交的缺点:
  • 单点问题,如果协调者宕机,整个流程将不可用。
  • 性能问题,在第一阶段,要等待所有节点反馈,才能进入第二个阶段。
  • 数据不一致,在执行阶段时,如果协调者发送崩溃,导致只有部分参与者收到提交的消息,那么就会存在数据不一致。


三阶段提交(3PC)

三阶段提交(Three-Phasecommit),即3PC。它是二阶段提交的改进版,三阶段提交流程如图3所示。
3.png

图3 三阶段提交流程

三阶段提交和两阶段提交有所不同,主要有两个改动点。
  • 引入超时机制。
  • 插入一个准备阶段,由此三阶段提交分为CanCommit、PreCommit、DoCommit三个阶段。


下面来看CanCommit、PreCommit、DoCommit三个阶段具体内容。

CanCommit阶段:协调者向各参与者发送CanCommit请求,询问是否可以执行事务提交操作,各参与者会响应Yes或No,如果是全部响应Yes,则会进入下一个阶段。

PreCommit阶段:协调者向各个参与者发送PreCommit请求,进入Prepared阶段。各参与者接到PreCommit请求后,执行事务操作,并将undo和redo信息记录到事务日志中。如果都执行成功,则向协调者反馈成功指令,并等待协调者的下一次请求。

另一种情况,假设任何一个参与者在PreCommit阶段向协调者反馈了失败,或者等待超时,协调者没有收到全部参与者的反馈,那么执行事务中断;协调者向各参与者发送abort请求,参与者收到abort指令后执行事务中断的操作。

DoCommit阶段:前两个阶段各参与者均反馈成功后,协调者再向各参与者发送DoCommit请求真正提交事务,各参与者收到DoCommit请求之后,执行正式的事务提交。参与者完成事务提交后,向协调者反馈成功,协调者收到所有参与者成功反馈后,完成事务。

另一种情况,假设何一个参与者在DoCommit阶段向协调者反馈了失败,或者等待超时,协调者没有收到全部参与者的反馈,协调者同样会向各参与者发送abort请求。参与者收到abort指令后,根据已记录的undo信息来执行事务的回滚,回滚后释放事务资源并向协调者反馈信息,协调者收到各参与者反馈后,完成事务中断。

下面来看三阶段提交的优缺点:

三阶段提交引入超时机制后,能一定程度上解决单点问题,并减少阻塞。因为一旦参与者无法与协调者通信时,它会默认执行commit,而不会一直是阻塞状态。

但是这种机制的缺陷也很明显,可能在极端情况下导致数据不一致。假设这样的场景,由于网络原因,协调者发送的abort指令部分参与者并没有收到,那么这部分参与者在等待超时之后就会执行commit操作,从而导致这部分参与者与其他接收到abort指令的参与者数据不一致。

保证最终一致性

所谓的保证最终一致性,就是两个系统数据副本同步或者说是系统之间的数据有关联(就是上面的订单和分配派送员一样),保证在一定时间内,最终保证数据一致性。而不是实时保证数据强一致性。

记住一个关键点,一定时间内。也就是说这个可以是异步的。关于异步的概念可能很多读者朋友都能想到消息中间件了。

的确,实现数据最终一致性一般会采用消息中间件来做。消息中间件有着异步、解耦,并且有的消息中间件还支持事务消息,这些特性的确很好用,为实现数据的最终一致性提供了强有力的支撑。

下面就来设想一下,使用消息中间件达到数据最终一致性的系统。这里以前面的订单服务和配送服务为例。

订单系统收到用户下单请求,往订单表插入一条新数据,然后给消息中间件发送一条消息(携带订单id);配送系统作为消息的消费者,会收到这条消息,解析到订单id后,选择配送人,往订单配送表插入一条数据。

整个流程就这样完了吗?

不,整个过程看似完美,实则并非无懈可击。

假设一下这样的场景。在用户下订单时,往订单表插入数据成功了,但是向消息中间件发送消息失败了,订单表数据回滚。请注意,这里的失败,发送方并不知道消息中间件有没有收到消息,有可能是因为网络波动的缘故,导致消息中间件已收到消息,只是返回的response是失败。那么消息就会被配送系统消费,数据同样会不一致。

下面来看一种改良方案,基于RocketMQ事务消息来做。RocketMQ的事务消息可以分为两个阶段。Prepare(消息预发送)和Confirm(确认发送)。
  1. 订单系统系统创建订单时,先调用RocketMQ的Prepare接口,发送预备消息。此时消息暂存在RocketMQ中,但不会给配送系统消费。
  2. 发送预备消息成功后,往订单表插入一条新数据。
  3. 订单数据新增成功后,订单系统调用RocketMQ的Confirm接口,确认发送消息,此时RocketMQ才会把消息给消费者消费。
  4. 消费者也就是配送服务,消费此消息,分配派送员,生成订单配送表的数据。


为了让方案无懈可击,设想下步骤2或者步骤3失败或者超时会怎样呢?

可以确定的是步骤1,发送预备消息是成功的,消息只会暂存到RocketMQ。如果步骤2或者步骤3失败,RocketMQ的机制会定时扫描所有处于预备状态的消息,回调给发送方,由发送方决定该消息是继续发生还是取消。这样消息发送两阶段的设计和回调的机制一定程度上避免了数据不一致的问题。

为什么说是一定程度上避免了数据不一致的问题,因为还有一个问题,如果配送服务(消息消费端)关键时刻宕机了,有可能会重复消费怎么办?

配送服务可以消费消息记录表,消费消息记录表记录消息的ID,如果业务比较复杂,还可以记录下处理到哪个步骤了,下次还来这条消息,业务代码判断一下,直接从上次的位置开始。

所以基于RocketMQ事务消息实现数据最终一致性的流程如图4所示。
4.png

图4 RocketMQ事务消息实现数据最终一致性的流程

如果配送服务收到消息,但是由于其他异常导致数据一直无法插入成功怎么办?

此时可以捕获异常,并且记录未插入成功的数据,可以采取人工干预的手段。

本文节选自《Spring Cloud Alibaba微服务实战》,作者周仲清,本文已获得北京大学出版社转载授权。

0 个评论

要回复文章请先登录注册