首页 > 代码库 > 关于分布式事务

关于分布式事务

前些日子看到一篇关于乐观锁和悲观锁的blog,看了关于乐观锁和悲观锁的讨论以及在真实项目里的一些例子,也勾引起了我写篇blog的欲望,既然乐观锁和悲观锁是关于并发控制机制相关的知识,那我就来写一篇关于分布式事务的blog,我个人觉得分布式事务是乐观锁悲观锁的延伸阅读。

乐观锁和悲观锁多数是在处理与单个数据库读写交互时的并发控制解决方案,但是现在分布式的场景已经越来越多地占据了我们工作的重点,所以分布式场景下的并发控制怎么做?这就涉及到分布式事务的相关知识了,我觉得理解了分布式事务的相关知识绝对能对我们的工作起到事半功倍的作用!

先不忙着说方法论,我们来看看分布式事务的相关技术是怎么演变的。

数据镜像

假如我们有一个提供数据服务的系统,我们为了保证数据服务的高可用性,我们会如何部署?

一般会部署两套,且数据拷贝一份,这样如果其中的一套系统宕机了,另一套系统仍然会提供数据服务,达到了数据高可用性的目的。

但是,使用数据副本的方式(提供数据的高可用性的服务,数据副本基本是唯一的方式),就有一个“数据一致性”的问题,这个问题其实是分布式事务里最麻烦的问题,而为了解决“数据一致性”问题可能产生性能上的问题,数据一致性和性能的平衡问题其实是我们在做分布式业务系统面临的一个重要问题。

数据一致性:

1)Weak 弱一致性:当你写入一个新值后,读操作在数据副本上可能读出来,也可能读不出来。

2)Eventually 最终一致性:当你写入一个新值后,有可能读不出来,但在某个时间窗口之后保证最终能读出来。

3)Strong 强一致性:新的数据一旦写入,在任意副本任意时刻都能读到新值。

为了解决上述问题,智慧的人类想了很多方法,且各个都是里程碑式的想法

Master-Slave模型

这是我们数据库部署经常使用的模型:主库和备库。如果Slave就是作为Master的backup,那么Master提供数据服务,当Master故障了,则Slave马上顶上去,作为数据服务的提供者,一直等到Master恢复过来,这是个经典的模型。那么这个模型数据一致性怎么来做呢?

Master和Slave之间的数据同步要么是Master去push数据到Slave,要么是Slave自己去Master那pull数据,不管哪种方式一般都是周期性地push和周期性地pull,既然是周期性就肯定是最终一致性。试想一下写数据的数据同步流程:Master先写,如果Master写成功,则Slave写,如果Slave写成功,则流程commit;如果Slave写失败了,则通知Master回滚,返回写操作失败。

还有一种恶心的场景:Master挂了,那么Slave顶上,这时Slave只能提供读服务一直等到Master恢复,这时的写操作就只能delay了;如果Slave在Master挂的期间可以提供写服务,那等Master恢复后,Master的数据和Slave的数据就不一致了,那么还得把Slave与Master数据同步一下,这样想想已经非常麻烦了。
所以Master-Slave并不是完美的方案,还有其他方案吗?

2PC协议(两阶段提交)

维基百科对2PC协议的定义:

二阶段提交(Two-phase Commit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol)。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

两阶段提交其实分为两个阶段:Vote阶段和Decision阶段

两个阶段都有一个重要的角色:协调者

Vote阶段:

  • 协调者询问各个结点是否可以数据提交
  • 各个结点开始做提交的准备工作,然后回应协调者
  • 各个结点回应协调者,如果准备工作OK则回应成功,如果准备工作失败则回应失败

Decision阶段:

  • 协调者收到所有的结点回应,如果收到的回应都是OK,则协调者通知所有结点执行数据提交操作,各个结点完成提交操作后返回协调者“提交完成”,当协调者收到所有结点的“提交完成”后结束整个事务。
  • 当协调者收到一个或多个结点回应准备工作失败,则通知所有结点“回滚操作”,所有结点收到“回滚操作”执行准备工作的回滚操作,然后回应协调者“回滚完成”,协调者收到所有结点的“回滚完成”后结束整个事务。

2PC协议似乎是对分布式事务的处理做了很好的设计,为了避免有结点提交失败而导致的数据一致性问题采取了先Vote后Decision的方式。但是这个协议中还是有问题存在:
Timeout问题

Vote阶段:

  • 协调者发送给各个结点询问是否可以数据提交这个消息如果超时,那么没关系,如果某结点的消息超时那么有可能该结点收到这个消息晚了,执行准备工作也晚了,回应协调者“准备工作OK”的消息也晚了,那么协调者就需要有一个超时机制去控制,比如超时到了一个阀值就视为失败,不要影响后续的操作。
  • 结点回应协调者消息超时了,那么还是上面所说的协调者需要一个超时机制去控制

Decision阶段:

  • Vote阶段完成,协调者会通知所有结点执行提交操作或者准备工作回滚的消息,如果某些结点没收到这消息,那么这些结点就很“纠结”了,因为它不知道下一步要怎么做,而且也不知道其他结点都在“干什么”,进入一种未知的状态。如果给结点上设置个超时,超过多少时间没收到消息就做提交或回滚操作,那么提交还是回滚呢?还是不行。这是2PC协议中最槽糕的情况。

为了解决上述这些问题,尤其是“最槽糕的情况”,后面出来了3PC协议(三阶段提交),但3PC也只是解决了一部分问题。

3PC协议(三阶段提交)

由于在2PC协议中,Vote阶段完成后,结点在第二阶没有收到决策,那么结点会进入未知状态,这个状态会block住整个事务。3PC协议在Decision阶段多加了一个阶段。

  1. 协调者询问各个结点是否同意提交数据
  2. 结点收到消息不需要做准备工作,先返回同意还是不同意。
  3. 协调者收到所有结点的同意信息后再发送结点执行准备提交的消息,如果有一个结点不同意提交数据,则直接Abort
  4. 结点收到协调者发送的执行准备提交的消息后执行提交操作,操作完成后返回给协调者执行成功的消息,失败则返回执行失败的消息
  5. 协调者接收到所有结点的执行成功的消息后则结束整个事务,如果有一个结点返回失败的消息则通知结点回滚操作(这又是极其复杂的流程)。

3PC的协调者和结点的状态图如下:
技术分享

无论是2PC还是3PC,其实都是有瑕疵的解决方案,而且3PC的实现其实非常复杂,为设计周全会去解决流程中各种各样的失败场景。

其实网络环境本来就是一个不可靠的环境,所以试图在一个不可靠的通道(网络)上建立连接并进行协调工作是有巨大挑战的!网络环境的不可靠性就需要你不停地解决通信失败和超时等问题,让你解决得“肛肠寸断”!解决方法就是尽可能地设计出能容忍这种不可靠环境的方案,或者能把这种不可靠性降到一个可接受的程度。

还好,解决方法永远比挫折要多,Paxos算法拯救了我们!

Paxos算法

在上述中的不可靠环境中,是否有一个比较好的解决方案能容忍这种环境或者把不可靠性降到最低且在分布式系统中对某一个操作或者值的变更能达成一致呢?答案是肯定的,Paxos算法就能做到。

维基百科对Paxos算法:

Paxos 算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个「一致性算法」以保证每个节点看到的指令一致。一个通用的一致性算法可以应用在许多场景中,是分布式计算中的重要问题。因此从20世纪80年代起对于一致性算法的研究就没有停止过。

Paxos算法其实是个民主选举的算法,没有协调者这一角色,是由结点之间互相“投票”,如果收到超过半数的支持票则执行操作,具体的流程如下:

假如:有A1,A2,A3三个结点
1. A1结点有一个值的变更操作,在操作之前,A1给A1,A2,A3发送消息让它们投票(这个消息里包括两部分内容:操作请求内容 和 Sequence,这个Sequence是一个Paxos维护的唯一的自增序列)
2. 接收结点收到这个消息后,会先取出这个Sequence去与自己收到的其他结点的消息中得Sequence比较,如果是最大的则回应A1 OK。
3. 如果A1接收到超过半数的OK的话就向所有结点发送Accept Request,反之则失败。
4. 其他结点接收到Accept Request后,同样会先去比较Sequence,如果是最大的则执行操作,如果不是最大的则拒绝操作

上述的Sequence是时间和所发送请求结点的权重的组合,时间和权重作为请求的编号非常有意义。

可能这样描述流程比较抽象,下面讲几个实例,看了实例会立刻明白了:

实例1:有A1, A2, A3, A4, A5五个结点,A1有一个对某值变更为x的操作,A1给所有结点发请求,其中没有异常情况,时序图如下:
技术分享

实例2:一个异常情况,有A1, A3, A5三个结点,A1和A5同时发出修改值的请求,但是A5的请求慢了很久,在A1的提案全部结束后,A3才收到,A3怎么做呢?时序图如下:
技术分享

实例3:一个异常情况,有A1, A3, A5三个结点,A1和A5同时发出修改值的请求,但是A5的请求慢了一会,在A3回复A1 OK后A3收到A5的请求,这时A3怎么做呢?时序图如下:
技术分享

其实还有很多异常情况,大家可以自己去推导,可以发现只要达到半数以上就通过的原则基本都没什么问题。这也是为什么Zookeeper使用Paxos算法的原因

Google Chubby的作者Mike Burrows说过这个世界上只有一种一致性算法,那就是Paxos,其它的算法都是残次品。

总结

这是我个人对分布式系统如何处理事务的学习后的知识汇总,其实我们可能各个知识点都知道,但是如何把这些知识点串起来能够更深刻地理解这些知识,关于分布式事务,还有很多内容,比如Vector Clock、NWR模型等等,后面有空再分享给大家我的学习总结。

资料索引

2PC(http://zh.wikipedia.org/wiki/二阶段提交) 

Paxos维基百科(http://zh.wikipedia.org/zh/Paxos算法) 

Paxos-Simple(http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf)

关于分布式事务