写点什么

《我想进大厂》之分布式事务篇

用户头像
艾小仙
关注
发布于: 2021 年 01 月 18 日

对于分布式事务,相信所有人都应该很了解,为什么会有分布式事务?无论是数据量导致的分库,还是现在微服务盛行的场景都是他出现的原因。


这一篇内容还是避免不了俗套,主要的范围无非是 XA、2PC、3PC、TCC,再最后到 Seata。


但是,我认为这东西,只是适用于面试和理论的了解,你真要说这些方案实际生产中有人用吗?


有,但是会实现的更简单,不会套用理论来实现,大厂有大厂的解决方案,中小公司用框架或者压根就不存在分布式事务的问题。


那,为什么还要写这个?


为了你面试八股文啊,小可爱。


事务


要说分布式事务,首先还是从事务的基本特征说起。


A 原子性:在事务的执行过程中,要么全部执行成功,要么都不成功。


C 一致性:事务在执行前后,不能破坏数据的完整性。一致性更多的说的是通过 AID 来达到目的,数据应该符合预先的定义和约束,由应用层面来保证,还有的说法是 C 是强行为了 ACID 凑出来的。


I 隔离性:多个事务之间是互相隔离的,事务之间不能互相干扰,涉及到不同事务的隔离级别的问题。


D 持久性:一旦事务提交,数据库中数据的状态就应该是永久性的。


XA


XA(eXtended Architecture)是指由 X/Open 组织提出的分布式事务处理的规范,他是一个规范或者说是协议,定义了事务管理器 TM(Transaction Manager),资源管理器 RM(Resource Manager),和应用程序。


事务管理器 TM 就是事务的协调者,资源管理器 RM 可以认为就是一个数据库。



2PC


XA 定义了规范,那么 2PC 和 3PC 就是他的具体实现方式。


2PC 叫做二阶段提交,分为投票阶段和执行阶段两个阶段。


投票阶段


TM 向所有的参与者发送 prepare 请求,询问是否可以执行事务,等待各个参与者的响应。


这个阶段可以认为只是执行了事务的 SQL 语句,但是还没有提交。


如果都执行成功了就返回 YES,否则返回 NO。



执行阶段


执行阶段就是真正的事务提交的阶段,但是要考虑到失败的情况。


如果所有的参与者都返回 YES,那么就执行发送 commit 命令,参与者收到之后执行提交事务。


反之,只要有任意一个参与者返回的是 NO 的话,就发送 rollback 命令,然后执行回滚的操作。



2PC 的缺陷


  1. 同步阻塞,可以看到,在执行事务的过程当中,所有数据库的资源都被锁定,如果这时候有其他人来访问这些资源,将会被阻塞,这是一个很大的性能问题。

  2. TM 单点问题,只要一个 TM,一旦 TM 宕机,那么整个流程无法继续完成。

  3. 数据不一致,如果在执行阶段,参与者脑裂或者其他故障导致没有收到 commit 请求,部分提交事务,部分未提交,那么数据不一致的问题就产生了。


3PC


既然 2PC 有这么多问题,所以就衍生出了 3PC 的概念,也叫做三阶段提交,他把整个流程分成了 CanCommit、PreCommit、DoCommit 三个步骤,相比 2PC,增加的就是 CanCommit 阶段。


CanCommit


这个阶段就是先询问数据库是否执行事务,发送一个 canCommit 的请求去询问,如果可以的话就返回 YES,反之返回 NO。



PreCommit


这个阶段就等同于 2PC 的投票阶段了,发送 preCommit 命令,然后去执行 SQL 事务,成功就返回 YES,反之返回 NO。



但是,这个地方的区别在于参与者有了超时机制,如果参与者超时未收到 doCommit 命令的话,将会默认去提交事务。


DoCommit


DoCommit 阶段对应到 2PC 的执行阶段,如果上一个阶段都是收到 YES 的话,那么就发送 doCommit 命令去提交事务,反之则会发送 abort 命令去中断事务的执行。



相比 2PC 的改进


对于 2PC 的同步阻塞的问题,我们可以看到因为 3PC 加入了参与者的超时机制,所以原来 2PC 的如果某个参与者故障导致的同步阻塞的问题时间缩短了,这是一个优化,但是并没有完全避免。


第二个单点故障的问题,同样因为超时机制的引入,一定程度上也算是优化了。


但是数据不一致的问题,这个始终没有得到解决。


举个栗子:


在 PreCommit 阶段,某个参与者发生脑裂,无法收到 TM 的请求,这时候其他参与者执行 abort 事务回滚,而脑裂的参与者超时之后继续提交事务,还是有可能发生数据不一致的问题。


那么,为什么要加入 DoCommit 这个阶段呢?就是为了引入超时机制,事先我们先确认数据库是否都可以执行事务,如果都 OK,那么才会进入后面的步骤,所以既然都可以执行,那么超时之后说明发生了问题,就自动提交事务。


TCC


TCC 的模式叫做 Try、Confirm、Cancel,实际上也就是 2PC 的一个变种而已。


实现这个模式,一个事务的接口需要拆分成 3 个,也就是 Try 预占、Confirm 确认提交、最后 Cancel 回滚。


对于 TCC 来说,实际生产我基本上就没看见过有人用,考虑到原因,首先是程序员的本身素质参差不齐,多个团队协作你很难去约束别人按照你的规则来实现,另外一点就是太过于复杂。


如果说有简单的应用的话,库存的应用或许可以算做是一个。


一般库存的操作,很多实现方案里面都会会在下单的时候先预占库存,下单成功之后再实际去扣减库存,最终如果发生了异常再回退。



冻结、预占库存就是 2PC 的准备阶段,真正下单成功去扣减库存就是 2PC 的提交阶段,回滚就是某个发生异常的回滚操作,只不过在应用层面来实现了 2PC 的机制而已。


SAGA


Saga 源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。


主要思想就是将长事务拆分成多个本地短事务。


如果全部执行成功,就正常完成了,反之,则会按照相反的顺序依次调用补偿。


SAGA 模式有两种恢复策略:


  1. 向前恢复,这个模式偏向于一定要成功的场景,失败则会进行重试

  2. 向后恢复,也就是发生异常的子事务依次回滚补偿


由于这个模式在国内基本没看见有谁用的,不在赘述。


消息队列


基于消息队列来实现最终一致性的方案,这个相比前面的我个人认为还稍微靠谱一点,那些都是理论啊,正常生产的实现很少看见应用。


基于消息队列的可能真正在应用的还稍微多一点。


一般来说有两种方式,基于本地消息表和依赖 MQ 本身的事务消息。


本地消息表的这个方案其实更复杂,实际上我也没看到过真正谁来用。这里我以 RocketMQ 的事务消息来举例,这个方式相比本地消息表则更完全依赖 MQ 本身的特性做了解耦,释放了业务开发的复杂工作量。



  1. 业务发起方,调用远程接口,向 MQ 发送一条半事务消息,MQ 收到消息之后会返回给生产者一个 ACK

  2. 生产者收到 ACK 之后,去执行事务,但是事务还没有提交。

  3. 生产者会根据事务的执行结果来决定发送 commit 提交或者 rollback 回滚到 MQ

  4. 这一点是发生异常的情况,比如生产者宕机或者其他异常导致 MQ 长时间没有收到 commit 或者 rollback 的消息,这时候 MQ 会发起状态回查。

  5. MQ 如果收到的是 commit 的话就会去投递消息,消费者正常消费消息即可。如果是 rollback 的话,则会在设置的固定时间期限内去删除消息。


这个方案基于 MQ 来保证消息事务的最终一致性,还算是一个比较合理的解决方案,只要保证 MQ 的可靠性就可以正常实施应用,业务消费方根据本身的消息重试达到最终一致性。


框架


以上说的都是理论和自己实现的方式,那么分布式事务就没有框架来解决我们的问题吗?


有,其实还不少,但是没有能扛旗者出现,要说有,阿里的开源框架 Seata 还有阿里云的 GTS。


GTS(Global Transaction Service 全局事务服务)是阿里云的中间件产品,只要你用阿里云,付钱就可以用 GTS。


Seata(Simple Extensible Autonomous Transaction Architecture)则是开源的分布式事务框架,提供了对 TCC、XA、Saga 以及 AT 模式的支持。


那么,GTS 和 Seata 有什么关系呢?


实际上最开始的时候他们都是基于阿里内部的 TXC(Taobao Transaction Constructor)分布式中间件产品,然后 TXC 经过改造上了阿里云就叫做 GTS。


之后阿里的中间件团队基于 TXC 和 GTS 做出了开源的 Seata,其中 AT(Automatic Transaction)模式就是 GTS 原创的方案。


至于现在的版本,可以大致认为他们就是一样的就行了,到 2020 年,GTS 已经全面兼容了 Seata 的 GA 版本。



整个 GTS 或者 Seata 包含以下几个核心组件:


  • Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

  • Transaction Manager(TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

  • Resource Manager(RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。


无论对于 TCC 还是原创的 AT 模式的支持,整个分布式事务的原理其实相对来说还是比较容易理解。


  1. 事务开启时,TM 向 TC 注册全局事务,并且获得全局事务 XID

  2. 这时候多个微服务的接口发生调用,XID 就会传播到各个微服务中,每个微服务执行事务也会向 TC 注册分支事务。

  3. 之后 TM 就可以管理针对每个 XID 的事务全局提交和回滚,RM 完成分支的提交或者回滚。



AT 模式


原创的 AT 模式相比起 TCC 的方案来说,无需自己实现多个接口,通过代理数据源的形式生成更新前后的 UNDO_LOG,依靠 UNDO_LOG 来实现回滚的操作。


执行的流程如下:


  1. TM 向 TC 注册全局事务,获得 XID

  2. RM 则会去代理 JDBC 数据源,生成镜像的 SQL,形成 UNDO_LOG,然后向 TC 注册分支事务,把数据更新和 UNDO_LOG 在本地事务中一起提交

  3. TC 如果收到 commit 请求,则会异步去删除对应分支的 UNDO_LOG,如果是 rollback,就去查询对应分支的 UNDO_LOG,通过 UNDO_LOG 来执行回滚



TCC 模式


相比 AT 模式代理 JDBC 数据源生成 UNDO_LOG 来生成逆向 SQL 回滚的方式,TCC 就更简单一点了。


  1. TM 向 TC 注册全局事务,获得 XID

  2. RM 向 TC 注册分支事务,然后执行 Try 方法,同时上报 Try 方法执行情况

  3. 然后如果收到 TC 的 commit 请求就执行 Confirm 方法,收到 rollback 则执行 Cancel



XA 模式


  1. TM 向 TC 注册全局事务,获得 XID

  2. RM 向 TC 注册分支事务,XA Start,执行 SQL,XA END,XA Prepare,然后上报分支执行情况

  3. 然后如果收到 TC 的 commit 请求就执行 Confirm 方法,收到 rollback 则执行 Cancel



SAGA 模式


  1. TM 向 TC 注册全局事务,获得 XID

  2. RM 向 TC 注册分支事务,然后执行业务方法,并且上报分支执行情况

  3. RM 收到分支回滚,执行对应的业务回滚方法



总结


这里从事务的 ACID 开始,向大家先说了 XA 是分布式事务处理的规范,之后谈到 2PC 和 3PC,2PC 有同步阻塞、单点故障和数据不一致的问题,3PC 在一定程度上解决了同步阻塞和单点故障的问题,但是还是没有完全解决数据不一致的问题。


之后说到 TCC、SAGA、消息队列的最终一致性的方案,TCC 由于实现过于麻烦和复杂,业务很少应用,SAGA 了解即可,国内也很少有应用到的,消息队列提供了解耦的实现方式,对于中小公司来说可能是较为低成本的实现方式。


最后再说目前国内的实现框架,云端阿里云的 GTS 兼容 Seata,非云端使用 Seata,它提供了 XA、TCC、AT、SAGA 的解决方案,可以说是目前的主流选择。


发布于: 2021 年 01 月 18 日阅读数: 48
用户头像

艾小仙

关注

公众号:艾小仙。阿里P7,编程修仙 2020.09.01 加入

公众号:科技缪缪

评论

发布
暂无评论
《我想进大厂》之分布式事务篇