两阶段提交
第一,2PC 是一个阻塞式协议。当 2PC 的一个参与者,在阶段 1 做出了“是”的回复后,参与者将不能单方面放弃,它必须等待协调者的决定,也就意味着参与者所有占用的资源都不能释放。如果协调者出现故障,不能将决定通知给参与者,那么这个参与者只能无限等待,直到协调者恢复后,成功收到协调者的决定为止。因为 2PC 有阻塞问题,所以后来又提出了 3PC 协议,它在 2PC 的两个阶段之间插入了一个阶段,增加了一个相互协商的过程,并且还引入了超时机制来防止阻塞。虽然 3PC 能解决 2PC 由于协调者崩溃而无限等待的问题,但是它却有着超高的延迟,并且在网络分区时,还可能会出现不一致的问题,这些原因导致它在实际应用中的效果并不好,所以目前普遍使用的依然是 2PC 。
第二, 2PC 是一个逆可用性协议。如果在阶段 1 ,任何一个参与者发生故障,使准备请求失败或者超时,协调者都将中止操作;如果在阶段 2 ,协调者发生故障,也会导致参与者只能等待,无法完成操作。你是否感觉很奇怪,同样是共识算法,Raft 和 Paxos 等共识算法都能容忍少数节点失败的情况,那为什么 2PC 则完全不能容忍节点的失败呢?其实,这个差异的出现是因为 2PC 是一个原子提交协议,为了 all-or-nothing ,在操作过程中就需要与所有的节点达成共识;而 Raft 和 Paxos 则只需要与大部分节点达成一致,确保共识成立即可,它可以容忍少数节点不可用,当故障恢复的时候,之前不可用的节点可以向其他正常的节点同步之前达成的共识。
第三,虽然 2PC 能保证事务的原子性,即一个事务所有的操作,要么都成功,要么都失败,但是它并不能保证多个节点的事务操作会同时提交。如果没有同时提交,即一部分节点已经提交成功,而另一部分节点还没有提交的时候,就将使事务的可见性出现问题
缺点总结:
1.需要所有节点达成共识
2.强依赖协调者自身的运行状态,如果协调者不正常,则完事休矣。
TCC
TCC全名是Try-Confirm-Cancel,和两阶段提交一样,它也分为两个阶段,也有一个协调者负责协调整个分布式事务的流程。和两阶段提交不同的是,业务系统需要负责整个分布式事务的执行,而不能全权交给底层的数据库。
在TCC的第一个阶段,协调者要求所有数据库尝试(Try)进行所有本地事务。本地尝试之后将尝试的结果返回给协调者。和2PC两阶段提交不同的时,2PC的第一阶段,事务并没有提交,而是到达了“准备成功”的状态,而在TCC的情况下,事务会真正提交。
TCC第一阶段结束之后,协调者知道了所有节点的状态。如果所有节点的本地事务提交都成功,那么协调者会给所有节点发送确认(Confirm)消息。节点在收到 确认 消息之后进行确认操作。
另外,如果有任何一个节点在第一阶段出了问题,协调者就会给所有节点发送取消(Cancel)的消息。节点在收到 取消 消息之后,会对第一阶段的事务做逆向操作,取消掉第一阶段的影响。
请你注意,TCC的取消操作不是事务的回滚,而是业务的回滚。因为第一阶段已经提交了事务,所以不能对已经提交的事务进行回滚操作。
这时候用到的是事务补偿,也就是说用一个反向业务来对冲正向业务的效果。因此你如果想要实现TCC的话,需要把每个业务实现两遍。一遍是正向的业务,另一遍是反向的业务。
假设和前面一样,一个用户 x 的账户开始有100元钱,账户信息存储在数据库 A 中。另一个用户 y 的账户里最开始没钱,账户信息存储在数据库 B 中。然后系统发起了一笔从 x 到 y 的转账,金额为100元。所以转账后 x 的余额为0,而 y 的余额为100。
我们先看看第一阶段对用户 x 的操作。这一步和两阶段提交基本相同,都是将用户 x 的余额变为0。
和两阶段提交不一样的地方在于对用户 y 的操作。在两阶段提交的情况下,用户 y 会在第一阶段就增加100元钱。但是在TCC的情况下,用户 y 在第一阶段的金额不变。
在TCC第一阶段结束后, x 和 y 账号的钱都为0,因此在这一瞬间整个系统掉了100元钱。不过不用担心,因为在协调者的全局事务数据库里记录了当前TCC的状态,之后会在第二阶段把缺失的100元钱再补回来。
在单机版和两阶段提交的情况下,数据库隐藏了所有上面这些中间细节,因此你会感觉事务有原子性。但是在TCC的情况下,由于业务系统控制了分布式事务的进程,这些中间状态会暴露给业务系统,因此你才能感受到一些临时的不一致状态。(TCC的特点就是你会感知到不一致存在)
TCC是国内互联网用得最多的分布式事务实现方式。它和两阶段提交不一样的地方在于,上层的业务系统需要自己管理分布式事务的进度。上层业务系统需要实现3个方法:尝试提交、确认提交和取消。
TCC的整个过程也分为两个阶段。第一个阶段由协调者和所有节点之间进行尝试提交。之后在第二阶段,协调者根据第一阶段的结果来判断是确认提交还是取消。
TCC和两阶段提交的不同在于,TCC的每个阶段都是完整的本地数据库事务,而两阶段提交只有在第二阶段完成后,本地事务才真正结束。因此TCC的好处是事务的加锁时间短,对应的代价是业务系统复杂,需要感知分布式事务的存在,还需要通过空回滚和防悬挂来解决乱序问题。
事务和共识的关系
首先,对于原子性来说,在分布式系统中,需要通过 2PC 或 3PC 之类的原子提交协议来实现。以 2PC 为例,协调者在第一阶段通过接收所有参与者对 Prepare 请求的响应,才能最终确定当前的事务是提交还是中止,而这就是典型的共识场景:所有的参与者都同意,就提交事务;如果有参与者不同意,就中止事务。所以,我们认为 2PC 或 3PC 之类的原子提交协议是共识协议。
2PC 不是一个完备的共识算法,它满足共识算法的一致同意、诚实性以及合法性,但是在协调者出现故障的时候,并不能满足共识算法的可终止性。
其次,对于隔离性来说,我们一般通过 2PL 或 MVCC 的方式来实现,可是它们能正确实现隔离性的前提条件,建立在底层数据为单副本的基础之上。但是在分布式系统中,为了系统的高可用,底层存储的数据是多副本,为了对事务操作表现出单副本的状态,数据的复制协议必须是线性一致性的,而线性一致性的数据复制协议,通常都是通过共识算法来实现的。学到这里,你会发现特别有意思,我们从事务的隔离性深层次去探索,就会触碰到共识这个话题。
最后,对于持久性来说,在分布式系统中,为了进一步提高事务的持久性,我们会对数据进行复制,通过冗余来提高持久性。虽然数据复制可以不需要共识,但是为了保障事务的隔离性,数据的复制必须是线性一致性的。所以我们可以得出,事务为了持久性而引入了数据复制,但是为了保障隔离性,只能选择线性一致性的数据复制算法,而一旦涉及线性一致性,就说明我们又回到共识了。
你是否会感觉到在分布式系统中,当我们为了实现一个确定性正确的程序,一步一步深挖下去,就一定会碰到共识问题呢?其实这一点很好理解,比如在现实生活中,多人合作完成一件事情,如果人们的意见不能达成一致,是很难将事情正确完成的。想要使他们的意见达成一致,就是共识问题了,人们通过沟通来达成共识,计算机节点之间通过交换信息来达成共识,本质上都是一样的。
我们简单回忆一下事务的隔离级别:读未提交 (Read Uncommitted)、读已提交 (Read Committed)、可重复读 (Repeatable Read)、快照隔离级别 (Snapshot Isolation) 和串行化 (Serializable) ,从隔离级别的名称和异常情况中,我们都不难发现,隔离级别都是从读异常情况的角度来定义的(其中,脏写和写倾斜也可以看成是,由于脏读和幻读导致的写异常),那么这是为什么呢?
其实这是由于事务面对的数据存储,是单副本数据或线性一致的多副本,单个写操作完成后,读操作都是可以立即读取到的,所以在单个写操作的层面,事务是不会出现异常情况的。但是,由于事务一般都涉及对多个数据对象的读写操作,为了避免并发事务的相互影响,事务需要将还未提交的写操作结果,与其他并发事务进行隔离处理,那么如何实现隔离呢?
既然写操作已经实际发生了,那就只能通过读操作进行隔离了,即将一个事务时间内多个离散的写操作,通过对读操作在并发事务之间隔离的方式,使事务的多个操作对外表现为一个原子操作一样。
我们不难看出线性一致性、顺序一致性、因果一致性和最终一致性,这四种线性一致性模型讨论的都是,对单个数据对象操作时,单节点或多节点的多个写操作的顺序,以及复制时延的问题。在数据一致性的模型中,读异常都是由于对单个数据对象的写操作,在多个副本之间的不同原子同步导致的。(操作对象是单个还是多个,读节点是单个还是多个)
到这里,我们会发现事务和数据一致性是非常类似的,它们本质上都是期望它的一个完整操作是原子操作,研究的本质问题都是数据的一致性问题。
事务对一个完整操作的定义是,一个事务内,对一个或多个数据对象的一个或多个读写操作,它需要解决的是对多个数据对象操作的一致性问题;
数据一致性对一个完整操作的定义是,在多个数据副本上对一个数据对象的写操作,它要解决的是单个数据操作,复制到多个副本上的一致性问题。