提到分布式系统,分布式事务是经常被大家提起的话题,也是经常在我们编码或是系统设计时遇到的问题,很常见。
如果让大家说一种解决分布式场景下分布式事务解决方案,相信大部分同学首先会提到两阶段提交,两阶段提交确实是分布式事务处理中最经典的一种强一致,中心化的事务解决方案。
虽然两阶段提交协议被很多人诟病其性能存在问题,但并不失为一种可被简单理解的解决方案,其实很多其他的分布式事务中间件或是分布式事务框架底层的实现细节还是借助于两阶段搞定的。
两阶段提交是一种中心化副本控制协议,参与的节点分为两种:
- 中心化协调节点
- N个参与者节点
实现思路是:
第一阶段,协调者询问所有参与者是否可以提交事务,所有参与者向协调者回复。
第二阶段,协调者根据所有参与者的回复作出是否全局提交事务的决定,并通知所有参与者执行所做的决定。
如果所有参与者回复可以提交,则成功提交事务。只要有一个参与者不允许提交事务,则整个事务放弃。
协调者的实现细节如下:
- 本地日志记录“begin_transaction”,并进入wait状态;
- 向所有参与者发出“prepare”消息;
- 等待左右参与者返回“prepare”的响应。 1. 如果收到任何一个参与者失败消息,则协调者本地日志记录“global-abort”,进入abort状态,并向所有参与者发送“abort”消息,参与者们进入abort状态。 2. 如果所有参与者返回返回commit消息,则协调者本地日志记录“global-commit”,进入commit状态,向所有参与者发送“commit”消息。
- 等协调者接收到参与者针对“abort”或“commit”的响应后,进行本地记录“end_transaction”日志结束事务;
参与者实现细节如下:
- 本地记录日志“init”,进入init状态
- 等待并接收协调者发送的“prepare”消息; 1. 如果该参与者可以提交本次事务,则在本地日志记录“ready”,进入ready状态,同时向协调者发送“commit”消息,进入等待协调者再次消息状态。 2.1 如果得到协调者“abort”消息后,则本地日志记录“abort”,进入abort状态,并向协调者发送“abort”消息。 2.2 如果得到协调者“commit”消息后,则本地日志记录“commit”,进入commit状态,并向协调者发送“commit”消息
- 每次收到协调者发送的消息都要发送一个确认消息;
我们前面文章中提到过,日志之于分布式系统的应用,可以利用日志解决崩溃恢复问题,上面的实现细节中,协调者和参与者通过记录日志追踪执行状态,我们就可以利用日志解决崩溃恢复问题了。
协调者宕机恢复
协调者恢复过程中,通过日志查找找到宕机之前的状态。
如果日志最后记录的是“begin_transaction”,说明宕机前协调者处于wait状态,协调者可能发送了"prepare"消息,也可能没有发。但协调者一定没有发送过"commit"消息或“abort”消息,此时协调者可以重新发送"prepare"消息,继续两阶段提交流程。即使参与者已发送过对于"prepare"消息的响应,也不过是再次重传之前的响应,不会影响协议的一致性。
如果日志最后记录的是“global-commit”或“global-abort”记录,说明宕机前,协调者处于commit或abort状态,此时协调者重新向所有参与者发送commit消息或abort消息,就可以继续两阶段提交过程了。
参与者宕机恢复
参与者恢复过程中,同样查看本地日志查找宕机前状态,如果日志最后记录的是“init”,说明参与者处于init状态,还没有对事务作出响应,参与者可以继续流程等待协调者发送“prepare”消息。
如果日志记录的是“ready”消息,说明参与者处于ready状态,此时说明参与者已经对于本次事务作出了响应,但不知道是否向协调者发出了响应。此时参与者可以向协调者重新发送“commit”消息,并进行执行协议流程。
如果日志记录的是“commit”或“abort”,说明参与者已经收到协调者的“global-commit”消息或“global-abort”消息。但是是否对协调者进行了响应不得而知,可以继续等待协调者的消息,不影响整体协议流程。
大家对于两阶段的诟病主要在于其容错能力较差,比如中间存在大量异常风险,也很难判断后续的流程状态。我们可以借助前文提到的lease机制,在lease生效期内,可以保证协议不受阻塞的执行下去,从而提高了两阶段提交的容错性。
还有一个缺点在于性能较差,需要参与者和协调者之间多次进行网络通信和确认,流程上包含“prepare”,“commit”,“global-commit”和参与者的ack消息,大量网络通信造成性能降低,任何一个参与者不能正常ack都会拖慢整个协议流程。
分布式系统中如何解决并发控制也是一道难题,技术分为宏观技术和微观技术,很多技术可以在宏观和微观下解决问题,比如我们可以借助于innodb的mvcc技术实现分布式场景下的并发处理。
mvcc最初是在数据库系统下提出来的,就是多个不同版本的数据实现并发控制。基本思想是每次事务生成一个新的版本数据,在读取这个数据时,选择不同版本的数据以实现对事务结果的隔离和完整性读取。
mvcc中每个事务都是基于一个已经生效的基础版本进行更新,事务可以并行进行:
比如数据版本为1,同时两个事务a和b对数据进行了本地修改,这些修改由于隔离性原因,只有事务本身可见,不影响全局数据。
之后事务a首先提交,生成了数据的版本2,之后基于数据版本2发起了事务c,事务c继续提交形成数据3。
之后事务b提交,此时事务b的数据结果需要和事务c的数据结果合并,如果数据没有冲突,则事务b正常提交,否则事务b提交失败。
我们可以发现mvcc的思路类似于我们的代码版本控制系统。
而针对于多版本下强一致性的实现协议,可以基于paxos做变种,paxos是一种强一致性,去中心化的分布式协议,整个思想较为复杂很难说的特别清楚,但是核心思想并不难理解。
paxos体系下存在一系列完全对等的参与者节点,叫做acceptor,每个节点都可以提出一个议题,节点对于议题采用投票方式,如果某个议题获得了超过半数节点的支持,则提议生效。所以在paxos协议下,只要超过一般节点正常,就可以正常工作,解决了网络分区和节点宕机的风险。
paxos下存在以下几种角色:
proposer:提案者,可以存在多个,主要负责提案,paxos中统一将提案称为value。
acceptor:批准者,可以存在多个,针对于提案value必须有超过半数的acceptor同意后才能通过。acceptor之间相互独立。
learner:学习者,主要读取proposer中value的值,进行学习结果。
Paxos 协议是被人为设计出来,其设计过程也是协议的推导过程。
Paxos 协议利用了Quorom 机 制,选择的W=R=N/2 1。简单而言,协议就是Proposer 更新Acceptor 的过程,一旦某个Acceptor 成功更新了超过半数的Acceptor,则更新成功。
Learner 按Quorum 去读取Acceptor,一旦某个value 在超过半数的Proposer 上被成功读取,则说明这是一个被批准的value。协议通过引入轮次,使得高轮次的提议抢占低轮次的提议来避免死锁。协议设计关键点是如何满足“在一次Paxos 算法实例过程中只批准一个Value”这一约束条件。
由于Paxos协议本身较为复杂,大家可以找一些更为详细的论文进行学习。
本文主要介绍了分布式系统下的一些常用协议,以解决分布式事务,分布式并发,和分布式一致性的协调等问题。