zhaojp0 2019-12-19
事务是数据库从一个稳定状态变迁到另一个稳定状态的保证,具备 ACID 这 4 个特性:
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态。
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性限制没有被破坏。
隔离性(Isolation):两个事务的执行是互不干扰的,两个事务时间不会互相影响。
例如应用程序需要更新多条相关数据时就需要进行事务处理。
当遇到复杂业务调用时,可能会出现跨库多资源调用(一个事务管理器,多个资源)/多服务调用(多个事务管理器,多个资源),期望全部成功或失败回滚,这就是分布式事务,用以保证“操作多个隔离资源的数据一致性”。
分布式事务是指会涉及到操作多个数据库的事务,同样必须保证 ACID。其就是将对同一库事务的概念扩大到了对多个库的事务:对同一库的 SQL 操作对应了分布式事务中对一个库的事务。
X/Open XA 定义了分布式事务处理的规范,并由数据库厂商在驱动层面进行实现。XA 规范的基础是两阶段提交协议,并定义了分布式事务处理所涉及的角色:
应用程序(AP)
事务管理器(TM)
资源管理器(RM)
通信资源管理器(CRM)
常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库,常见的通信资源管理器( CRM )是消息中间件。
可以这样认为,事务管理器即事务处理中间件(分布式事务处理系统);资源管理器即各个数据库。
两阶段提交协议(Two-phase commit protocol, 2PC)将一次分布式事务处理划分为两个阶段:预提交阶段(也称为准备阶段或投票阶段),提交阶段。
1 预提交阶段
这是两阶段提交协议的第一个阶段,分布式事务处理系统咨询各个资源管理器是否可以提交本地事务,各个资源管理器会把这个咨询过程写入日志,以便进行回滚或提交。
当一个数据库接收到咨询后,它会将需要执行的操作写入日志,禁止其他写入操作(锁定资源)。
如果分布式事务中某数据库预提交失败或提交失败,那该数据库会根据日志进行自身的操作回滚,并解锁。
2 提交阶段
分布式事务处理系统对各个资源管理器下达提交/回滚的指令,使整个分布式事务结束。
当一个数据库接受到提交/回滚指令时,它将根据第一阶段的日志进行提交/回滚处理。
两阶段提交协议可以在数据库层面通过驱动支持,也可以在应用框架中按照其原理进行设计实现。
两阶段提交协议(Two Phase Commitment Protocol)是分布式事务的基础协议。
在此协议中,一个事务协调器(TM, transaction manager)协调多个资源管理器(RM, resource manager)的活动;在一阶段所有资源管理器(RM)向事务管理器(TM)汇报自身活动状态,在第二阶段事务管理器(TM)根据各资源管理器(RM)汇报的状态,来决定各RM是执行提交操作还是回滚操作;具体描述如下:
应用程序向事务管理器(TM)提交请求,发起方分布式事务;
一阶段,事务管理器(TM)联络所有资源管理器(RM),通知它们执行准备操作;
资源管理器(RM)返回准备成功,或者失败的消息给TM(响应超时算作失败);
通过事务管理器2阶段协调资源管理器,使所有资源管理器的状态最终都是一致的,要么全部提交,要么全部回滚。
XA是X/Open组织提出的,定义了事务管理器与资源管理器之间通信的接口协议;XA协议由数据库实现,目前支持XA协议的数据库有Oracle、MySql、BD2等;
XA定义了一系列的接口:
xa_start: 启动XA事务
xa_end: 结束XA事务
xa_prepare: 准备阶段,XA事务预提交
xa_commit:提交XA事务
一个数据库实现XA协议之后,它就可以作为作为一个资源管理器参与到分布式事务中;
在一阶段,事务管理器协调所有数据库执行XA事务(xa_start、用户SQL、xa_end),并完成XA事务预提交(xa_prepare);
在二阶段,如果所有数据库上XA事务预提交均成功,那么事务管理器协调所有数据库提交XA事务(xa_commit);如果任一数据库上XA是我预提交失败,那么事务管理器会协调所有数据组回滚XA事务(xa_rollback);
分布式事物基本理论: 基本遵循CPA理论,采用柔性事物特征,软状态或者最终一致性特点保证分布式事物一致性问题。
分布式事务常见解决方案:
2PC两段提交协议
3PC三段提交协议(弥补两端提交协议缺点)
TCC或者GTS(阿里)
消息中间件最终一致性
两阶段提交又称2PC,2PC是一个非常经典的强一致、中心化的原子提交协议
。
这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点
(coordinator)和N个参与者节点
(partcipant)。
两个阶段
:第一阶段:投票阶段?和第二阶段:提交/执行阶段。
举例
?订单服务A,需要调用?支付服务B?去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。
那么看2PC阶段是如何处理的
image
第一阶段主要分为3步
1)事务询问
协调者?向所有的?参与者?发送事务预处理请求,称之为Prepare,并开始等待各?参与者?的响应。
2)执行本地事务
各个?参与者?节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向?协调者?报告说:“我这边可以处理了/我这边不能处理”。.
3)各参与者向协调者反馈事务询问的响应
如果?参与者?成功执行了事务操作,那么就反馈给协调者?Yes?响应,表示事务可以执行,如果没有?参与者?成功执行事务,那么就反馈给协调者?No?响应,表示事务不可以执行。
第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。
成功条件
:所有参与者都返回Yes。
第二阶段主要分为两步
1)所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交
?协调者?向?所有参与者?节点发出Commit请求.
2)事务提交
?参与者?收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
异常条件
:任何一个?参与者?向?协调者?反馈了?No?响应,或者等待超时之后,协调者尚未收到所有参与者的反馈响应。
异常流程第二阶段也分为两步
1)发送回滚请求
?协调者?向所有参与者节点发出?RoollBack?请求.
2)事务回滚
?参与者?接收到RoollBack请求后,会回滚本地事务。
通过上面的演示,很容易想到2pc所带来的缺陷
1)性能问题
无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务?协调者?才会通知进行全局提交,
参与者?进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
2)单节点故障
由于协调者的重要性,一旦?协调者?发生故障。参与者?会一直阻塞下去。尤其在第二阶段,协调者?发生故障,那么所有的?参与者?还都处于
锁定事务资源的状态中,而无法继续完成事务操作。(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
2PC出现单点问题的三种情况
(1)协调者正常,参与者宕机
由于?协调者?无法收集到所有?参与者?的反馈,会陷入阻塞情况。
?解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。
(2)协调者宕机,参与者正常
无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.
?解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。
(3)协调者和参与者都宕机
2)发生在第二阶段 并且 挂了的参与者在挂掉之前没有收到协调者的指令。也就是上面的第4步挂了,这是可能协调者还没有发送第4步就挂了。这种情形下,新的协调者重新执行第一阶段和第二阶段操作。
3)发生在第二阶段 并且 有部分参与者已经执行完commit操作。就好比这里订单服务A和支付服务B都收到协调者?发送的commit信息,开始真正执行本地事务commit,但突发情况,Acommit成功,B确挂了。这个时候目前来讲数据是不一致的。虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了!2PC 无法解决这个问题。
三阶段提交协议(3PC)主要是为了解决两阶段提交协议的阻塞问题,2pc存在的问题是当协作者崩溃时,参与者不能做出最后的选择。因此参与者可能在协作者恢复之前保持阻塞。
三阶段提交(Three-phase commit),是二阶段提交(2PC)的改进版本。
与两阶段提交不同的是,三阶段提交有两个改动点。
1、 引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有
CanCommit
、PreCommit
、DoCommit
三个阶段。
之前2PC的一阶段是本地事务执行结束后,最后不Commit,等其它服务都执行结束并返回Yes,由协调者发生commit才真正执行commit。而这里的CanCommit指的是?尝试获取数据库锁?如果可以,就返回Yes。
这阶段主要分为2步
事务询问
?协调者?向?参与者?发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待?参与者?的响应。响应反馈
?参与者?接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。这里的PreCommit阶段?跟上面的第一阶段是差不多的,只不过这里?协调者和参与者都引入了超时机制?(2PC中只有协调者可以超时,参与者没有超时机制)。
这里跟2pc的阶段二是差不多的。
总结
相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢?
这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,
自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),但是3PC依然没有完全解决数据不一致的问题。
TCC三个操作描述:
Try: 检测、预留资源;
Confirm: 业务系统执行提交;默认Confirm阶段是不会出错的,只要TRY成功,CONFIRM一定成功;
Cancel: 业务取消,预留资源释放;
TCC又称补偿事务。其核心思想是:"针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)"。它分为三个操作:
1、Try阶段:主要是对业务系统做检测及资源预留。
2、Confirm阶段:确认执行业务操作。
3、Cancel阶段:取消执行业务操作。
TCC对应?Try、Confirm、Cancel?三种操作可以理解成关系型数据库事务的三种操作:DML、Commit、Rollback。
在一个跨应用的业务操作中
Try
:Try操作是先把多个应用中的业务资源预留和锁定住,为后续的确认打下基础,类似的,DML操作要锁定数据库记录行,持有数据库资源。
Confirm
:Confirm操作是在Try操作中涉及的所有应用均成功之后进行确认,使用预留的业务资源,和Commit类似;
Cancel
:Cancel则是当Try操作中涉及的所有应用没有全部成功,需要将已成功的应用进行取消(即Rollback回滚)。其中Confirm和Cancel操作是一对反向业务操作。
TCC的具体原理图如(盗图):
从图中我们可以明显看到Confirm和Cancel操作是一对反向业务操作
?即要try返回成功执行Confirm,要么try返回失败执行Cancel操作。
分布式事务协调者
:分布式事务协调者管理控制整个业务活动,包括记录维护TCC全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时确认所有的TCC型
操作的confirm操作,在业务活动取消时调用所有TCC型操作的cancel操作。
例子
:A服务转30块钱、B服务转50块钱,一起到C服务上。
Try
:尝试执行业务。完成所有业务检查(一致性):检查A、B、C的帐户状态是否正常,帐户A的余额是否不少于30元,帐户B的余额是否不少于50元。预留必须业务资源
(准隔离性):帐户A的冻结金额增加30元,帐户B的冻结金额增加50元,这样就保证不会出现其他并发进程扣减了这两个帐户的余额而导致在后续的真正转帐操作过程中,
帐户A和B的可用余额不够的情况。
Confirm
:确认执行业务。真正执行业务:如果Try阶段帐户A、B、C状态正常,且帐户A、B余额够用,则执行帐户A给账户C转账30元、帐户B给账户C转账50元的转帐
操作。?这时已经不需要做任何业务检查,Try阶段已经完成了业务检查。只使用Try阶段预留的业务资源:只需要使用Try阶段帐户A和帐户B冻结的金额即可。
Cancel
:取消执行业务释放Try阶段预留的业务资源:如果Try阶段部分成功,比如帐户A的余额够用,且冻结相应金额成功,帐户B的余额不够而冻结失败,则需要
对帐户A做Cancel操作,将帐户A被冻结的金额解冻掉。
2PC是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
XA事务中的两阶段提交内部过程是对开发者屏蔽的,事务管理器在两阶段提交过程中,从prepare到commit/rollback过程中,资源实际上一直都是被加锁的。
如果有其他人需要更新这两条记录,那么就必须等待锁释放。
TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
我的理解就是当执行try接口的时候,已经把所需的资源给预扣了,比如上面举例的A服务已经预扣30元,B服务已经预扣50元,它是由try接口实现,这样就保证不会
出现其他并发进程扣减了这两个帐户的余额而导致在后续的真正转帐操作过程中,帐户A和B的可用余额不够的情况,同时保证不会一直锁住整个资源。(核心点应该就在这)
TCC中的两阶段提交并没有对开发者完全屏蔽,也就是说从代码层面,开发者是可以感受到两阶段提交的存在。
1、try过程的本地事务,是保证资源预留的业务逻辑的正确性。
2、confirm/cancel执行的本地事务逻辑确认/取消预留资源,以保证最终一致性,也就是所谓的补偿型事务
。
由于是多个独立的本地事务,因此不会对资源一直加锁。
TCC 实质上是应用层的2PC ,好比把 XA 两阶段提交那种在数据资源层做的事务管理工作提到了数据应用层。
2PC是资源层面的分布式事务,是强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
TCC相比较于2PC来讲性能会好很多,但是因为同时需要改造try、confirm、canel3个接口,开发成本高。
注意:还有一点需要注意的是Confirm和Cancel操作可能被重复调用,故要求Confirm和Cancel两个接口必须是幂等。
列子
:假设?A?给?B?转?100块钱,同时它们不是同一个服务上。
目标
:就是?A?减100块钱,B?加100块钱。
实际情况可能有四种:
1)就是A账户减100 (成功),B账户加100 (成功)2)就是A账户减100(失败),B账户加100 (失败)3)就是A账户减100(成功),B账户加100 (失败)4)就是A账户减100 (失败),B账户加100 (成功)
这里?第1和第2?种情况是能够保证事务的一致性的,但是?第3和第4?是无法保证事务的一致性的。
单数据库事务完全遵循ACID规范,属于刚性事务,分布式事务要完全遵循ACID规范比较困难, 分布式事务属于柔性事务,满足BASE理论;
BASE描述:BA(Basic Availability 基本业务可用性)、S(Soft state 柔性状态)、E(Eventual consistency 最终一致性);
柔性事务对ACID的支持:
1、原子性:严格遵循;
2、一致性:事务完成后的一致性严格遵循,事务中的一致性可适当放宽;
3、隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽;
4、持久性:严格遵循
为了可用性、性能的需要,柔性事务降低了一致性(C)与隔离性(I) 的要求,即“基本可用,最终一致”.
那我们来看下RocketMQ是如何来保证事务的一致性的。
RocketMQ虽然之前也支持分布式事务,但并没有开源,等到RocketMQ 4.3才正式开源。
最终一致性
RocketMQ是一种最终一致性的分布式事务,就是说它保证的是消息最终一致性,而不是像2PC、3PC、TCC那样强一致分布式事务,至于为什么说它是最终一致性事务下面会详细说明。
Half Message(半消息)
是指暂不能被Consumer消费的消息。Producer 已经把消息成功发送到了 Broker 端,但此消息被标记为暂不能投递
状态,处于该种状态下的消息称为半消息。需要 Producer
对消息的二次确认
后,Consumer才能去消费它。
消息回查
由于网络闪段,生产者应用重启等原因。导致 Producer 端一直没有对?Half Message(半消息)?进行?二次确认。这是Brock服务器会定时扫描长期处于半消息的消息
,会
主动询问 Producer端 该消息的最终状态(Commit或者Rollback),该消息即为?消息回查。
理解这张阿里官方的图,就能理解RocketMQ分布式事务的原理了。
我们来说明下上面这张图
1、A服务先发送个Half Message给Brock端,消息中携带 B服务 即将要+100元的信息。 2、当A服务知道Half Message发送成功后,那么开始第3步执行本地事务。 3、执行本地事务(会有三种情况1、执行成功。2、执行失败。3、网络等原因导致没有响应) 4.1)、如果本地事务成功,那么Product像Brock服务器发送Commit,这样B服务就可以消费该message。4.2)、如果本地事务失败,那么Product像Brock服务器发送Rollback,那么就会直接删除上面这条半消息。4.3)、如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查。
从上面流程可以得知?只有A服务本地事务执行成功 ,B服务才能消费该message
。
然后我们再来思考几个问题?
为什么要先发送Half Message(半消息)
我觉得主要有两点
1)可以先确认 Brock服务器是否正常 ,如果半消息都发送失败了 那说明Brock挂了。 2)可以通过半消息来回查事务,如果半消息发送成功后一直没有被二次确认,那么就会回查事务状态。
什么情况会回查
也会有两种情况
1)执行本地事务的时候,由于突然网络等原因一直没有返回执行事务的结果(commit或者rollback)导致最终返回UNKNOW,那么就会回查。 2) 本地事务执行成功后,返回Commit进行消息二次确认的时候的服务挂了,在重启服务那么这个时候在brock端 它还是个Half Message(半消息),这也会回查。
特别注意: 如果回查,那么一定要先查看当前事务的执行情况,再看是否需要重新执行本地事务。
想象下如果出现第二种情况而引起的回查,如果不先查看当前事务的执行情况,而是直接执行事务,那么就相当于成功执行了两个本地事务。
为什么说MQ是最终一致性事务
通过上面这幅图,我们可以看出,在上面举例事务不一致的两种情况中,永远不会发生
A账户减100 (失败),B账户加100 (成功)
因为:如果A服务本地事务都失败了,那B服务永远不会执行任何操作,因为消息压根就不会传到B服务。
那么?A账户减100 (成功),B账户加100 (失败)?会不会可能存在的。
答案是会的
因为A服务只负责当我消息执行成功了,保证消息能够送达到B,至于B服务接到消息后最终执行结果A并不管。
那B服务失败怎么办?
如果B最终执行失败,几乎可以断定就是代码有问题所以才引起的异常,因为消费端RocketMQ有重试机制,如果不是代码问题一般重试几次就能成功。
如果是代码的原因引起多次重试失败后,也没有关系,将该异常记录下来,由人工处理
,人工兜底处理后,就可以让事务达到最终的一致性。