zoukankan      html  css  js  c++  java
  • 分布式事务解决办法

    什么是分布式事务?

    简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上说,分布式事务就是为了保证不同数据库的数据一致性。

    分布式事务产生的原因

    服务化,随着服务化,出现各个微服务,以及这些服务对应的库表,多个库表之间的数据操作 可能需要保证原子性。

    CAP定理

    CAP理论告诉我们,一个分布式系统不可能同时满足 一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partion tolerance)这三个基本需求,最多只能同时满足其中的两项。

    一致性

    在分布式环境下,一致性是指数据在多个副本之间是否能够保持一致的特性。

    可用性

    可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回正确结果。

    分区容错性

    分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

    网络分区是指在分布式系统中,不同的节点分布在不同的子网络(机房或异地网络等)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状况,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干孤立的区域。需要注意的是,组成一个分布式系统的每个节点的加入与退出都可以看作是一个特殊的网络分区。

    image.png-183.1kB

    BASE理论

    BASE理论指的是:

    • Basically Available(基本可用):允许响应时间拉长,允许功能上的损失,允许降级页面(系统繁忙,稍后重试等)
    • Soft state(软状态):是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。
    • Eventually consistent(最终一致性):本质就是需要保证最终数据能够达到一致性,而不需要实时保证系统数据的强一致性。

    两阶段提交

    阶段一 提交事务请求

    1. 事务询问:
      协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
    2. 执行事务:
      各参与者节点执行事务操作,并将Undo和Redo信息记录事务日志中。
    3. 各参与者向协调者反馈事务询问的响应:
      如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。

    上面的过程在形式上近似是协调者组织各参与者对一次事务操作的投票表态过程,因此这个阶段也被称为“投票阶段”,即各参与者投票表明是否要继续执行接下去的事务提交操作。

    阶段二 执行事务提交

    在阶段二中,协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包含以下两种可能:

    执行事务提交:

    假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交。

    1. 发送提交请求:协调者向所有参与者节点发出Commit请求。
    2. 事务提交:参与者接收到Commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
    3. 反馈事务提交结果:参与者在完成事务提交之后,向协调者发送ACK消息。
    4. 完成事务:协调者接收到所有参与者反馈的Ack消息后,完成事务。

    中断事务
    假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。

    1. 发送回滚请求:协调者向所有参与者阶段发出Rollback请求。
    2. 执行事务回滚:参与者接收到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
    3. 反馈事务回滚结果:参与者在完成事务回滚之后,向协调者发送Ack消息。
    4. 完成事务中断:协调者接收到所有参与者反馈的Ack消息后,完成事务中断。

    两阶段提交将一个事务的处理过程分为了投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式。

    优缺点

    两阶段提交协议的优点:原理简单,实现方便。

    两阶段提交协议的缺点:

    1. 同步阻塞:在两阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过程中,将无法进行任何其他操作。
    2. 单点问题:协调者单点问题。
    3. 数据不一致:在两阶段提交协议的阶段二,即执行事务提交的时候,当协调者向所有的参与者发送Commit请求之后。发送了局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部门参与者收到了Commit请求。于是,这部分收到了Commit请求的参与者就会进行事务的提交,而其它没有收到Commit请求的参与者则无法进行事务提交,于是整个分布式系统便出现了数据不一致性的问题。
    4. 太过保守:如果在协调者指示参与者进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,这时协调者只能依靠自身的超时机制来判断是否需要中断事务,这样的策略显得比较保守。换句话说,两阶段提交协议没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。

    三阶段提交

    协议说明

    三阶段提交,将两阶段提交协议的“提交事务内容”过程一分为二,形成了由CanCommit、PreCommit和DoCommit三个阶段组成的事务处理协议。

    阶段一

    1. 事务询问:协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
    2. 等待反馈:各参与者在接收到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺序执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No响应。

    阶段二:PreCommit

    在阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作,正常情况下,包含两种可能。

    执行事务预提交:
    假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交。

    1. 发送事务预提交请求:协调者向所有参与者节点发出PreCommit的请求,并进入Prepared阶段。
    2. 执行事务预提交操作:参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。(执行但不提交)
    3. 反馈事务执行的响应:如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交(commit)或中止(abort)。

    中断事务:
    假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。

    1. 发送中断请求:协调者向所有参与者节点发出abort请求。
    2. 无论是收到来自协调者的abort请求,或者是在等待协调者发送请求过程中出现超时,参与者都会中断事务。

    阶段三:doCommit

    该阶段将进行真正的事务提交,会存在以下两种可能的情况。

    执行提交

    1. 发送提交请求:进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送doCommit请求。
    2. 执行事务操作:参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
    3. 反馈事务提交结果:参与者在完成事务提交之后,向协调者发送Ack消息。
    4. 完成事务:协调者接收到所有参与者反馈的Ack消息之后,完成事务。

    中断事务:
    如果有任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。

    1. 发送中断请求:协调者向所有的参与者节点发送abort请求。
    2. 事务回滚:参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
    3. 反馈事务回滚结果:参与者在完成事务回滚之后,向协调者发送Ack消息。
    4. 中断事务:协调者接收到所有参与者反馈的Ack消息后,中断事务。

    需要注意的是,一旦进入阶段三,可能会存在以下两种故障:

    • 协调者出现问题。
    • 协调者和参与者之间的网络出现故障。

    无论出现那种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort请求,针对于这样的异常情况,参与者都会在等待超时之后,继续进行事务提交。(乐观态度,认为前面已经进行过PreCommit请求了,事务一定能执行成功)。

    优缺点:

    三阶段提交协议的优点:相较于两阶段提交协议,三阶段提交协议最大的优点是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。

    三阶段提交协议的缺点:三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和部分参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。

    ZooKeeper的ZAB协议

    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服务器出现崩溃,或者说由于网络原因导致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列表中,并开始之后的其他流程。

    分布式事务解决方案:

    原则上尽量避免分布式事务,保证强一致性的成本远比快速发现不一致并修复的成本高。

    补偿事务机制,保证最终一致性

    事务补偿就是指在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须保证事务链中的每一个业务操作都有对应的可逆服务。

    比如提现操作,需要经历账户余额扣除,第三方真正打款等多个操作。此时,如果使用补偿操作流程如下:

    1. 用户发起提现,扣除用户账户余额,生成提现流水。
    2. 提交给第三方。
    3. 第三方尝试打款,无论成功与失败都会通知发起方。
    4. 发起方收到通知后,如果发现提现失败,就尝试给用户生成一条提现失败返回流水。将之前扣减的钱返回去。

    TCC(try Confirm Cancel)

    TCC其实也就是采用的补偿机制,它分为三个阶段:

    • Try阶段主要是对业务系统做检测及资源预留。
    • Confirm阶段主要是对业务操作做确认提交,
    • Cancel阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消操作,释放预留资源。

    以会员卡扣款或余额消费或优惠券使用 冻结/占用-使用-核销流程为例:

    • try:检查账户余额是否充足,充足的话就冻结(相当于预留出的资源,也可对多个资源进行冻结)
    • confirm:执行整个分布式事务操作(相当于只利用try预留出的资源)。
    • cancel:恢复try阶段预留的资源。(若在某一个阶段失败,则将try预留的资源恢复try前的状态,理解为把余额给加回去)

    再以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。

    总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。

    基于消息的分布式事务

    这类事务机制将分布式事务分成多个本地事务,这里称之为主事务与从事务。首先主事务本地进行提交,然后使用消息通知从事务,从事务从消息中获取事务操作关键信息进行本地操作提交。可以看出这是一个异步事务机制、只能保证最终一致性;但可用性非常高,不会因为故障而发生阻塞。另外,主事务已经先行提交,如果因为从事务无法提交,要回滚主事务还是比较麻烦,所以这种模式只适用于理论上大概率成功的业务情况,即从事务的提交失败可能是由于故障,而不大可能是逻辑错误。

    基于异步消息的事务机制主要有两种方式:本地消息表与事务消息。二者的区别在于:怎么保证主事务的提交与消息发送这两个操作的原子性。

    1. 本地消息表

      基于本地消息表的方案是指将消息写入本地数据库,通过本地事务保证主事务与消息写入的原子性。例如银行转账的例子,伪码如下:

       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模式,那么从事务定时去拉取消息,然后执行。

    2. 事务消息

      所谓的事务消息就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发送消息放在一个分布式事务里,保证要么本地操作成功并且对外发送消息成功,要么二者都失败,开源的RocketMQ就支持这一特性(最新版已经不再支持了),下面我们以RocketMQ来分析其具体原理:

    image.png-31.5kB

    1. RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址。
    2. 第二阶段执行本地事务。
    3. 第三阶段根据第二阶段执行的结果通过第一阶段拿到的地址去访问消息,并修改消息状态。如果此时确认消息发送失败了,RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,就会向消息发送方确认(确认的方式 是强制在第一阶段注册一个回调接口,此时对该回调接口进行调用,获得执行结果。),以决定事务消息状态。

    基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆分成一个消息事务(A系统的本地操作 + 发消息) + B系统的本地操作,其中B系统的操作由消息驱动,只要事务消息成功,那么A操作一定成功,消息也一定发出来了,这时候B收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。如果更完善的话,考虑B一直重试失败情况,还可以提供一个A操作的回滚机制。整个过程原理如下:

    image.png-40.5kB

    对账(最鲁棒的技术)

    实时对账、准实时对账、T+1的离线对账。对不平,自动冲正,自动轧差。常见于交易结算等财务系统中。

    最大努力送达型

    1. 允许中间状态出现,比如提现处理中,同时记录失败记录表,定时Job重试。
    2. 使用MQ延迟消息重试。
  • 相关阅读:
    cocos2dx3.1从零学习(二)菜单、场景切换、场景传值
    XCode5添加新建类模板(Cocos2dx Template Class for Scene or Layer)
    根据Uri获取图片绝对路径,解决Android4.4以上版本Uri转换
    如何学习 cocos2d-x ?
    Java数据类型中String、Integer、int相互间的转换
    Android各种效果集合
    重新生成IE02
    nvl与 is not null的区别等
    自定义view
    select into from 和 insert into select 的用法和区别(转)
  • 原文地址:https://www.cnblogs.com/boothsun/p/8976409.html
Copyright © 2011-2022 走看看