写在前面

本文隶属于专栏《100个问题搞定大数据理论体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和文献引用请见100个问题搞定大数据理论体系

解答

一次大的操作由不同的小操作组成的,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
从本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

实现方式:
1.2PC
2.本地消息表
3.3PC
4.TCC
5.消息事务+最终一致性

补充

2PC

两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终決定这些参与者是否要真正执行事务。

运行过程

第一阶段——准备

协调者询问参与者事务是否执行成功,参与者发回事务执行结果。询问可以看成种投票,需要参与者都同意才能执行。

第二阶段——提交

如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务; 否则,协调者发送通知让参与者回滚事务需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

2PC

存在的问题

  1. 同步阻塞所有事务参与者在等待其它参与者响应的时候都处于同步阻塞等待状态,无法进行其它操作。
  2. 单点问题协调者在2PC中起到非常大的作用,发生故障将会造成很大影响。特别是在提交阶段发生故障,所有参与者会一直同步阻塞等待,无法完成其它操作。
  3. 数据不一致在提交阶段,如果协调者只发送了部分Commit消息,此时网络发生异常,那么只有部分参与者接收到Commit消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  4. 太过保守任意一个节点失败就会导致整个事务失败,没有完善的容错机制。

本地消息表

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

  1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
  2. 之后将本地消息表中的消息转发到消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
  3. 在分布式事务操作的另ー方从消息队列中读取一个消息,并执行消息中的操作。

3PC

3PC其实在2PC的基础上增加了CanCommit阶段,是2PC的变种,并引入了超时机制。
一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效的解决了协调者单点故障的问题。
但是,性能和数据一致性问题没有根本解决。

3PC分为三个阶段:CanCommit、PreCommit、DoCommit

CanCommit阶段

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

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

PreCommit阶段

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

  1. 假如协调者从所有的参与者获得的反馈都是Yes,那么就会执行事务的与执行。
    发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
    事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
    响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
  2. 假如有任何一个参与者向协调者发送了No响应,或者等待超时,或者协调者都没有接到参与者的响应,那么就执行事务的中断。
    发送中断请求:协调者向所有参与者发送abort请求。
    中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

DoCommit阶段

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

执行提交

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

中断事务

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

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

3PC

存在的问题

相对于2PC而言,3PC对于协调者和参与者都设置了超时时间,而2PC只有协调者才拥有超时时间机制。
这个优化解决了,参与者在长时间无法与协调者节点通讯的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。
而这种机制也侧面降低了整个事务的阻塞时间和范围。但是仍然没有解决数据一致性问题,即在参与者收到PreCommit请求后等待最终指令,如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

补偿事务(TCC)

TCC(Try-Confirm-Cancel)又称补偿事务。它实际上与2PC、3PC一样,都是分布式事务的一种实现方案而已。它分为三个操作:

Try阶段:主要是对业务系统做检测及资源预留。
Confirm阶段:确认执行业务操作。
Cancel阶段:取消执行业务操作。
TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在DB层面,而TCC本质上就是应用层面的2PC,需要通过业务逻辑来实现。它的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提交吞吐量。

不过对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。

TCC

消息事务+最终一致性

所谓的消息事务就是基于消息中间件的两阶段提交,本质上是中间件的一种特殊利用,他是将本地事务和发消息放在一个分布式事务里,保证要么本地操作成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:

消息事务

实现步骤

  1. 服务A向消息中间件发送一条预备消息。
  2. 消息中间件保存预备消息并返回成功。
  3. 服务A执行本地事务。
  4. 服务A发送提交消息给消息中间件,服务B接收到消息之后执行本地事务。

基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(服务A的本地操作+发消息)+服务B的本地操作,其中服务B的操作由消息驱动,只要消息事务成功,那么服务A一定成功,消息也一定发出来了,这时候服务B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到服务B操作成功,这样就变相地实现了A与B的分布式事务。

异常情况分析

  1. 步骤一出错:则整个事务失败,不会执行服务A的本地操作。
  2. 步骤二出错:则整个事务失败,不会执行服务A的本地操作。
  3. 步骤三出错:需要做回滚预备消息,由服务A实现一个消息中间件的回调接口,消息中间件会不断执行回调接口,检查服务A事务执行是否执行成功,如果失败则回滚预备消息。
  4. 步骤四出错:这个时候服务A的本地事务是成功的,但是消息中间件不需要回滚,其实通过回调接口,消息中间件能够检查到服务A执行成功了,这个时候其实不需要服务发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务。

Q.E.D.


Apache Spark Contributor