原文连接:https://zhuanlan.zhihu.com/p/51684618面试
若是一个事务调用了不一样服务器上的操做,那么它就成为了一个分布式事务。算法
考虑下面一种场景:当你发了工资以后,把你的当月工资¥1024从支付宝转到了余额宝。服务器
若是在支付宝帐户扣除¥1024以后,余额宝系统挂掉了,余额宝的帐户并无增长¥1024,这时候就出现了数据不一致的状况。多线程
在不少系统中都能找到上述状况的影子:架构
在一个分布式事务结束的时候,事务的原子特性要求全部参与该事务的服务器必须所有提交或所有放弃该事务。为了实现这一点,其中一个服务器承担了协调者(coordinater)的角色,由它来保证全部的服务器得到相同的结果。并发
协调者(coordinater)的工做方式取决于它选用的协议,“两阶段提交”是分布式事务最经常使用的协议。分布式
一、two-phase commit protocol微服务
两阶段提交协议(two-phase commit protocol)的设计出发点是容许任何一个参与者自行放弃它本身的那部分事务。因为事务原子性的要求,若是部分事务被放弃,那么整个分布式事务也必须被放弃。高并发
在该协议的第一个阶段,每一个参与者投票表决该事务是放弃仍是提交,一旦参与者要求提交事务,那么就不容许放弃该事务。所以,在一个参与者要求提交事务以前,它必须保证最终可以执行分布式事务中本身的那部分,即便该参与者出现故障而被中途替换掉。性能
一个事务的参与者若是最终能提交事务,那么能够说参与者处于事务的准备好(prepared)状态。为了保证可以提交,每一个参与者必须将事务中全部发生改变的对象以及自身的状态(prepared)保存到持久性存储中。
在该协议的第二个阶段,事务的每一个参与者执行最终统一的决定。若是任何一个参与者投票放弃事务,那么最终的决定是放弃事务。若是全部的参与者都投票提交事务,那么最终的决定是提交事务。
问题在于,要保证每一个参与者都投票,而且达成一个共同的决定。在无端障时,该协议至关简单。可是,协议必须在出现各类故障(例如服务器崩溃,消息丢失或服务暂时没法通讯)时可以正常工做。
二、两阶段提交的实现
为了实现两阶段提交协议,分布式事务中的协调者和参与者一般按照下面的接口进行通讯:
协调者询问参与者是否能够提交事务,参与者回复本身的投票结果。
协调者告诉参与者提交它的那部分事务。
协调者告诉参与者放弃它的那部分事务。
参与者用该操做向协调者确认它提交了事务。
当参与者在投Yes票后一段时间内未收到应答时,参与者用该操做向协调者询问事务的投票表决结果。该操做用于从服务器崩溃或从消息延迟中恢复。
阶段一(投票阶段): 1)协调者向分布式事务的全部参与者发送canCommit?请求 2)当参与者收到canCommit请求后,它向协调者回复本身的投票(Yes/No)。 在投Yes票以前,它在持久性存储中保存全部对象,准备提交。若是投No票,参与者当即放弃。 阶段二(提交阶段): 1)协调者收集全部的投票(包括它本身的投票)。 a)若是不存在故障而且全部的投票都是Yes时,那么协调者将决定提交事务并向全部参与者发送doCommit 请求 b)不然,协调者决定放弃该事务,并向全部投Yes票的参与者发送doAbort请求 2)投Yes票的等待者等待协调者发送的doCommit或者doAbort请求。一旦参与者接收到任何一种请求消息, 它将根据该请求放弃或者提交事务。若是请求是提交事务,那么他还要向协调者发送一个haveCommitted 来确认事务已经提交
三、分布式事务的故障模型
在分布式事务中执行的过程当中,可能出现磁盘故障,进程崩溃以及消息的丢失,超时等。
两阶段提交是一种达成共识的协议,在该系统中,若是进程崩溃,那么是不可能达成共识的。可是,两阶段提交倒是在这些条件下达成了共识,这是因为进程的崩溃被屏蔽,崩溃的进程被一个新的进程取代,新进程的状态根据持久性存储中保存的信息和其余进程拥有的信息来设定。
3.一、故障模型
Lampson提出过一个分布式事务的故障模型,包括了硬盘故障、服务器故障以及通讯故障。该故障模型声称:能够保证算法在出现故障时正确工做,可是对于不可预见的灾难性故障则不能正确处理。尽管会出现错误,可是能够在发生不正确行为以前发现并处理这些错误。Lampson的故障模型包括如下故障:
利用这个关于持久性存储、处理器和通讯的故障模型可以设计出一个可靠系统,该系统的组件可对付任何单一故障,并提供一个简单的故障模型。特别是,可靠存储(stable storage)能够在出现一个write操做故障或者进程崩溃的状况下提供原子写操做。它是经过将每个数据块复制到两个磁盘上实现的。此时一个write操做用于两个磁盘块,在一个磁盘出现故障的前提下,另外一个好的磁盘也能够提供正确数据。可靠处理器(stable processor)使用可靠存储,用于在崩溃以后恢复对象。可经过可靠的远程过程调用机制来屏蔽通讯错误。
3.二、两阶段提交协议的超时
在两阶段协议的不一样阶段,协调者或参与者都会遇到这种场景:不能处理它的那部分协议,直到接收到下一个请求或应答为止。
首先考虑这样的情形:某个投票者投Yes票并等待协调者发回最终决定,即告诉它是提交事务仍是放弃事务。这样参与者的结果是不肯定(uncertain)的,它在协调者处获得投票结果以前不能进行进一步处理。参与者不能单方面决定下一步作什么,同时该事务使用的对象也不能释放以用于其余事物。参与者向协调者发出getDecision请求来获取事务的结果,直到收到应答时,才能进入两阶段协议的第二阶段。
同理,若是协调者发生故障,那么参与者将不能得到协定,直到协调者被替代为止,这可能致使不肯定状态的参与者长时间的延迟。
不依赖协调者获取最终决定的方法是经过参与者协做来得到决定。这种策略的优势是能够在协调者出故障时使用。(在本篇文章中我讨论这种方式)
四、两阶段提交的故障处理
五、两阶段提交的性能
假设一切运转正常,即协调者参与者不出现故障,通讯也正常时,有N个参与者的两阶段提交协议须要N个canCommit消息和应答,而后再有N个doCommit消息。这样消息开销和3N成正比,时间开销是3次消息往返。因为协议在没有haveCommitted消息时仍能够正常运做(它们的做用只是通知服务器删除过期的协调者消息),所以在估计协议开销上,不将haveCommitted消息计算在内。
在最坏的状况下,两阶段提交协议在执行过程当中可能出现任意屡次服务器和通讯故障。尽管协议不能指定协议完成的时间限制,但它能正确处理连续故障(服务崩溃或者消息丢失),并保证最终完成。加Q群:479499375可获取一份Java架构进阶技术精品视频。(高并发+Spring源码+JVM原理解析+分布式架构+微服务架构+多线程并发原理+BATJ面试宝典)
六、使用消息队列来避免分布式事务
6.一、消息队列
因为分布式事务存在严重的性能问题,在设计高并发服务的时候,每每经过其余途径来解决数据一致性问题。
举例来说,你在北京颇有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,而是给你一张小票,而后让你拿着小票到出货区排队去取。为何他们要将付钱和取货两个动做分开呢?缘由不少,其中一个很重要的缘由是为了使他们接待能力加强(并发量更高)。
仍是回到咱们的问题,只要这张小票在,你最终是能拿到炒肝的。同理转帐服务也是如此,当支付宝帐户扣除1万后,咱们只要生成一个凭证(消息)便可,这个凭证(消息)上写着“让余额宝帐户增长 1万”,只要这个凭证(消息)能可靠保存,咱们最终是能够拿着这个凭证(消息)让余额宝帐户增长1万的,即咱们能依靠这个凭证(消息)完成最终一致性。
这样咱们上述的转帐就变成了以下过程:
6.二、重复投递
还有一个严重的问题是消息重复投递,以咱们支付宝转帐到余额宝为例,若是相同的消息被重复投递两次,那么咱们余额宝帐户将会增长2万而不是1万了。