导读
在上一篇文章《【分布式事务】基于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-两阶段提交协议中会遇到的一些问题:
-
性能问题。从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态。各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。