导读面试
在上一篇文章《【分布式事务】基于RocketMQ搭建生产级消息集群?》中给你们介绍了基于RocketMQ如何搭建生产级消息集群。由于本系列文章最终的目的是介绍基于RocketMQ的事物消息来解决分布式系统中的数据一致性问题,因此先给你们率先介绍了RocketMQ消息集群的搭建。数据库
本来是想着在这篇文章中直接介绍RocketMQ的事务消息特性,可是在梳理的过程当中做者发现对于分布式事务的概念,可能还会有不少同窗不理解或者理解得不是很深入的地方,而跳过这些基本概念直接去学习上层的实践可能并非一件很好的事情,所以在这篇文章中,做者打算重点给你们先介绍下分布式事务相关的基本概念,诸如分布式事务、2PC、3PC、TCC之类的基本问题,以后再单独去介绍RocketMQ事务消息相关的实践。网络
数据库事务的概念架构
在讲述分布式事务的概念以前,咱们先来回顾下事务相关的一些概念。并发
事务的基本概念:异步
就是一个程序执行单元,里面的操做要么所有执行成功,要么所有执行失败,不容许只成功一半另一半执行失败的事情发生。例如一段事务代码作了两次数据库更新操做,那么这两次数据库操做要么所有执行成功,要么所有回滚。分布式
事务的基本特性:微服务
咱们知道事务有4个很是重要的特性,即咱们常说的(ACID)。性能
Atomicity(原子性):是说事务是一个不可分割的总体,全部操做要么全作,要么全不作;只要事务中有一个操做出错,回滚到事务开始前的状态的话,那么以前已经执行的全部操做都是无效的,都应该回滚到开始前的状态。学习
Consistency(一致性):是说事务执行先后,数据从一个状态到另外一个状态必须是一致的,好比A向B转帐( A、B的总金额就是一个一致性状态),不可能出现A扣了钱,B却没收到的状况发生。
Isolation(隔离性): 多个并发事务之间相互隔离,不能互相干扰。关于事务的隔离性,可能不是特别好理解,这里的并发事务是指两个事务操做了同一份数据的状况;而对于并发事务操做同一份数据的隔离性问题,则是要求不能出现脏读、幻读的状况,即事务A不能读取事务B尚未提交的数据,或者在事务A读取数据进行更新操做时,不容许事务B率先更新掉这条数据。而为了解决这个问题,经常使用的手段就是加锁了,对于数据库来讲就是经过数据库的相关锁机制来保证。
Durablity(持久性):事务完成后,对数据库的更改是永久保存的,不能回滚。
关于数据库事务的基本概念你们能够去网上搜一下,这里只是给你们回顾下事务的基本概念及特性,诸如事务并发问题、事务隔离级别等你们若有遗忘能够去回顾下(tips:面试常常会问到的问题哦)。
什么是分布式事务
以上内容咱们回顾了下事务的基本概念,那么分布式事务又是个什么概念呢?它与数据库事务之间又有什么区别呢?
其实分布式事务从实质上看与数据库事务的概念是一致的,既然是事务也就须要知足事务的基本特性(ACID),只是分布式事务相对于本地事务而言其表现形式有很大的不一样。举个例子,在一个JVM进程中若是须要同时操做数据库的多条记录,而这些操做须要在一个事务中,那么咱们能够经过数据库提供的事务机制(通常是数据库锁)来实现。
而随着这个JVM进程(应用)被拆分红了微服务架构,本来一个本地逻辑执行单元被拆分到了多个独立的微服务中,这些微服务又分别操做不一样的数据库和表,服务之间经过网络调用。
举个例子:服务A收到一笔购物下单请求后,须要调用服务B去支付,支付成功则处理购物订单为待发货状态,不然就须要将购物订单处理为失败状态。(如图所示)
在上面这个例子中会不会出现服务B支付成功了,可是因为网络调用的问题没有通知到服务A,致使用户付了钱,可是购物订单没法显示支付成功的状态呢?
答案是这种状况是广泛存在的,由于服务B在处理成功后须要向服务A发送网络请求,而这个过程是极有可能失败的。那么如何确保“服务A->服务B”这个过程可以组成一个事务,要么所有成功、要么所有失败呢?而这就是典型的须要经过分布式事务解决的问题。
分布式事务是为了解决微服务架构(形式都是分布式系统)中不一样节点之间的数据一致性问题。这个一致性问题本质上解决的也是传统事务须要解决的问题,即一个请求在多个微服务调用链中,全部服务的数据处理要么所有成功,要么所有回滚。固然分布式事务问题的形式可能与传统事务会有比较大的差别,可是问题本质是一致的,都是要求解决数据的一致性问题。
而分布式事务的实现方式有不少种,最具备表明性的是由Oracle Tuxedo系统提出的XA分布式事务协议。XA协议包括两阶段提交(2PC)和三阶段提交(3PC)两种实现,接下来咱们分别来介绍下这两种实现方式的原理。
两阶段提交(2PC)
两阶段提交又称2PC(two-phase commit protocol),2pc是一个很是经典的强一致、中心化的原子提交协议。这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点(coordinator)和N个参与者节点(partcipant)。
下面咱们就以一个尽可能贴近实际业务场景的操做来举例:"假设在一个分布式架构的系统中事务的发起者经过分布式事务协调者(如RocketMQ,在早期RocketMQ版本不提供事务消息特性时,有些公司会本身研发一个基于MQ的可靠消息服务来实现必定的分布式事务的特性)分别向应用服务A、应用服务B发起处理请求,两者在处理的过程当中会分别操做自身服务的数据库,如今要求应用服务A、应用服务B的数据处理操做要在一个事务里"?
在上面这个例子中若是采用两阶段提交来实现分布式事务,那么其运行原理应该是个什么样的呢?(如👇):
第一阶段:请求/表决阶段(点击放大)
既然称为两阶段提交,说明在这个过程当中是大体存在两个阶段的处理流程。第一个阶段如👆图所示,这个阶段被称之为请求/表决阶段。是个什么意思呢?
就是在分布式事务的发起方在向分布式事务协调者(Coordinator)发送请求时,Coordinator首先会分别向参与者(Partcipant)节点A、参与这节点(Partcipant)节点B分别发送事务预处理请求,称之为Prepare,有些资料也叫"Vote Request"。
说的直白点就是问一下这些参与节点"这件事大家能不能处理成功了",此时这些参与者节点通常来讲就会打开本地数据库事务,而后开始执行数据库本地事务,但在执行完成后并不会立马提交数据库本地事务,而是先向Coordinator报告说:“我这边能够处理了/我这边不能处理”。
若是全部的参与这节点都向协调者做了“Vote Commit”的反馈的话,那么此时流程就会进入第二个阶段了。
第二阶段:提交/执行阶段(正常流程)
若是全部参与者节点都向协调者报告说“我这边能够处理”,那么此时协调者就会向全部参与者节点发送“全局提交确认通知(global_commit)”,即大家均可以进行本地事务提交了,此时参与者节点就会完成自身本地数据库事务的提交,并最终将提交结果回复“ack”消息给Coordinator,而后Coordinator就会向调用方返回分布式事务处理完成的结果。
第二阶段:提交/执行阶段(异常流程)
相反,在第二阶段除了全部的参与者节点都反馈“我这边能够处理了”的状况外,也会有节点反馈说“我这边不能处理”的状况发生,此时参与者节点就会向协调者节点反馈“Vote_Abort”的消息。此时分布式事务协调者节点就会向全部的参与者节点发起事务回滚的消息(“global_rollback”),此时各个参与者节点就会回滚本地事务,释放资源,而且向协调者节点发送“ack”确认消息,协调者节点就会向调用方返回分布式事务处理失败的结果。
以上就是两阶段提交的基本过程了,那么按照这个两阶段提交协议,分布式系统的数据一致性问题就能获得知足吗?
实际上分布式事务是一件很是复杂的事情,两阶段提交只是经过增长了事务协调者(Coordinator)的角色来经过2个阶段的处理流程来解决分布式系统中一个事务须要跨多个服务节点的数据一致性问题。可是从异常状况上考虑,这个流程也并非那么的无懈可击。
假设若是在第二个阶段中Coordinator在接收到Partcipant的"Vote_Request"后挂掉了或者网络出现了异常,那么此时Partcipant节点就会一直处于本地事务挂起的状态,从而长时间地占用资源。固然这种状况只会出如今极端状况下,然而做为一套健壮的软件系统而言,异常Case的处理才是真正考验方案正确性的地方。
如下几点是XA-两阶段提交协议中会遇到的一些问题:
性能问题。从流程上咱们能够看得出,其最大缺点就在于它的执行过程当中间,节点都处于阻塞状态。各个操做数据库的节点此时都占用着数据库资源,只有当全部节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
协调者单点故障问题。事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会致使参与者收不到提交或回滚的通知,从而致使参与者节点始终处于事务没法完成的中间状态。
丢失消息致使的数据不一致问题。在第二个阶段,若是发生局部网络问题,一部分事务参与者收到了提交消息,另外一部分事务参与者没收到提交消息,那么就会致使节点间数据的不一致问题。
既然两阶段提交有以上问题,那么有没有其余的方案来解决呢?
三阶段提交(3PC)
三阶段提交又称3PC,其在两阶段提交的基础上增长了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。
可是性能问题和不一致问题仍然没有根本解决。下面咱们仍是一块儿看下三阶段流程的是什么样的?
第一阶段:CanCommit阶段
这个阶段相似于2PC中的第二个阶段中的Ready阶段,是一种事务询问操做,事务的协调者向全部参与者询问“大家是否能够完成本次事务?”,若是参与者节点认为自身能够完成事务就返回“YES”,不然“NO”。而在实际的场景中参与者节点会对自身逻辑进行事务尝试,其实说白了就是检查下自身状态的健康性,看有没有能力进行事务操做。
第二阶段:PreCommit阶段
在阶段一中,若是全部的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。此时分布式事务协调者会向全部的参与者节点发送PreCommit请求,参与者收到后开始执行事务操做,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操做后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
不然,若是阶段一中有任何一个参与者节点返回的结果是No响应,或者协调者在等待参与者节点反馈的过程当中超时(2PC中只有协调者能够超时,参与者没有超时机制)。整个分布式事务就会中断,协调者就会向全部的参与者发送“abort”请求。
第三阶段:DoCommit阶段
在阶段二中若是全部的参与者节点均可以进行PreCommit提交,那么协调者就会从“预提交状态”-》“提交状态”。而后向全部的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操做,并向协调者节点反馈“Ack”消息,协调者收到全部参与者的Ack消息后完成事务。
相反,若是有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向全部的参与者节点发送abort请求,从而中断事务。
看到这里,你是否是会疑惑"3PC相对于2PC而言到底优化了什么地方呢?"
相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢?这个优化点,主要是避免了参与者在长时间没法与协调者节点通信(协调者挂掉了)的状况下,没法释放资源的问题,由于参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面下降了整个事务的阻塞时间和范围。
另外,经过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段以前各参与节点的状态是一致的。
以上就是3PC相对于2PC的一个提升(相对缓解了2PC中的前两个问题),可是3PC依然没有彻底解决数据不一致的问题。
补偿事务(TCC)
提及分布式事务的概念,很多人都会搞混淆,彷佛好像分布式事务就是TCC。实际上TCC与2PC、3PC同样,只是分布式事务的一种实现方案而已。
TCC(Try-Confirm-Cancel)又称补偿事务。其核心思想是:"针对每一个操做都要注册一个与其对应的确认和补偿(撤销操做)"。它分为三个操做:
Try阶段:主要是对业务系统作检测及资源预留。
Confirm阶段:确认执行业务操做。
Cancel阶段:取消执行业务操做。
TCC事务的处理流程与2PC两阶段提交相似,不过2PC一般都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,须要经过业务逻辑来实现。这种分布式事务的实现方式的优点在于,可让应用本身定义数据库操做的粒度,使得下降锁冲突、提升吞吐量成为可能。
而不足之处则在于对应用的侵入性很是强,业务逻辑的每一个分支都须要实现try、confirm、cancel三个操做。此外,其实现难度也比较大,须要按照网络状态、系统故障等不一样的失败缘由实现不一样的回滚策略。为了知足一致性的要求,confirm和cancel接口还必须实现幂等。
TCC的具体原理图如👇:
消息队列MQ事务
在前面介绍2PC、3PC的时候咱们说没有根本解决性能问题,而若是经过MQ的事务消息来进行异步解耦,并实现系统的数据的最终一致性的话会不会好不少呢?实际上这就是咱们下一篇文章要继续讲述的《分布式事务之如何基于RocketMQ的事务消息特性实现分布式系统的最终一致性?》。敬请期待!