分布式事务的总结与思考
MayaSwallow
7年前
<p>思来想去,个人觉得要理解 「分布式事务」 ,必须先知道什么是“事务(Transaction)”。</p> <p>当然,这里提到的“事务”是在事务型数据库(Transactional Database)知识领域内的。</p> <p>一个事务包含了若干个数据库操作,这些操作通常都会对数据库产生变化。值得一提的是,多个事务之间是互不影响,独立运行的,事务里的各个操作最终都得以持久化。</p> <p>事务一个很重要的特性是: "all-or-nothing" 。</p> <p>通俗来讲,事务是对数据的一个逻辑操作,事务内的各个单元操作要么完整执行成功,要么全部都不执行。</p> <p>因此,数据库的事务机制为一系列的数据库操作提供了相对独立(隔离)的运行环境,保证了 数据的一致性 ,并且使得数据库能被正确的恢复过来。</p> <h2>0、事务的特性</h2> <p>这一小节讲的是数据库事务四个经典的特性: 「ACID」 。</p> <p>读者如果熟悉,可以选择性略过。</p> <p>「ACID」是四个单词的首字母缩写,首次被提出来是在1983年。</p> <p>与其说这是事务的四大特性,倒不如认为是一种设计思想,这种思想在以后影响了数据库系统开发的方方面面。</p> <p>Atomicity - 原子性</p> <p>这个特性在上文中也有提到过,即 "all or nothing":一个事务中的所有操作都顺利完成了,才可以认为这个事务是成功的,否则,但凡有一个操作失败了,就意味着整个事务的失败,数据库的相关数据状态也就不会被改变。</p> <p>值得一提的是,操作失败不仅仅是业务或者编码层面,也包括一些外部因素,比如断点,磁盘故障等。</p> <p>Consistency - 一致性</p> <p>讲到事务的一致性,通常指的是数据库相关的约束条件,包括数据库自带的约束以及用户自定义的约束,比如数据的唯一性(unique),数据的级联相关性(cascade),触发器(trigger)等。</p> <p>这一系列的约束旨在保证事务能将数据库从一个合理的状态转移到另一个合理的状态,而所写入的数据也都是遵从上述定义的约束条件。</p> <p>当然,这里所提到的一致性,没法从更高的应用层面(通常是业务层)保证数据一致性,仅仅是从程序上规避编码错误导致对约束条件的破坏。比如插入了一个已经存在的主键值,或者没给一个NOT NULL字段赋上一个合理的值……</p> <p>Durability - 持久性</p> <p>这个特性比较好理解,一个事务一旦被成功提交,那么所操作的数据将被永久存放在磁盘里了。持久化到磁盘,也是为了防止断电导致数据丢失,直接放内存中,显然不是明智的选择。</p> <p>Isolation - 隔离性</p> <p>这个特性是最晚被提出来的,Jim Gray最早只提出上述的三个特性,后来Andreas Reuter和 Theo Härder在此基础上,增加了隔离性。</p> <p>隔离性的提出,是为了解决事务并发的问题,是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。</p> <p>从隔离性的使用场景能感受到,提出此特性也是有时代背景的,也是人们为了解决并发控制问题而提出的对策。</p> <h2>1、何为「分布式事务」</h2> <p>在事务的基础上,加上了「分布式」,这意味着要在分布式的环境下满足事务的ACID特性。而分布式意味着网络可能是异构的,操作系统也不尽相同,数据库系统也不只在一台主机上了。</p> <p>实际上,随着数据规模的急剧增长,单体数据库无法承载如此海量的数据,需要对原有数据进行合理的分库分表,此外,对于近来盛行的「微服务」架构,每个服务独立部署,有独立的数据库,也不得不面临分布式事务的问题。</p> <p>总之,随着分布式架构的大规模运用,分布式事务是不可回避的问题。</p> <p>如果我们将「分布式事务」认为是一种事务,那么又该如何设计,使其满足ACID四大特性呢?</p> <h2>2、两段式提交</h2> <p>最经典的分布式事务解决方法就是“两段式提交(two-phase commit)”。</p> <p>在两段式提交过程中,涉及两类角色, 协调者(Coordinator) 和 参与者(Participants) 。</p> <p>顾名思义,“两段式提交”将事务的提交过程分为了两个阶段:</p> <ul> <li> <p>第一个阶段:预提交阶段,也可以称之为投票阶段。在这个阶段,协调者询问所有的参与者是否已准备好提交事务,参与者通常都会给出一个“YES or NO”的回答,即我们认为的投票过程。根据事务的Atomic特性,但凡有一个参与者Say NO,那么协调者就认为这个事务提交是失败的。只有全部参与者给出“YES”的回答,才能让事务顺利提交。</p> </li> </ul> <ul> <li> <p>第二个阶段:提交决定阶段。协调者根据上一个阶段的投票结果决定是Commit还是Abort,这个决定是全局性的,会通知到所有的参与者执行最终的决定,并回传一个ack确认信息。</p> </li> </ul> <p>值得注意的是,在进入两段式提交过程期间,所有的参与者不能临时改变主意,即投票不可反悔,下面是两段式提交过程的示意图:</p> <p><img src="https://simg.open-open.com/show/4ca153d2273cc3a1477035d74e82e5c6.jpg"></p> <p>two-phase commit protocol</p> <p>仔细想想,两段提交协议也并不完美。</p> <p>两段式提交时一个典型的 中心化架构 协议,被指定为协调者的节点如果没有做高可用措施的话,协调者的宕机意味着事务再也无法正常提交了。与此同时,各个节点(包括协调者和参与者)只有在永无故障的前提下(虽然绝大多数的时候是OK的),才能使得事务顺利提交。</p> <p>这些假设条件无疑是严苛的,因为要达到 强一致性 的目的。</p> <p>不过,这也使得两段式提交协议在某些时候是比较脆弱的。两段式提交协议的性能比较差, 消息交互多,且受最慢节点影响。</p> <p>基于两段式提交的分布式事务在提交事务时需要在多个节点之间进行协调,最大限度地推后了提交事务的时间点。</p> <p>客观上延长了事务的执行时间,同时也会</p> <p>提高事务在访问共享资源时发生冲突和死锁的几率</p> <p>。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平伸缩的"枷锁"。 这是很多Sharding系统不采用分布式事务的主要原因。</p> <p>就好比你在旅游网站上订了一个行程,除了生成一个出行订单外,还需要去航空公司为你预订相应的机票,还要去酒店为你预订每天的房间。如果根据两段式提交协议,只有当订单、机票和房间都准备好了,这个行程才算预订好了,这在显然是不合理的。对于这种长生命周期的分布式事务就不适合两段式提交。</p> <h2>3、三段式提交</h2> <p>显然,三段式提交协议是基于两段式提交而生的,为了解决两段式提交带来的阻塞等待问题,三段式提交引入TIMEOUT机制,可在超时后自动释放资源。</p> <p>和两段式提交一样,三段式提交协议有两类角色, 协调者(Coordinator) 和 参与者(Participants) ,由三个阶段构成。</p> <ul> <li> <p>第一个阶段:询问阶段。协调者询问每个参与者是否可以进行提交,这时候会出现多种情况。参与者明确自己是否能提交,可以给出“YES or NO”的准确回答,也有可能因为各种因素,导致不能确定,直到此次询问超时,返回“NO”。</p> </li> </ul> <ul> <li> <p>第二个阶段:预提交阶段。根据上阶段得到的应答,协调者决定事务Commit or Abort,将投票最终结果发送给各个参与者,参与者收到此决定后再继续下面的操作,只不过到了此阶段,双方都有超时机制了。协调者也有可能因为各种原因不能及时做出决定,超时后就自动给出了Abort决定,与此同时,参与者收到了协调者的决定,需要回传ACK信息以确定,如果没有在规定的时间窗口内确认,协调者认为事务应该Abort。</p> </li> </ul> <ul> <li> <p>第三个阶段:正式提交阶段。在上一个阶段,各个参与者已经收到了事务Commit or Abort的确认信息,其实这个阶段可以认为是一个二次确认阶段,协调者会发送一个DoCommit指令,参与者才真正开始进行事务的操作,并给协调者回复一个ACK。如果此时协调者接收ACK超时,协调者也会Abort整个事务。值得注意的是,如果协调者本身发送DoCommit就超时了,参与者也不会直接Abort事务,而是按照第二个阶段的结果执行。</p> </li> </ul> <p>下面附上两段式提交与三段式提交的框架图:</p> <p><img src="https://simg.open-open.com/show/ec143d47b218e6f39f1c9c81a63283ac.jpg"></p> <h2>4、TCC</h2> <p>TCC包含了三个阶段: T ry, C onfirm, C ancel,因此而得名「TCC」。</p> <p>TCC的概念属于国产,因为支付宝的技术布道而广为人知。</p> <p>其实,TCC算是一种编程模型,通常被理解为是一种柔性事务解决方案。</p> <ul> <li> <p>Try ,尝试执行业务。完成所有业务检查,预留相应的业务资源。</p> </li> </ul> <ul> <li> <p>Confirm ,如果Try阶段执行成功,则此阶段利用Try阶段预留的资源,不再进行业务检查,而是执行真正的业务提交。并且Confirm阶段的操作满足幂等性,以便支持重试。这个阶段有一个假设:只要Try阶段成功,那么Confirm阶段一定成功。</p> </li> </ul> <ul> <li> <p>Cancel ,此阶段是发生在Try阶段出现失败的时候,回滚之前的操作,释放Try阶段预留的业务资源,同样也满足幂等性。</p> </li> </ul> <p>借用一个例子:</p> <p>用户下完订单后,使用红包帐户和资金帐户来付款,红包帐户服务和资金帐户服务在不同的系统中。一个是CapitalTradeOrderService,代表着资金帐户服务,另一个是RedPacketTradeOrderService,代表着红包帐户服务。</p> <p>下完订单后,订单状态为DRAFT,在TRY阶段,订单支付服务将订单状态变成PAYING,同时远程调用红包帐户服务和资金帐户服务,将付款方的余额减掉(预留业务资源);</p> <p>如果在Try阶段,任何一个服务失败,将会调用这些服务对应的cancel方法,订单支付服务将订单状态变成PAY_FAILED,同时远程调用红包帐户服务和资金帐户服务,将付款方余额减掉的部分增加回去;</p> <p>如果Try阶段正常完成,则进入Confirm阶段,在Confirm阶段,订单支付服务将订单状态变成CONFIRMED,同时远程调用红包帐户服务和资金帐户服务对应的Confirm方法,将收款方的余额增加。</p> <h2><em>总结</em></h2> <p>本文对事务进行了简单介绍,重点分析了事务的ACID四大特性,接着介绍分布式事务的解决方案。</p> <p>希望对你有所帮助!</p> <p>THANKS!</p> <p> </p> <p>来自:http://mp.weixin.qq.com/s/UKNK9UzdiZzrNCd5U_4Ytg</p> <p> </p>