面试官:聊一下分布式事务!

441 阅读14分钟

概要

在微服务架构盛行的情况下,在分布式的多个服务中保证业务的一致性,即分布式事务就显得尤为重要。本文将讲述分布式事务及其解决方案,有XA协议、TCC和Saga事务模型、本地消息表、事务消息和阿里开源的Seata。

分布式事务

聊什么是分布式事务前,先聊一下我们熟悉的单机事务。所谓单机事务是相对分布式事务来说的,即数据库事务。大家都知道数据库事务有ACID这四个特性:

  • A(Atomicity):指单个事务中的操作要不都执行,要不都不执行
  • C(Consistency):指事务前后数据的完整性必须保持一致
  • I(Isolation):指多个事务对数据可见性的规则
  • D(Durability):指事务提交后,就会被永久存储下来

既然数据库事务有这四个特性的,那么分布式事务也不例外,应该具备这四个特性。

在微服务架构下,服务之间通过RPC远程调用,相对单机事务来说,多了“网络通信”这一不确定因素,使得本来服务的调用只有“成功”和“失败”这两种返回结果,变为“成功”、“失败”和“未知”三种返回结果。系统之间的通信可靠性从单一系统中的可靠变成了微服务架构之间的不可靠,分布式事务其实就是在不可靠的通信下实现事务的特性。一般因为网络导致的异常可能有机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失、其他异常等等。

分布式事务方案

2PC/3PC

2PC即二阶段提交) :

二阶段提交(英语:Two-phase Commit)是指在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议(Protocol)。

2PC是一种协议,它的作用保证在分布式系统中每个节点要不都提交事务,要么都取消事务。这个跟ACID中的A原子性的定义很像。

2PC引入一个第三方的节点协调者,即Coordinator,其他参与事务的节点为参与者,即Participants。协调者统筹整个事务行为,负责通知参与者进行Commit还是Rollback操作。

2PC的过程比较简单,分为两个阶段:

  1. 准备阶段

    协调者分别给每个参与者发送Prepare消息,每个参与者收到消息后,进行“预提交”操作(不是实际的提交操作),把操作的结果(成功或失败)返回给协调者。

  2. 提交阶段

    协调者根据准备阶段收到的参与者的返回结果进行判断,如果所有的参与者都返回成功,那么分别给每个参与者发送Commit消息,否则发送Rollback消息。

2PC是一个强一致性协议,同时它在实际应用中还存在几个问题

  • 同步阻塞,2PC的两个阶段中,协调者和参与者的通信都是同步的,这会导致整个事务的长时间阻塞
  • Coordinator的单点问题
  • 数据不一致,在Commit阶段,可能存在只有部分参与者收到Commit消息(或处理成功)的情况

3PC

3PC即三阶段提交,它比2PC多了一个阶段,即把原来2PC的准备阶段拆分成CanCommit和PreCommit两个阶段,同时

引入超时机制来解决2PC的同步阻塞问题。

但是在我看来3PC并没有解决2PC的根本问题,它只是在2PC的基础上做了一些优化,它增加了一个阶段(也增加了1个RTT)来提高对方可用性的概率,这本质跟TCP的三次握手一样,同样也改为四次握手,五次握手等等。

XA

XA是一种基于2PC协议实现的规范。在2PC中没有明确资源是什么,以及资源是怎么提交的等等,而XA就是数据库实现2PC的规范,已知常用的支持XA的关系型数据库有Mysql、Oracle等。

本地消息表

本地消息表方案应该是业界内使用最为广泛的,因为它使用简单,成本比较低。

本地消息表的方案最初是由 eBay 提出(完整方案),核心思路是将分布式事务拆分成本地事务进行处理。

它的处理流程如下:

  • 事务发起方把要处理的业务事务和写消息表这两个操作放在同一个本地事务里
  • 事务发起方有一个定时任务轮询消息表,把没处理的消息发送到消息中间件
  • 事务被动方从消息中间件获取消息后,返回成功
  • 事务发起方更新消息状态为已成功

从处理流程来看,本地消息表方案是一个基于消息中间件的可靠性来达到事务的最终一致性的方案。

一些分析:

  • 把业务处理和写消息表放在同一个事务是为了失败/异常后可以同时回滚

  • 为什么不直接发消息,而是先写消息表?

    试想,如果发送消息超时了,即不确定消息中间件收到消息没,那么你是重试还是抛异常回滚事务呢?回滚是不行的,因为可能消息中间件已经收到消息,接收方收到消息后做处理,导致双方数据不一致了;重试也是不行的,因为有可能会一直重试失败,导致事务阻塞。

  • 基于上述分析,消息的接收方是需要做幂等操作的

本地消息表方案整体来说还是比较简单、可用的,但是也有以下缺点:

  • 消息数据和业务数据耦合,消息表需要根据具体的业务场景制定,不能公用。就算可以公用消息表,对于分库的业务来说每个库都是需要消息表的。
  • 只适用于最终一致的业务场景。例如在 A -> B场景下,在不考虑网络异常、宕机等非业务异常的情况下,A成功的话,B肯定也会成功的。

事务消息

事务消息是通过消息中间件来解耦本地消息表和业务数据表,适用于所有对数据最终一致性需求的场景。现在支持事务消息的消息中间件只有RocketMQ,这个概念最早也是RocketMQ提出的。

通过事务消息实现分布式事务的流程如下:

  1. 发起方发送半事务消息会给RocketMQ ,此时消息的状态prepare,接受方还不能拉取到此消息
  2. 发起方进行本地事务操作
  3. 发起方给RocketMQ确认提交消息,此时接受方可以消费到此消息了

步骤1和3失败/异常该如何处理:

RocketMQ会定期扫描还没确认的消息,回调给发送方,询问此次事务的状态,根据发送方的返回结果把这条消息进行取消还是提交确认。

可以看出事务消息的本质的借鉴了二阶段提交的思想,它跟本地消息表的做法也很像,事务消息做的事情其实就是把消息表的存储和扫描消息表这两个事情放到消息中间件来做,使得消息表和业务表解耦。

TCC

TCC (Try-Confirm-Cancel)事务模型采用的是补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿操作。

相当于XA来说,TCC可以不依赖于资源管理器,即数据库,它是通过业务逻辑来控制确认和补偿操作的,所以它用了’Cancel’而非’Rollback’的字眼。它是一个应用层面的2PC。

TCC分为三个阶段:

  • Try阶段,对业务资源进行检测和预留
  • Confirm阶段,对Try阶段预留的资源进行确认提交,Try阶段执行成功是Confirm阶段执行成功的前提
  • Cancel阶段,对Try阶段预留的资源进行撤销或释放

看上去TCC跟2PC/3PC可能有点像,但是TCC强调的是补偿,而且对于对资源的“预留”,“确认”,“释放”,TCC并没有明确说要如何做,这个具体是要业务来定义的。

例如在转账的场景,“预留”操作可能就是对账号里的部分资金进行冻结,这样这个资金只能是当前事务才能用,别的事务用不了。

另外,对于异常的场景,TCC也没有说要怎么做,因为Try、Confirm、Cancel都是业务定义的,这三个阶段中发生了异常,那么就由业务来做相应的处理。一般都有以下几种处理:

  • 如果Try成功了,那么Confirm阶段异常了就一直重试,直到成功
  • Try、Confirm、Cancel三个阶段都有相应的资源及事务日志,应用根据日志(异步)来做重试或补偿
  • TCC的实现依赖底层数据库,异常后直接利用数据库的事务机制回滚

其中现在使用比较多的TCC框架ByteTCC、tcc-transaction的原理都是基于第三点

同时,在实现TCC时要注意以下三个问题

  • 允许空回滚

    在Try没有真正执行的情况下,触发了Cancel操作,这时要允许Cancel成功

  • 防悬挂控制

    Cancel操作比Try操作先执行(网络延迟原因),后面的Try操作不能执行成功

  • 幂等控制

TCC其实是把控制事务的逻辑放在业务应用层面,而非资源管理器,这样实现起来就会相对灵活很多,但相对对数据一致性的保证可能没那么强(具体看怎么实现Try),整体来说TCC还有以下缺点:

  • 对于Confirm和Cancel阶段失败后要完全靠业务应用自己去处理
  • 每个业务都需要实现Try、Confirm、Cancel三个接口,代码量比较多
  • 如果是基于现有的业务想使用TCC会比较困难。一是对于原来的接口要拆分为三个接口,入侵性比较大;二是因为要做“预留”资源的操作,有可能需要对原来的业务模型进行改造。

Saga

Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。Saga的论文。

该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Saga工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Saga工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。

Saga也是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

你可以看到Saga跟TCC很像,但是Saga更加宽松,一致性更弱,在Saga看来,在一阶段直接做提交/确认操作就好了,有问题再做补偿。这样的话,Saga可以拥有比XA和TCC更好的性能(XA、TCC需要锁定资源或预留资源),而且Saga强调通过事件驱动异步处理,实现高吞吐。

可以看出Saga是对TCC的一种“妥协”,从TCC的三个接口变为两个接口,一阶段直接提交缺少对资源的隔离(如果一阶段提交后,后面发现需要做补偿,但是补偿操作执行前有另外的事务更改了数据,这时数据已经变“脏”了,那么这时该如何处理是一个问题。在TCC没有这个问题,因为资源已经被hold住了),因此对使用者也是比较宽松的,对于现有业务的改造也会比较简单。

Saga实现分两种,一种是Saga状态机实现,一种是Saga AOP Proxy实现。Saga状态机实现,在关于参与者服务编排实现又有集中式和协同式两种分支。这点就不展开了。

TCC vs Saga

TCC和Saga都属于补偿型事务模型,Saga没有Try,直接Commit,所有会产生实际的事务痕迹,而补偿做的是反向操作。TCC是二阶段的广义实现,利用了数据的中间态,Cancel是中间状态的数据进行撤销,从而不存在数据污染问题。

使用场景对比:

  • TCC 适用于执行时间确定且较短、对一致性要求比较高、数据隔离强的业务
  • Saga 适用于业务流程长、业务流程多的业务,在银行业金融机构使用广泛
  • TCC 对现有业务改造较大,Saga则相对少点

Seata

Seata是一个由阿里做背书的分布式事务框架,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

AT模式

AT模式是Seata通过拦截、解释用户的SQL,对业务数据进行加锁、回滚等操作的基于二阶段协议的一个实现。

它的特点是对业务无入侵,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

TCC模式

Seata的TCC模式跟上面讲的TCC事务模型差不多

Saga模式

Saga模式也是上面讲的Saga事务模型差不多。在Seata中对服务的编排引入了状态机引擎, 使得对业务流程的定义更加标准化,提高可读性,不过相对来说配置会比较复杂繁琐。同时支持注解的方式,这个在开发上会简单一点,但功能可能少一点。

分布式事务一致性与Paxos一致性的思考

首先要明确一点的就是对于上述提到的分布式事务解决方案,如TCC、Saga、本地消息表等,其本质都是2PC。

Paxos算法解决的问题是一个分布式系统如何就某个值(决议)达成一致。

咋看起来2PC和Paxos都是解决关于“一致性”的问题,其实细想它们解决的问题不在一个层面。

2PC要求分布式系统中的每个节点要不全部成功,要不全部失败,强调的是原子性。

Paxos要求多个副本之间的数据一致性,其实这里用“一致性”并不准确,应该用“共识(Consensus)”才对。

例如2PC中的协调者单点的问题可以用Paxos算法通过选举出新的协调者来解决。

总结

总得看来,分布式事务的解决方案都很难做到有高一致性的同时,也有高性能,同时在实现上也有一定的难度。在业务允许的情况下,我们通常处理分布式事务的一般原则应是:业务规避 > 最终一致 > 强一致。