对于单机下的本地事务,很显然咱们有已被实践证实的成熟 ACID 模型来保证数据的严格一致性。但对于一个高访问量、高并发的分布式系统来讲,若是咱们指望实现一套严格知足 ACID 特性的分布式事务,极可能出现的状况就是在系统的可用性和严格一致性之间出现冲突——由于当咱们要求分布式系统具备严格一致性时,极可能就要牺牲掉系统的可用性。但毋庸置疑的一点是,可用性又是一个全部用户不容许咱们讨价还价的属性,好比像淘宝这样的网站,咱们要求它 7x24 小时不间断地对外服务。所以,咱们须要在可用性和一致性之间作一些取舍,围绕这种取舍,出现了两个经典的分布式理论——CAP 和 BASE,这二者也是全部分布式事务协议的基石。数据库
1、CAP 定理网络
CAP 首次在 ACM PODC 会议上做为猜测被提出,两年后被证实为定理,今后深深影响了分布式计算的发展。CAP 理论告诉咱们,一个分布式系统不可能同时知足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三个基本需求,最多只能同时知足其中的两项。架构
一致性:数据在多个副本之间保持一致。当有一个节点的数据发生更新后,其它节点应该也能同步地更新数据。并发
可用性:对于用户的每个操做请求,系统总能在有限的时间内返回结果。分布式
分区容错性:分布式系统中的不一样节点可能分布在不一样的子网络中,这些子网络被称为网络分区。因为一些特殊缘由致使子网络之间出现网络不连通的状况,系统仍须要可以保证对外提供一致性和可用性的服务。ide
CAP 定理告诉了咱们同时知足这三项是不可能的,那么放弃其中的一项会是什么样的呢?高并发
放弃项性能
放弃P : 若是但愿可以避免出现分区容错性问题,一种较为简单的作法是将全部数据放在一个节点上。这样确定不会受网络分区影响。但此时分布式系统也失去了意义。所以在实际的架构设计中,P是必定要知足的。网站
放弃A: 放弃可用性就是在系统遇到网络分区或其余故障时,受影响的服务能够暂时不对外提供,等到系统恢复后再对外提供服务。spa
放弃C: 放弃一致性不表明彻底放弃数据一致性,这样的话系统就没有意义了。而是放弃数据的强一致性,保留最终一致性。这样的系统没法保证数据保持实时的一致性,但可以承诺数据最终会达到一个一致的状态。
实际的实现中,咱们每每会把精力花在如何根据业务特色在 C(一致性)和 A(可用性)之间寻求平衡。
2、BASE 理论
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的简写。BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结。其核心思想是:即便没法作到强一致性,但每一个应用均可以根据自身的业务特色,采用适当的方式来使系统达到最终一致性。
基本可用:基本可用是指在分布式系统出现不可预知的故障时,容许损失部分性能。好比:正常状况下 0.5 秒就能返回结果的服务,但在故障状况(网络分区或其余故障)下,须要 1~2 秒;正常状况下,电商网站的首页展现的是每一个用户个性化的推荐内容,但在节日大促的状况下,展现的是统一的推荐内容。
软状态:软状态是指运行系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的总体可用性,即容许系统在不一样节点的数据副本之间进行数据同步的过程存在延时。好比秒杀系统中,用户余额的扣减和商家余额的增长能够存在延时,当用户余额减了以后便可返回支付成功,商家余额的增长能够等系统压力小的时候再作。
最终一致性:最终一致性强调的是系统中全部的数据副本,在通过一段时间的同步后,最终能达到一个一致的状态。这也是分布式系统的一个基本要求。
严格遵照 ACID 的分布式事务咱们称为刚性事务,而遵循 BASE 理论的事务咱们称为柔性事务。在分布式环境下,刚性事务会让系统的可用性变得难以忍受,所以实际生产中使用的分布式事务都是柔性事务,其中使用最多的就是 2PC、3PC 和 TCC。
3、2PC 协议
2PC 是二阶段提交(Two-phase Commit)的缩写,顾名思义,这个协议分两阶段完成。第一个阶段是准备阶段,第二个阶段是提交阶段,准备阶段和提交阶段都是由事务管理器(协调者)发起的,协调的对象是资源管理器(参与者)。二阶段提交协议的概念来自 X/Open 组织提出的分布式事务的规范 XA 协议,协议主要定义了(全局)事务管理器和(局部)资源管理器之间的接口。XA 接口是双向的系统接口,在事务管理器以及一个或多个资源管理器之间造成通讯桥梁。Java 平台上的事务规范 JTA(Java Transaction API)提供了对 XA 事务的支持,它要求全部须要被分布式事务管理的资源(由不一样厂商实现)都必须实现规定接口(XAResource 中的 prepare、commit 和 rollback 等)。
两阶段以下:
准备阶段:协调者向参与者发起指令,参与者评估本身的状态,若是参与者评估指令能够完成,参与者会写 redo 和 undo 日志,而后锁定资源,执行操做,可是并不提交。
提交阶段:若是每一个参与者明确返回准备成功,也就是预留资源和执行操做成功,协调者向参与者发起提交指令,参与者提交资源变动的事务,释放锁定的资源;若是任何一个参与者明确返回准备失败,也就是预留资源或者执行操做失败,协调者向参与者发起停止指令,参与者取消已经变动的事务,执行 undo 日志,释放锁定的资源。
两阶段提交协议成功场景示意图以下:
咱们看到两阶段提交协议在准备阶段锁定资源,是一个重量级的操做,并能保证强一致性,可是实现起来复杂、成本较高,不够灵活,更重要的是它有以下致命的问题:
阻塞:从上面的描述来看,对于任何一次指令必须收到明确的响应,才会继续作下一步,不然处于阻塞状态,占用的资源被一直锁定,不会被释放。
单点故障:若是协调者宕机,参与者没有了协调者指挥,会一直阻塞,尽管能够经过选举新的协调者替代原有协调者,可是若是以前协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接受,而且参与者接收后也宕机,新上任的协调者没法处理这种状况。
脑裂:协调者发送提交指令,有的参与者接收到执行了事务,有的参与者没有接收到事务,就没有执行事务,多个参与者之间是不一致的。
上面全部的这些问题,都是须要人工干预处理,没有自动化的解决方案,所以两阶段提交协议在正常状况下能保证系统的强一致性,可是在出现异常状况下,当前处理的操做处于错误状态,须要管理员人工干预解决,所以可用性不够好,这也符合 CAP 定理的一致性和可用性不能兼得的原理。
4、3PC 协议
三阶段提交协议(3PC 协议)是两阶段提交协议的改进版本。它经过超时机制解决了阻塞的问题,而且把两个阶段增长为三个阶段:
询问阶段:协调者询问参与者是否能够完成指令,协调者只须要回答是仍是不是,而不须要作真正的操做,这个阶段参与者在等待超时后会自动停止。
准备阶段:若是在询问阶段全部的参与者都返回能够执行操做,协调者向参与者发送预执行请求,而后参与者写 redo 和 undo 日志,锁定资源,执行操做,可是不提交操做;若是在询问阶段任何参与者返回不能执行操做的结果,则协调者向参与者发送停止请求,这里的逻辑与两阶段提交协议的的准备阶段是类似的,这个阶段参与者在等待超时后会自动提交。
提交阶段:若是每一个参与者在准备阶段返回准备成功,也就是预留资源和执行操做成功,协调者向参与者发起提交指令,参与者提交资源变动的事务,释放锁定的资源;若是任何一个参与者返回准备失败,也就是预留资源或者执行操做失败,协调者向参与者发起停止指令,参与者取消已经变动的事务,执行 undo 日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致。
三阶段提交协议成功场景示意图以下:
这里与两阶段提交协议有两个主要的不一样:
增长了一个询问阶段,询问阶段能够确保尽量早的发现没法执行操做而须要停止的行为,可是它并不能发现全部的这种行为,只会减小这种状况的发生。
增长了等待超时的处理逻辑,若是在询问阶段等待超时,则自动停止;若是在准备阶段以后等待超时,则自动提交。这也是根据几率统计上的正确性最大。
三阶段提交协议相比二阶段提交协议,避免了资源被无限锁定的状况。但也增长了系统的复杂度,增长了参与者和协调者之间的通讯次数。
5、TCC 协议
不管是 2PC 仍是 3PC,都存在一个大粒度资源锁定的问题。为了解释这个问题,咱们先来想象这样一种场景,用户在电商网站购买商品1000元,使用余额支付800元,使用红包支付200元。咱们看一下在 2PC 中的流程:
prepare 阶段:
下单系统插入一条订单记录,不提交
余额系统减 800 元,给记录加锁,写 redo 和 undo 日志,不提交
红包系统减 200 元,给记录加锁,写 redo 和 undo 日志,不提交
commit 阶段:
下单系统提交订单记录
余额系统提交,释放锁
红包系统提交,释放锁
为何说这是一种大粒度的资源锁定呢?是由于在 prepare 阶段,当数据库给用户余额减 800 元以后,为了维持隔离性,会给该条记录加锁,在事务提交前,其它事务没法再访问该条记录。但实际上,咱们只须要预留其中的 800 元,不须要锁定整个用户余额。这是 2PC 和 3PC 的局限,由于这二者是资源层的协议,没法提供更灵活的资源锁定操做。为了解决这个问题,TCC 应运而生。TCC 本质上也是一个二阶段提交协议,但和 JTA 中的二阶段协议不一样的是,它是一个服务层的协议,所以开发者能够根据业务自由控制资源锁定的粒度。咱们等会儿能够看到 TCC 在上面这个场景中的优点,但在那以前,咱们先来看一下 TCC 协议的运行过程。
TCC 将事务的提交过程分为 try-confirm-cancel(实际上 TCC 就是 try、confirm、cancel 的简称) 三个阶段:
try:完成业务检查、预留业务资源
confirm:使用预留的资源执行业务操做(须要保证幂等性)
cancel:取消执行业务操做,释放预留的资源(须要保证幂等性)
和 JTA 二阶段事务的参与方都要实现 prepare、commit、rollback 同样,TCC 的事务参与方也必须实现 try、confirm、cancel 三个接口。流程以下:
事务发起方向事务协调器发起事务请求,事务协调器调用全部事务参与者的 try 方法完成资源的预留,这时候并无真正执行业务,而是为后面具体要执行的业务预留资源,这里完成了一阶段。
若是事务协调器发现有参与者的 try 方法预留资源时候发现资源不够,则调用参与方的 cancel 方法回滚预留的资源,须要注意 cancel 方法须要实现业务幂等,由于有可能调用失败(好比网络缘由参与者接受到了请求,可是因为网络缘由事务协调器没有接受到回执)会重试。
若是事务协调器发现全部参与者的 try 方法返回都 OK,则事务协调器调用全部参与者的 confirm 方法,不作资源检查,直接进行具体的业务操做。
若是协调器发现全部参与者的 confirm 方法都 OK 了,则分布式事务结束。
若是协调器发现有些参与者的 confirm 方法失败了,或者因为网络缘由没有收到回执,则协调器会进行重试。这里若是重试必定次数后仍是失败,会怎么样?常见的是作事务补偿。
TCC 执行场景示意图以下:
如今咱们再回到开始的那个支付场景中,看看 TCC 在该场景中的流程:
Try操做
tryX 下单系统建立待支付订单
tryY 冻结帐户红包 200 元
tryZ 冻结资金帐户 800 元
Confirm操做
confirmX 订单更新为支付成功
confirmY 扣减帐户红包 200 元
confirmZ 扣减资金帐户 800 元
Cancel操做
cancelX 订单处理异常,资金红包退回,订单支付失败
cancelY 冻结红包失败,帐户余额退回,订单支付失败
cancelZ 冻结余额失败,帐户红包退回,订单支付失败
能够看到,咱们使用了冻结代替了原先的帐号锁定(实际操做中,冻结操做能够用数据库减操做+日志实现),这样在冻结操做以后,事务提交以前,其它事务也能使用帐户余额,提升了并发性。
总结一下,相比于二阶段提交协议,TCC 主要有如下区别:
2PC 位于资源层而 TCC 位于服务层。
2PC 的接口由第三方厂商实现,TCC 的接口由开发人员实现。
TCC 能够更灵活地控制资源锁定的粒度。
TCC 对应用的侵入性强。业务逻辑的每一个分支都须要实现 try、confirm、cancel 三个操做,应用侵入性较强,改形成本高。