分布式事务有2种实现方式:java
使用数据库自己自带的特性(强一致)数据库
复杂的业务交互过程当中,不建议使用强一致性的分布式事务。解决分布式事务的最好办法就是不考虑分布式事务。就像刚说的问题同样,把分布式的事务过程拆解成多个中间状态,中间状态的东西不容许用户直接操做,等状态都一致成功,或者检测到不一致的时候所有失败掉。就解耦了这个强一致性的过程。服务器
通常状况下准实时就成了。涉及到钱,有时候也能够这么搞。
淘宝几s内完整一个订单处理,不是什么问题吧。
银行也不是所有都强一致性。也会扎差,也会冲正。
并发
特别是涉及到多个系统的时候,咱们好比买记票,支付完成之后,只支付完成状态,而后返回给用户了,咱们过几分钟再刷新页面,才会看到变成已出票,订单完成状态。
这个时候,若是咱们要求全部处理,都是强一致性的,那么久完蛋了。页面要死在那儿几分钟,才把这个事务处理完成,返回给用户。分布式
这样就确定涉及一个问题,支付了,可是最终出票没出来。那就没办法,商量换票或退款。
淘宝的订单改为出票失败,给支付发消息通知退款。spa
慢的时候,有多是手工出票,这时出一张票半小时均可能,若是要求都必须强一致性的话,全部处理线程都挂在哪儿,系统早就完蛋了。线程
解决分布式事务的最好办法就是不考虑分布式事务。设计
拆分,大的业务流程,转化成几个小的业务流程,而后考虑最终一致性。日志
模拟分布式事务流程(两段式提交就是解决分布式事务的一种方式):code
两段式提交设计自己的思路很是的容易理解,步骤以下:
1. 协调员服务器(协调员)发送一条投票请求消息给全部参与此次事务的服务器(参与者)。
2. 当一个参与者收到一条投票请求,它会向协调员发送一条响应请求消息,该响应消息包含了参与者的投票:YES 或者NO。若是参与者的消息的投票是NO,那就意味着因为某些缘由,参与者不能参与此次事务,等价于收到了ABORT决定,本次事务的工做到此为止。
3. 协调员收集全部参与者的响应投票,若是全部的响应投票都是YES,那么协调员就会作出决定:COMMIT,而且会把COMMIT消息发送给全部参与者。不然,协调员则会作出决定:ABORT,此时协调员会把ABORT消息发给那些投票为YES的那些参与者(投票为NO的参与者已经单方面ABORT了此次事务,协调员没必要再发送消息给这些参与者)。发送完决定后,协调员对于本次事务的工做就此中止了。
4. 投了YES票的参与者等待着来自协调员的决定(COMMIT或者ABORT),而后根据决定作完相应的操做,而后本次事务的工做也就此为止。
步骤1,2属于两段式提交的阶段1,步骤3,4属于两段式提交的阶段2。在整个过程当中,参与者会存在一段不肯定时间段(从它发送YES的票开始,到它收到COMMIT/ABORT的决定结束),在此时间段内,参与者的进程会被block住,它须要等待接下来的决定。而协调员则不存在任何不肯定时间段,它能够继续处理其它的事务请求,发送其它事务的投票请求,在作完COMMIT/ABORT决定以后,它能够立刻去干别的事情,无需任何等待。由于协调员的工做不具备原子性,它能够交叉得作任何事。而参与者完成的是事务,具备原子性,它作出承诺后,他必须保持好事务的现场,避免别的事务的交叉感染,从而违反了ACID中的Isolated。
从描述来看很是简单,很容易理解,可是请注意,在整个过程当中的任什么时候间点,都有可能发生的各类各样的故障,有的是链路故障,有的是服务器故障。若是详细考虑这些状况,实现就不是这么简单了。
先考虑第一个问题,在整个执行的过程当中,不管是参与者的进程,仍是协调者的进程,他们在作下一步的处理前都必须等待消息。可是,消息可能会失败,并不老是可以到达。为了不无休止的等待消息,所以须要加入Timeout 。当消息超过必定的时间还没到来的时候,咱们必须作出处理,这些处理咱们称之为Timeout-Action。当服务器或者服务器的进程(不管是协调员仍是参与者)从一次失败中恢复过来的时候,咱们但愿服务器的进程可以尝试着得到一个和其余进程一致的决定。这很好理解,COMMIT/ABORT的决定已经由协调员发出了,那么恢复的参与者进程也但愿可以获得这个决定从而参与完成该事务。固然,在参与者从失败中恢复过来的时候,因为其它的一些可能的失败,可能COMMIT/ABORT的决定还未能作出,此时该参与者也须要作出相应的正确处理。所以,服务器的进程必须保存一些信息,好比是一些Log。有了这些Log,才能使得从失败中恢复的进程可以正确恢复事务处理。
Timeout-Action
进程须要在3个地方等待消息:在(2),(3),(4)步开始的地方:
在(2)步骤中,参与者进程须要等来来自协调员进程的投票请求。此时若是在等待投票请求时发生了timeout,参与者服务器就能够简单得中止该事务的工做就能够了。
在(3)步骤中,协调员须要等待接受全部参与者回应的YES或NO的投票,在此时,协调员还未达成任何决定,参与者也没有提交任何数据,所以协调员在Timeout发生后,只须要发送ABORT决定给全部的参与者就能够了。
在(4)步骤中,参与者p已经投了YES票,正在等待来自协调员的COMMIT或ABORT命令。在这个时间节点上,p处在不肯定时间段。所以此时,p不能在timeout的时候简单得单方面做出决定,他须要向其余服务器作咨询才能知道该如何处理。最简单的终止设计能够是这样的:p依然被block住,一直询问等待协调员,直到p从新创建起和协调员之间的联系。接着,协调员就会告诉p已经做出的决定(协调员没有不肯定时间期),而后p就能够接着处理决定。
简单终止协议的缺点是参与者p会被没必要要得block住一段时间。好比,假若有2个参与者p和q,协调员把COMMIT/ABORT决定成功发送给q了,可是在它给p发送的决定失败了。的确,p这时是处在不肯定时期,可是q已经不在不肯定期了,若是p可以和q通讯的话,p能够从q那里获得协调员发出的决定,没必要一直block等到协调员恢复。
这须要参与者可以互相知道对方,参与者之间能够直接交换信息,没必要老是经过协调员的中介。要实现这种自由的信息交换也并非十分困难,协调员在发送投票请求的时候能够把全部参与者的ID列表附在投票请求消息后面发送给全部的参与者,这样参与者p在收到投票请求后就能够直接和其余全部的参与者进行交流了。这么作也不会带来什么反作用,在收到投票请求以前,参与者之间仍是互相不认识,所以在此以前(2),(3)发生的timeout仍是能够单方面得停止任务或者中止事务。这个思路就出现另外的一个设计-协同终止设计,设计以下:
当一个参与者p在其不肯定时间段内发生了timeout,他会依次向全部其余的进程发送一个询问请求消息,询问作出的决定是什么或者是否能单方面得作出一个决定(由于若是有一个被询问的参与者已经向协调员回复了一个NO的投票,那么询问者天然就能够单方面得作出决定ABORT此次事务,由于只要有一个参与者回复了NO,那么协调员作出的决定确定是ABORT,无需再向协调员确认了)。在这种场景下,参与者p就被称之为发起人,做出询问回答的服务器进程 q就能够称之为回应人。那么回应人q可能有3种状况:
1. q已经收到了COMMIT/ABORT决定:q只须要把该决定回应给p,而后p就能够自行处理了。
2. q还没进行投票:q此时能够单方面作出决定,由于此时协调员已经发生故障,此时q能够回应ABORT给p,p就能够本身作出处理。
3. q已经回复YES投票给协调员,处在不肯定期内,也没有收到来自协调员的决定。此时q也没法给p任何帮助。
根据这个设计,若是p发送询问请求给q,碰巧q处在状况(1)或者(2)时,p立刻就能够达成(也就是得到)一个决定而无需任何block。若是p能通信的其余全部的进程都处在状况(3),那么p也会被block住,直到足够的故障被修复使得p至少可以和一个处在状况(1)或(2)的参与者进程q通信。须要注意的是询问请求能够发给全部的其余服务器进程,包括协调员进程,这样至少能够确认协调员在没有故障的状态下能够回复投票请求,避免了碰巧全部其余的参与者进程都在不肯定期而没法提供帮助回应这样的窘境。
总之,协同终止设计能够下降block的几率,但不能彻底排除它。
恢复
一个服务器进程p刚刚从一次故障中恢复,咱们但愿p可以得到一个和其它进程们已经达成的决定一致的决定,若是不能立刻恢复这个决定,那么至少在其它的故障被修复后可以恢复这个决定。
当一个服务器进程p把系统恢复到了故障发生时现场保存的状态,咱们来进一步考虑一下。若是p是在它发送YES投票到协调员以前就发生故障了,那么该进程就能够单方面的决定取消此次事务,发送NO投票给协调员,不作任何处理。一样,若是p是在已经收到COMMIT/ABORT决定以后或者本身已经做出ABORT的决定以后发生故障了,那么此时p因为已经作出了决定,p就能够做出相应的处理,好比说取消事务操做,或者继续把COMMIT决定的操做执行完毕。在这些状况下,p都可以独立得进行故障恢复。
可是,若是p发生故障时是处在它的不肯定期时,那么它就没法在恢复时独立得作决定了,这就是问题的复杂之处。由于它投了YES,在p故障时,可能其余的参与者所有投了YES而且协调者作出了COMMIT的决定。又或者p发生故障时,其余参与者并未所有投票YES,所以协调者做出的是ABORT的决定。此时p没法根据本地信息就能独立得进行恢复,他须要和其余进程进行交流。在这种状况下,p所面临的状况是和time-action的状况(3)是同样的。(设想一下,p设置了一个很是长的timeout 时间,整个故障期间都没有超过timeout的期限)。所以此时p也采用前面提到的终止设计来解决问题。
为了保存故障发生时的状态,每一个进程都必须维护一个DT Log(Database Transaction Log)。每一个进程只能访问他本身服务器上的DT Log。假设咱们采用的是协同终止设计,咱们来看看若是管理这些DT log.
1. 当协调员发送投票请求以前或以后,它写了一条开始两阶段记录在DT log中。该记录大概相似这样:
{ Type: start-2PC, time: 2011-10-30 19:20:20, Participants: [ { Hostname:participant-1, Ip:192.168.0.3 }, { Hostname:participant-2, Ip:192.168.0.4 }, { Hostname:participant-3, Ip:192.168.0.5 } ] }
2. 若是参与者线程发送了YES投票,那么他必须在发送投票以前写这么YES 投票记录在DT Log中,大概相似这样:
{ Type: VOTE, Value:YES, time: 2011-10-30 19:20:20, Coordinator: 192.168.0.2 OtherParticipants: [ { Hostname:participant-2, Ip:192.168.0.4 }, { Hostname:participant-3, Ip:192.168.0.5 } ] }
若是参与者发送了NO投票,那么它能够在发送投票以前或以后写一条ABORT ACCEPT记录在DT log中。
3. 在协调员发送COMMIT决定给全部参与者进程以前,他写入一条COMMIT DECISION记录。
4. 当协调员发送ABORT决定给全部参与者进程以前或以后,它写入一条ABORT DECISION记录
5. 参与者服务器进程在收到COMMIT/ABORT决定以后,参与者进程写入一条COMMIT ACCEPT/ABORT ACCPET记录。
对上述Log作一些说明,一旦参与者服务器进程在DT日志中写入COMMIT ACCEPT或者ABORT ACCEPT记录后,DM(database manager)就能够执行commit或者abort数据库操做。具体来说还有不少细节,好比系统中的DT Log多是DM Log中的一部分,所以DT Log中的COMMIT ACCEPT/ABORT ACCEPT记录是经过本地DM的Commit/Abort子程序来实现的,在子程序中进行具体的操做以前,DM会写入COMMIT ACCEPT/ABORT ACCEPT记录到日志中去。
有了这个日志系统,当服务器S就能够按照下面的方式进行恢复:
1> 若是S检查DT Log发现了记录,那么S就知道本身是一台协调员。若是发现日志还包含了COMMIT DECISION或者ABORT DECISION日志,那就证实在故障发生以前已经产生了决定,他能够选择从新发送这些决定。若是没有发现这两条记录中的任何一条,那么S就能够单方面得决定Abort,同时向日志中写入ABORT DECISION记录,并重发决定。须要注意的是,要先插入COMMIT DECISION日志,再发送COMMIT决定给各个参与者进程,这很关键。为何顺序这么关键呢?试想一下,若是发送决定消息在前,插入日志在后,那么就会有一种可能,消息COMMIT DECISION发送完了但日志还没来得及写入的时候服务器发生故障了,当服务器恢复以后,按照前面的逻辑,它会认为还未作出任何决定,因而又单方面的决定ABORT DECISION,这下就和实际状况冲突了,参与者就会受到两条彻底冲突的决定:ABORT DECISION和COMMIT DECISION,系统会没法处理。若是写日志在前,发送消息在后,系统也有可能在两个时间点之间发生故障,协调员恢复时会看见日志,所以不会作任何事或者把决定从新发送一遍,由于决定事先已经达成,即便有可能消息尚未发送,但至少不会作出自相矛盾的决定令参与者没法是从。
2> 若是S没有发现任何记录,S就会认为本身是一台参与者。那么就会有三种状况:
1. DT log中包含了COMMIT ACCEPT或者ABORT ACCEPT记录,那参与者已经得到了决定,那么参与者能够本身来决定,能够根据记录来查看相应的操做是否完成,若是还未完成能够继续从而完成相应操做。
2. 若是日志中没有包含VOTE YES记录以及任何COMMIT ACCEPT或者ABORT ACCEPT记录,咱们没法获得它当时是选择YES仍是NO。咱们写VOTE YES记录的时间也要比发送实际消息早,尽量早得保存决定。此时S能够单方面得决定ABORT ACCEPT。
3. 若是日志中包含VOTE YES记录但没有任何COMMIT ACCEPT或者ABORT ACCEPT记录。那么参与者是在不肯定期发生故障的,所以它采用终止协议来得到决定。
对于一个实际的系统而言,系统须要处理的是不少的事务,所以不一样事务的日志是交错得存放在DT Log里。所以每条日志记录须要包含事务的名字。并且随着时间的积累,事务愈来愈多,日志的体积也会愈来愈庞大。所以须要按期对日志进行垃圾回收。日志垃圾回收有2个准则:
GC1:一台服务器不能删除事务T的日志,直到它的RM(Recovery Manager)已经处理完了RM-Commit(T)或者RM-Abort(T)
GC2:一台服务器不能删除事务T的日志,直到该服务器收到消息,全部其余服务器的RM-Commit(T)或者Rm-Abort(T)已经处理完毕。
对于GC1,经过本地的信息很容易获得。对于GC2,则须要服务器之间可以相互通讯,你可让协调员来执行GC2,或者彻底分布式得由各个服务器经过相互交流完成GC2.
因为实际系统同时并发得处理不少事务,所以在某台服务器恢复的时候,咱们还须要考虑一些细节问题。当服务器恢复时,它须要把继续完成那些还未COMMIT或ABORT的事务,这些事务在彻底恢复以前都会被block住从而没法访问数据库这部分资源,这会形成浪费。所以解决的方法是否是在整个恢复阶段一直hold住这些待恢复而且在故障以前处于不肯定期被block住得事务的全部的读写锁,而是把这些锁暂时所有释放,而后再经过从新争取锁的方式来和新到的事务来竞争锁,这样避免了在整个恢复阶段全部的block资源都没法访问。具体的流程是这样的,服务器恢复后,先处理那些没有被block住的事务,为这些事务作出决定。而后再处那些故障前被block的事务,这时候恢复程序先释放这些事务的全部读写锁,而后再与故障以后新的事务一块儿竞争从新请求这些读写锁。一旦恢复程序先释放了待恢复的block事务的读写锁,那么这些事务所持有的数据库资源就能够被访问了。固然因为有竞争,原来原本能够COMMIT的事务可能因为资源竞争被ABORT掉了,但带来的好处是吞吐量大大提升。在原来的方案中,事务的锁能够保存在DT Log里,在竞争的方案中,锁能够没必要保存,由于服务器进程能够根据Log自行决定。