@boothsun
2018-05-01T10:49:31.000000Z
字数 8048
阅读 1035
ZK
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上说,分布式事务就是为了保证不同数据库的数据一致性。
服务化,随着服务化,出现各个微服务,以及这些服务对应的库表,多个库表之间的数据操作 可能需要保证原子性。
CAP理论告诉我们,一个分布式系统不可能同时满足 一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partion tolerance)这三个基本需求,最多只能同时满足其中的两项。
在分布式环境下,一致性是指数据在多个副本之间是否能够保持一致的特性。
可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回正确结果。
分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
网络分区是指在分布式系统中,不同的节点分布在不同的子网络(机房或异地网络等)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状况,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干孤立的区域。需要注意的是,组成一个分布式系统的每个节点的加入与退出都可以看作是一个特殊的网络分区。
BASE理论指的是:
上面的过程在形式上近似是协调者组织各参与者对一次事务操作的投票表态过程,因此这个阶段也被称为“投票阶段”,即各参与者投票表明是否要继续执行接下去的事务提交操作。
在阶段二中,协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包含以下两种可能:
执行事务提交:
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交。
中断事务
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
两阶段提交将一个事务的处理过程分为了投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式。
两阶段提交协议的优点:原理简单,实现方便。
两阶段提交协议的缺点:
三阶段提交,将两阶段提交协议的“提交事务内容”过程一分为二,形成了由CanCommit、PreCommit和DoCommit三个阶段组成的事务处理协议。
在阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作,正常情况下,包含两种可能。
执行事务预提交:
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交。
中断事务:
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
该阶段将进行真正的事务提交,会存在以下两种可能的情况。
执行提交
中断事务:
如果有任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
需要注意的是,一旦进入阶段三,可能会存在以下两种故障:
无论出现那种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort请求,针对于这样的异常情况,参与者都会在等待超时之后,继续进行事务提交。(乐观态度,认为前面已经进行过PreCommit请求了,事务一定能执行成功)。
三阶段提交协议的优点:相较于两阶段提交协议,三阶段提交协议最大的优点是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。
三阶段提交协议的缺点:三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和部分参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。
ZooKeeper并没有完全采用Paxos算法,而是使用了一种称为ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子消息广播协议)的协议作为其数据一致性的核心算法。
ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。
ZAB并不是分布式事务的解决方案,而是分布式(主从)各副本之间的数据一致性解决方案。
ZAB的核心是定义了对于那些会改变ZooKeeper服务器数据状态的事务请求的处理方式。
ZooKeeper只允许唯一的一个Leader服务器来接收并处理客户端的所有事务请求,非Leader服务器即使接收到事务请求,也会转发给Leader服务器。
所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,而余下的其他服务器则成为Follower服务器。Leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器。之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。
这是一个类似于两阶段提交的过程,但是需要注意此处是只要求有超过半数的Follower服务器进行了正确的反馈即提交事务;并且在ZAB协议的提交过程中,移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务Proposal,要么抛弃Leader服务器。
在整个消息广播中,Leader服务器会为每个事务请求生成对应的Proposal来进行广播,并且在广播事务Proposal之前,Leader服务器会首先为这个事务Proposal分配一个全局单调递增的唯一ID,我们称之为事务ID(ZXID)。由于ZAB协议需要保证每一个消息严格的因果关系,因此必须将每一个Proposal按照其ZXID的先后顺序来进行排序和处理。
这个ZXID是一个64位的数字,其中低32位可以看作是一个简单的单调递增的计数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal的时候,都会对该计数器进行加1操作。而高32位则代表了Leader周期epoch的编号,每当选举产生一个新的Leader服务器,就会从这个Leader服务器上取出其本地日志中最大事务Proposal的ZXID,并从该ZXID中解析出对于的epoch值,然后再对其进行加1操作,之后就会以此编号作为新的epoch,并将低32位置0来开始生成新的ZXID。
具体的保证顺序性的解决办法就是,在消息广播过程中,Leader服务器会为每一个Follower服务器都各自分配一个单独的队列,然后将需要广播的事务Proposal依次放入这些队列中,并且根据FIFO策略来进行消息发送。每一个Follower服务器在接收到这个事务Proposal之后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写入后反馈给Leader服务器一个Ack响应。当Leader服务器接收到超过半数Follower的Ack响应之后,就会广播一个Commit消息给所有的Follower服务器以通知其进行事务提交,同时Leader自身也会完成对事务的提交,而每一个Follower服务器在接收到Commit消息后,也会完成对事务的提交。
一旦Leader服务器出现崩溃,或者说由于网络原因导致Leader服务器失去了与过半Follower的联系,那么就会进入崩溃恢复模式。此时,ZK集群将会发起一轮Leader选举,选举规则是:优先选事务ID大的,在事务Id相同的情况下,优先选服务器编号大的。
第一轮每台服务器都选择自己,并进行投票广播;同时也接收来自其他服务器的投票信息,并拿接收到的投票中事务ID和服务器编号与自己的投票对比,谁高就选谁,然后再次广播投票。 每次接收到投票,都会计算自己接收到选票是否能判断某台机器已经有超过半数的选票,一旦感知到这种情况,就会更改自己的状态。 这样选举出来的Leader一定拥有集群中最大的事务ID,也就是数据最全的那台机器。
因为是事务Id最大的机器,那个这个新选举出来的leader一定具有所有已提交的提案。更为重要的是,如果让具有最高事务Id的机器来成为Leader,就可以省去Leader服务器检查Proposal的提交和丢失工作这一步操作了。 这里说的提交是指:ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器提交。这里说的丢弃是指:ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务。
在完成Leader选举之后,Leader服务器会检查事务日志中的所有已经提交的Proposal是否都已经被集群中过半的机器提交了,即是否完成数据同步。数据同步过程如下:
Leader服务器会为每一个Follower服务器都准备一个队列,并将那些没有被各个Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每一个Proposal消息后面紧接着再发送一个Commit消息,以表示该事务已经被提交。等到Follower服务器将所有其尚未同步的事务Proposal都从Leader服务器上同步过来并成功应用到本地数据库中后,Leader服务器就会将该Follower服务器加入到真正可用的Follower列表中,并开始之后的其他流程。
原则上尽量避免分布式事务,保证强一致性的成本远比快速发现不一致并修复的成本高。
事务补偿就是指在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须保证事务链中的每一个业务操作都有对应的可逆服务。
比如提现操作,需要经历账户余额扣除,第三方真正打款等多个操作。此时,如果使用补偿操作流程如下:
TCC其实也就是采用的补偿机制,它分为三个阶段:
以会员卡扣款或余额消费或优惠券使用 冻结/占用-使用-核销流程为例:
再以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。
总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。
这类事务机制将分布式事务分成多个本地事务,这里称之为主事务与从事务。首先主事务本地进行提交,然后使用消息通知从事务,从事务从消息中获取事务操作关键信息进行本地操作提交。可以看出这是一个异步事务机制、只能保证最终一致性;但可用性非常高,不会因为故障而发生阻塞。另外,主事务已经先行提交,如果因为从事务无法提交,要回滚主事务还是比较麻烦,所以这种模式只适用于理论上大概率成功的业务情况,即从事务的提交失败可能是由于故障,而不大可能是逻辑错误。
基于异步消息的事务机制主要有两种方式:本地消息表与事务消息。二者的区别在于:怎么保证主事务的提交与消息发送这两个操作的原子性。
本地消息表
基于本地消息表的方案是指将消息写入本地数据库,通过本地事务保证主事务与消息写入的原子性。例如银行转账的例子,伪码如下:
begin transaction:
update User set account = account - 100 where userId = 'A' ;
insert into message(userId, amount, status) values('A', 100, 1) ;
commit transaction
主事务将消息写入本地消息表后,从事务通过pull或者push模式,获取消息并执行。如果是push模式,那么一般使用具有持久化功能的消息队列,从事务订阅消息。如果是pull模式,那么从事务定时去拉取消息,然后执行。
事务消息
所谓的事务消息就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发送消息放在一个分布式事务里,保证要么本地操作成功并且对外发送消息成功,要么二者都失败,开源的RocketMQ就支持这一特性(最新版已经不再支持了),下面我们以RocketMQ来分析其具体原理:
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆分成一个消息事务(A系统的本地操作 + 发消息) + B系统的本地操作,其中B系统的操作由消息驱动,只要事务消息成功,那么A操作一定成功,消息也一定发出来了,这时候B收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。如果更完善的话,考虑B一直重试失败情况,还可以提供一个A操作的回滚机制。整个过程原理如下:
实时对账、准实时对账、T+1的离线对账。对不平,自动冲正,自动轧差。常见于交易结算等财务系统中。