wenjieyatou 2020-06-09
即ACID:Atomicity、Consistency、Isolation、Durability
原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性:从事务开始到事务结束,数据库的完整性约束(包括外建约束、级连关系、触发器等)不应该因为任何原因被破坏。
隔离性:对于并发提交的事务,数据库需要提供并发控制,保证数据库从一个事务的开始状态转换到该事务的结束状态的过程中,中间状态不被其他事务可见,就像事务串行执行一样。
持久性:在事务完成以后,即使发生掉电、系统宕机等错误,该事务对数据库所作的更改仍需持久的保存在数据库中。
重点说一下一致性问题,一致性大体上分为三类:
1)Weak 弱一致性:当你写入一个新值后,读操作在数据副本上可能读出来,也可能读不出来。比如:某些cache系统,网络游戏其它玩家的数据和你没什么关系,VOIP这样的系统,或是百度搜索引擎。
2)Eventually 最终一致性:当你写入一个新值后,有可能读不出来,但在某个时间窗口之后保证最终能读出来。比如:DNS,电子邮件、Amazon S3,Google搜索引擎这样的系统。
3)Strong 强一致性:新的数据一旦写入,在任意副本任意时刻都能读到新值。比如:文件系统,RDBMS,Azure Table都是强一致性的。
为了解决“数据一致性”问题可能产生性能上的问题,数据一致性和性能的平衡问题其实是我们在做分布式业务系统面临的一个重要问题。
拿常见的转账问题举个栗子:
A:把“K1向K2转账20元”分解为两个操作:(1)DB1上K1减去20元,剩余100-20=80元,(2)DB2上K2加上20元,剩余100+20=120元。两个操作要么同时成功、要么同时失败。如果(1)成功,而(2)失败,必然会引起用户投诉;而如果(2)成功,而(1)失败,那么会带来银行或者金融机构发生资损;
C:K1、K2两个账户组成的账务系统,K1+K2的余额总和在事务发生前、发生后都必须是一致的,都应该是200元,否则会引入系统性风险;
I:在转账操作结束之前,其他并发的事务不应该读到事务变更后的账户信息,不能对两个账户的余额字段进行修改操作;
D:事务一旦提交,对账户K1、K2的修改应该是持久的,这个属性一般的数据库都是可以保证的。
XA协议指的是TM(事务管理器)和RM(资源管理器)之间的接口。目前主流的关系型数据库产品都是实现了XA接口的。JTA(Java Transaction API)是符合X/Open DTP模型的,事务管理器和资源管理器之间也使用了XA协议。 XA是一个分布式事务协议,由Tuxedo提出。
DTP(Distributed Transaction Process)是一种分布式事务模型。其模型如下:
上图模型中,主要涉及三个对象:
AP(Application Program):应用程序;
TM(Transaction Manager):事务管理器,负责协调和管理事务。
RM(Resource Manager):资源管理器,可以理解为数据库。
三者的关系如下:
(1)AP通过TM来操作多个RM,AP也可以通过RM的本地事务接口来操作单个RM;
(2)TM和RM可以互相通信,他们之间的通信协议就是XA协议。
基本原理图如下:
第一个阶段:事务管理器通过XA协议,向资源管理器发送prepare命令,询问他们预提交是否成功,每个资源管理器给出自己的响应,预提交成功或者失败。
第二个阶段:根据第一个阶段资源管理器的回复情况发送不同的命令。若全部表示预提交成功,则事务管理器向所有的资源管理器发送最终的提交命令;若有一个资源管理器回复预提交失败,则事务管理器向所有的资源管理器发送回滚命令,全部进行回滚。
优点:XA接口标准化、使用简单,使用成本也很低;
缺点:
1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。所以事务管理器必须等待每一个资源管理器发送回复之后,才能进行下一步操作。一旦资源管理器挂掉,事务管理器则收不到响应,就会造成事务管理器一直等待。这时候就必须引入超时机制,一旦超过某个时间还没有回复,则认为失败,回滚所有操作,这些都是非常耗性能的。
2、单点故障。两阶段提交协议是阻塞式协议,由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题。
三段提交的核心理念是:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁资源。
1、CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
2、PreCommit阶段
协调者根据参与者的反应情况来决定是否可以继续事务的PreCommit操作。根据响应情况,有以下两种可能:
1) 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
2)假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
3、doCommit阶段
该阶段进行真正的事务提交,也可以分为成功和失败两种情况。如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了,所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。
2PC与3PC的区别:
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。
但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
TCC方案其实是两阶段提交的一种改进。其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。这是一种补偿方案,confirm与cancel都是对try的补偿,基本原理如下图所示:
事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。
TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 当然TCC方案也有不足之处,集中表现在以下两个方面:
对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。
微服务倡导服务的轻量化、易部署,而TCC方案中很多事务的处理逻辑需要应用自己编码实现,复杂且开发量大。
所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:
1、A系统向消息中间件发送一条预备消息
2、消息中间件保存预备消息并返回成功
3、A执行本地事务
4、A发送提交消息给消息中间件
通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:
步骤一出错,则整个事务失败,不会执行A的本地操作
步骤二出错,则整个事务失败,不会执行A的本地操作
步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息
步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务。
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:
虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。