事务是数据库从一个稳定状态变迁到另外一个稳定状态的保证,具有 ACID 这 4 个特性:面试
原子性(Atomicity):一个事务中的全部操做,要么所有完成,要么所有不完成,不会结束在中间某个环节。事务在执行过程当中发生错误,会被回滚到事务开始前的状态。数据库
一致性(Consistency):在事务开始以前和事务结束之后,数据库的完整性限制没有被破坏。安全
隔离性(Isolation):两个事务的执行是互不干扰的,两个事务时间不会互相影响。服务器
例如应用程序须要更新多条相关数据时就须要进行事务处理。网络
当遇到复杂业务调用时,可能会出现跨库多资源调用(一个事务管理器,多个资源)/多服务调用(多个事务管理器,多个资源),指望所有成功或失败回滚,这就是分布式事务,用以保证“操做多个隔离资源的数据一致性”。并发
分布式事务是指会涉及到操做多个数据库的事务,一样必须保证 ACID。其就是将对同一库事务的概念扩大到了对多个库的事务:对同一库的 SQL 操做对应了分布式事务中对一个库的事务。框架
X/Open XA 定义了分布式事务处理的规范,并由数据库厂商在驱动层面进行实现。XA 规范的基础是两阶段提交协议,并定义了分布式事务处理所涉及的角色:分布式
应用程序(AP)
事务管理器(TM)
资源管理器(RM)
通讯资源管理器(CRM)ide
常见的事务管理器( 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 响应,表示事务不能够执行。
第一阶段执行完后,会有两种可能。一、全部都返回Yes. 二、有一个或者多个返回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)的改进版本。
与两阶段提交不一样的是,三阶段提交有两个改动点。
一、 引入超时机制。同时在协调者和参与者中都引入超时机制。
二、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段以前各参与节点的状态是一致的。
也就是说,除了引入超时机制以外,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又称补偿事务。其核心思想是:"针对每一个操做都要注册一个与其对应的确认和补偿(撤销操做)"。它分为三个操做:
一、Try阶段:主要是对业务系统作检测及资源预留。
二、Confirm阶段:确认执行业务操做。
三、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中的两阶段提交并无对开发者彻底屏蔽,也就是说从代码层面,开发者是能够感觉到两阶段提交的存在。
一、try过程的本地事务,是保证资源预留的业务逻辑的正确性。
二、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的支持:
一、原子性:严格遵循;
二、一致性:事务完成后的一致性严格遵循,事务中的一致性可适当放宽;
三、隔离性:并行事务间不可影响;事务中间结果可见性容许安全放宽;
四、持久性:严格遵循
为了可用性、性能的须要,柔性事务下降了一致性(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分布式事务的原理了。
咱们来讲明下上面这张图
一、A服务先发送个Half Message给Brock端,消息中携带 B服务 即将要+100元的信息。 二、当A服务知道Half Message发送成功后,那么开始第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有重试机制,若是不是代码问题通常重试几回就能成功。
若是是代码的缘由引发屡次重试失败后,也没有关系,将该异常记录下来,由人工处理
,人工兜底处理后,就可让事务达到最终的一致性。