分布式

一、两个定理

  分布式系统一定是由多个节点组成的系统,一般来说一个节点就是我们的一台计算机;这些节点并不是孤立的,而是互相连通的;这些连通的节点上都部署了我们的组件,并且相互之间的操作会有协同。
  为什么要有分布式系统呢?

  1. 升级单机处理能力的性价比越来越低。
  2. 单机处理能力存在瓶颈。
  3. 出于稳定性和可用性的考虑。

1.1 CAP定理*

  在分布式系统中,一个Web应用最多只能同时支持的两个属性:

  1. 一致性(Consistency,C): 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  2. 可用性(Availability,A): 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
  3. 分区容错性(Partition Tolerance,P): 即使出现单个组件无法可用,操作依然可以完成。
  • 1、一致性
      更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态分布式环境中,一致性是指多个应用之间能否保持一致的特性
      数据一致性分为强一致性、弱一致性、最终一致性。

  如果时刻保证客户端看到的数据都是一致的,那么称之为强一致性
  如果允许存在中间状态,只要求经过一段时间后,数据最终是一致的,则称之为最终一致性
  此外,如果允许存在部分数据不一致,那么就称之为弱一致性

  • 2、可用性
      系统提供的服务必须一直处于可用的状态,对于用户的每个操作请求总是能够在有限的时间内返回结果。
      有限时间内:对于用户的一个操作请求,系统必须能够在指定的时间(响应时间)内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的。
      返回正常结果:要求系统在完成对用户请求的处理后,返回一个正常的响应结果。正常的响应结果通常能够明确地反映出对请求的处理结果,即成功或失败。
  • 3、分区容错性
      分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
      网络分区,是指分布式系统中,不同的节点分布在不同的子网络(机房/异地网络)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状态,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干孤立的区域。
1.1.1 CAP取舍

  CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性(P)是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡。3种取舍策略:

  • 放弃P(不考虑)
      放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。
  • 放弃A
      如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。
      设计成CP的系统其实不少,最典型的就是分布式数据库,如Redis等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。
  • 放弃C
      要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在 A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。

  对于分布式系统来说,P是不能放弃的,因此通常是在可用性和强一致性之间权衡。

  在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性

1.1.2 为什么无法同时保证C和A

  对于分布式系统而言,分区容错性是一个最基本的要求,因此基本上我们在设计分布式系统的时候只能从一致性(C)和可用性(A)之间进行取舍。
  如果保证了一致性(C):对于节点N1和N2,当往N1里写数据时,N2上的操作必须被暂停,只有当N1同步数据到N2时才能对N2进行读写请求,在N2被暂停操作期间客户端提交的请求会收到失败或超时。显然,这与可用性是相悖的。
  如果保证了可用性(A):那就不能暂停N2的读写操作,但同时N1在写数据的话,这就违背了一致性的要求。

  对于多数大型互联网应用的场景,主机众多、部署分散,且现在的集群规模越来越大,所以节点故障、网络故障是常态,且要保证服务可用性达到 N 个9,即保证 P 和 A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。
  对于涉及到钱财这样不能有一丝让步的场景,C 必须保证。网络发生故障宁可停止服务,这是保证 CA,舍弃 P。

1.2 BASE定理*

  CAP是分布式系统设计理论,BASE是CAP理论中AP方案的延伸。对于C,我们采用的方式和策略就是保证最终一致性。
  BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE基于CAP定理演化而来,核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性

  • 1、Basically Available(基本可用)
      基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。两个就是“基本可用”的典型例子:
      1、响应时间上的损失:正常情况下,一个在线搜索引擎需要0.5秒内返回给用户相应的查询结果,但由于出现异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
      2、功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
  • 2、Soft state(软状态)
      指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。
      允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
  • 3、Eventually consistent(最终一致性)
      强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

  在实际工程实践中,最终一致性存在以下五类主要变种:

  • 1、因果一致性
      因果一致性是指,如果进程A在更新完某个数据项后通知了进程B,那么进程B之后对该数据项的访问都应该能够获取到进程A更新后的最新值,并且如果进程B要对该数据项进行更新操作的话,务必基于进程A更新后的最新值,即不能发生丢失更新情况。与此同时,与进程A无因果关系的进程C的数据访问则没有这样的限制。
  • 2、读己之所写
      读己之所写是指,进程A更新一个数据项之后,它自己总是能够访问到更新过的最新值,而不会看到旧值。也就是说,对于单个数据获取者而言,其读取到的数据一定不会比自己上次写入的值旧。因此,读己之所写也可以看作是一种特殊的因果一致性。
  • 3、会话一致性
      会话一致性将对系统数据的访问过程框定在了一个会话当中:系统能保证在同一个有效的会话中实现“读己之所写”的一致性,也就是说,执行更新操作之后,客户端能够在同一个会话中始终读取到该数据项的最新值。
  • 4、单调读一致性
      单调读一致性是指如果一个进程从系统中读取出一个数据项的某个值后,那么系统对于该进程后续的任何数据访问都不应该返回更旧的值。
  • 5、单调写一致性
      单调写一致性是指,一个系统需要能够保证来自同一个进程的写操作被顺序地执行。

  在实际系统实践中,可以将其中的若干个变种互相结合起来,以构建一个具有最终一致性的分布式系统。事实上,可以将其中的若干个变种相互结合起来,以构建一个具有最终一致性特性的分布式系统。
  总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统事务的ACID特性使相反的,它完全不同于ACID的强一致性模型,而是提出通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

二、分布式事务


  当尝试去解决分布式事务问题时,一般会谈到两种解决方向:即刚性事务和柔性事务。

2.1 刚性事务

  刚性事务指的是,要使分布式事务,达到像本地式事务一样,具备数据强一致性。从CAP来看,就是说,要达到CP状态。
  对于刚性事务来说,一个成熟的理论模型就是基于XA的2PC。

  在X/Open DTP(Distributed Transaction Process)模型里,有三个角色:
  AP:Application,应用程序。也就是业务层。哪些操作属于一个事务,就是AP定义的。AP,即应用,也就是我们的业务服务。
  TM:Transaction Manager,事务管理器。接收AP的事务请求,对全局事务进行管理,管理事务分支状态,协调RM的处理,通知RM哪些操作属于哪些全局事务以及事务分支等等。这个也是整个事务调度模型的核心部分。
  RM:Resource Manager,资源管理器。一般是数据库,也可以是其他的资源管理器,如消息队列(如JMS数据源),文件系统等。

  AP自己操作TM,当需要事务时,AP向TM请求发起事务,TM负责整个事务的提交,回滚等。
  XA规范是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。

  XA 规范描述了全局的事务管理器与局部的资源管理器之间的接口。XA规范的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使ACID属性跨越应用程序而保持有效。
  XA规范使用两阶段提交(2PC,Two-Phase Commit)协议来保证所有资源同时提交或回滚任何特定的事务。

  XA规范(XA Specification) 是X/OPEN 提出的分布式事务处理规范。XA规范了TM与RM之间的通信接口,在TM与多个RM之间形成一个双向通信桥梁,从而在多个数据库资源下保证ACID四个特性。目前知名的数据库,如Oracle、DB2、mysql等,都是实现了XA接口的,都可以作为RM。
  XA是数据库的分布式事务,强一致性,在整个过程中,数据一张锁住状态,即从prepare到commit、rollback的整个过程中,TM一直把持着数据库的锁,如果有其他要修改数据库的该条数据,就必须等待锁的释放,存在长事务风险。

  • XA协议的实现
      1、两阶段提交(2PC)
      2、三阶段提交(3PC):对 2PC协议的一种扩展。
      3、Seata。Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata为用户提供了AT、TCC、SAGA 和XA事务模式。
      4、作为Java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持。实际上,JTA是基于XA架构上建模的,在JTA中,事务管理器抽象为TransactionManager接口,并通过底层事务服务(即JTS)实现。像很多其他的Java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下2种:
  1. J2EE容器所提供的JTA实现(JBoss)。
  2. 独立的JTA实现:如JOTM、Atomikos。

  这些实现可以应用在那些不使用J2EE应用服务器的环境里用以提供分布事
事务保证。如Tomcat、Jetty以及普通的java应用。
  5、JTS规范。为了规范事务开发,Java增加了关于事务的规范,即JTA和JTS。
  JTA定义了一套接口,其中约定了几种主要的角色:TransactionManager、UserTransaction、Transaction、XAResource,并定义了这些角色之间需要遵守的规范,如Transaction的委托给TransactionManager等。
  TransactionManager:事务管理器,可以创建新的事务,并设置事务的相关属性,还允许应用获取创建后的事务,并且将事务与线程绑定。具体有以下方法:

  begin:创建新的事务,并且将事务与线程关联,如果不支持嵌套事务并且事务已存在将抛出 NotSupportedException 异常。
  commit:提交与线程关联的事务,并断开线程与事务的关联。如果没有事务与线程关联将抛出异常。实现将触发 Transaction.commit、XAResource.prepare、XAResource.commit 方法调用。
  rollback:回滚线程关联的事务,并断开线程与事务的关联。如果没有事务与线程关联也将抛出异常。实现将触发 Transaction.rollback、XAResource.rollback 方法的调用。
  getTransaction:获取线程关联的事务 Transaction 对象,如果没有事务与当前线程关联则返回 null。
  setTansactionalTimeout:设置线程关联事务的超时秒数。
  getStatus:获取线程关联事务的状态。
  setRollbackOnly:设置线程关联事务的唯一结果是回滚。
  suspend:暂时挂起与线程关联的事务,将断开线程与事务的关联,方法返回的 Transaction 对象可作为 resume 方法参数恢复线程与事务的关系。实现将触发 Transaction.delistResource 与 XResource.end 方法的调用。
  resume:恢复指定事务与线程之间的关联。实现将触发 Transaction.enlistResource、XAResource.start 方法的调用。

  Transaction:事务接口,用于对活动的事务进行操作,这里活动的事务是指未提交的事务,对其他事务不可见。

  enlistResource:将资源添加到事务,以便执行两阶段提交,允许添加多个资源,资源的最终的操作结果将与事务保持一致,即要么提交、要么回滚。实现将触发 XAResource.start 方开启事务分支。
  delistResource:解除资源与事务的关联。实现将触发 XAResource.end 方法调用结束事务分支。
  registerSynchronization:注册 Synchronization,接口方法将在事务完成前后被回调。
  commit:与 TransactionManager.commit 含义相同。
  getStatus:与 TransactionManager.getStatus 含义相同。
  rollback:与 TransactionManager.rollback 含义相同。
  setRollbackOnly:与 TransactionManager.setRollbackOnly 含义相同。

  XAResource:用来表示分布式事务角色中的资源管理器,资源适配器将实现XAResource接口以支持事务与资源的关联,允许不同的线程同时对XAResource进行操作,但一个XAResource同时只能关联一个事务。事务提交时,触发资源管理器准备与提交。具体方法:

  start:开启事务分支,全局事务通过该方法与事务资源关联,由资源适配器获取资源(连接)时隐式触发。一但调用该方法资源将一直保持开启状态,直到释放(close)资源。
  end:结束事务分支,解除资源与事务的关联,调用该方法后可再调用 start 方法与其他事务建立关联。
  prepare:为 xid 指定的事务提交做准备。
  commit:提交 xid 指定的事务分支。
  rollback:回滚 xid 指定的事务分支。
  isSameRM:判断当前资源管理器是否与给定的资源管理器相同,用于 Transaction.enlistResource 添加资源时判断资源是否已添加。
  recover:用于意外导致事务管理器与资源管理器断开通信后,事务管理器恢复时查询准备好的事务。XAResource 在故障发生时未持久化,事务管理器需要有某种方法查找 XAResource,如通过 JNDI 查找机制,事务管理器可以忽略不属于它的事务。
  forget:用于忘记准备好的事务分支。
  getTransactionTimeout:获取事务超时秒数。
  setTransactionTimeout:设置事务超时秒数。

  JTS也是一组规范,上面提到JTA中需要角色之间的交互,那应该如何交互?JTS就是约定了交互细节的规范。
  总体上来说JTA更多的是从框架的⻆度来约定程序角色的接口,而JTS则是从具体实现的⻆度来约定程序角色之间的接口,两者各司其职。
  6、Seata AT模式。Seata AT模式是增强型2pc模式。AT 模式: 两阶段提交协议的演变,没有一直锁表:

一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:提交异步化,非常快速地完成。或回滚通过一阶段的回滚日志进行反向补偿。

  • XA的主要限制
      1、必须要拿到所有数据源,且数据源还要支持XA协议。目前MySQL中只有InnoDB存储引擎支持XA协议。
      2、性能比较差,要把所有涉及到的数据都要锁定,是强一致性的,会产生长事务。
2.1.1 两阶段提交协议*

  2PC分为两个阶段处理,阶段一(提交事务请求);阶段二(执行事务提交)。如果阶段一超时或者出现异常,2PC的阶段二就会中断事务。

  • 阶段一:提交事务请求
      1、事务询问。协调者向所有参与者发送事务内容,询问是否可以执行提交操作,并开始等待各参与者进行响应;
      2、执行事务。各参与者节点,执行事务操作,并将Undo和Redo操作计入本机事务日志;
      3、各参与者向协调者反馈事务问询的响应。成功执行返回Yes,否则返回No。
  • 阶段二:执行事务提交
      协调者在阶段二决定是否最终执行事务提交操作。这一阶段包含两种情形。其中一种情形为:所有参与者reply Yes,那么执行事务提交。
  1. 发送提交请求。协调者向所有参与者发送Commit请求;
  2. 事务提交。参与者收到Commit请求后,会正式执行事务提交操作,并在完成提交操作之后,释放在整个事务执行期间占用的资源;
  3. 反馈事务提交结果。参与者在完成事务提交后,写协调者发送Ack消息确认;
  4. 完成事务。协调者在收到所有参与者的Ack后,完成事务。

  • 阶段二:中断事务
      事情总会出现意外,当存在某一参与者向协调者发送No响应,或者等待超时。协调者只要无法收到所有参与者的Yes响应,就会中断事务。
  1. 发送回滚请求。协调者向所有参与者发送Rollback请求;
  2. 回滚。参与者收到请求后,利用本机Undo信息,执行Rollback操作。并在回滚结束后释放该事务所占用的系统资源;
  3. 反馈回滚结果。参与者在完成回滚操作后,向协调者发送Ack消息;
  4. 中断事务。协调者收到所有参与者的回滚Ack消息后,完成事务中断。

  2PC方案实际很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。 现在微服务,一个大的系统分成几十甚至上百个服务。一般来说,我们的规定和规范,是要求每个服务只能操作自己对应的一个数据库。

  • 二阶段事务的缺点
      1)同步阻塞问题/性能问题。执行过程中,所有参与节点都是事务阻塞型的。XA协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。
      2)单点故障。事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。
      3)数据不一致/脑裂问题。在二阶段提交的阶段二中,当协调者向参与者发送 commit 请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,导致只有一部分参与者接受到了commit请求。于是整个分布式系统便出现了数据不一致性的现象(脑裂现象)。
      4)数据状态不确定(二阶段无法解决的问题 )。 协调者再在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
2.1.2 三阶段提交协议

  三阶段提交协议,是二阶段提交(2PC)的改进版本。
  与两阶段提交不同的是,三阶段提交有两个改动点:

  1、引入超时机制。同时在协调者和参与者中都引入超时机制。
  2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

  • 阶段1、CanCommit
      1、事务询问。协调者向所有参与者发送包含事务内容的canCommit的请求,询问是否可以执行事务提交,并等待应答;
      2、各参与者反馈事务询问。正常情况下,如果参与者认为可以顺利执行事务,则返回Yes,否则返回No。

  • 阶段2、PreCommit
      在本阶段,协调者会根据上一阶段的反馈情况来决定是否可以执行事务的PreCommit操作。有以下两种可能:
    【执行事务预提交】
      1、发送预提交请求。协调者向所有节点发出PreCommit请求,并进入prepared阶段;
      2、事务预提交。参与者收到PreCommit请求后,会执行事务操作,并将Undo和Redo日志写入本机事务日志;
      3、各参与者成功执行事务操作,同时将反馈以Ack响应形式发送给协调者,同事等待最终的Commit或Abort指令。
    【中断事务】
      假如任意一个参与者向协调者发送No响应,或者等待超时,协调者在没有得到所有参与者响应时,即可以中断事务。
      1、发送中断请求。 协调者向所有参与者发送Abort请求;
      2、中断事务。无论是收到协调者的Abort请求,还是等待协调者请求过程中出现超时,参与者都会中断事务。

  • 阶段3、doCommit
      在这个阶段,会真正的进行事务提交,同样存在两种可能。
    【执行提交】
      1、发送提交请求。假如协调者收到了所有参与者的Ack响应,那么将从预提交转换到提交状态,并向所有参与者,发送doCommit请求;
      2、事务提交。参与者收到doCommit请求后,会正式执行事务提交操作,并在完成提交操作后释放占用资源;
      3、反馈事务提交结果。参与者将在完成事务提交后,向协调者发送Ack消
    息;
      4、完成事务。协调者接收到所有参与者的Ack消息后,完成事务。
    【中断事务】
      在该阶段,假设正常状态的协调者接收到任一个参与者发送的No响应,或在超时时间内,仍旧没收到反馈消息,就会中断事务。
      1、发送中断请求。协调者向所有的参与者发送abort请求;
      2、事务回滚。参与者收到abort请求后,会利用阶段二中的Undo消息执行事务回滚,并在完成回滚后释放占用资源;
      3、反馈事务回滚结果。参与者在完成回滚后向协调者发送Ack消息;
      4、中端事务。协调者接收到所有参与者反馈的Ack消息后,完成事务中断。

2.1.3 2PC和3PC的区别

  3PC有效降低了2PC带来的参与者阻塞范围,并且能够在出现单点故障后继续达成一致;
  但3PC带来了新的问题,在参与者收到preCommit消息后,如果网络出现分区,协调者和参与者无法进行后续的通信,这种情况下,参与者在等待超时后,依旧会执行事务提交,这样会导致数据的不一致。

  • 2PC和3PC的区别
      三阶段提交协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交。三阶段提交的三个阶段分别为:can_commit,pre_commit,do_commit。

      在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者abort请求时,会在等待超时之后,继续进行事务的提交。
  • 3PC主要解决的单点故障问题
      相对于2PC,3PC主要解决的单点故障问题,并减少阻塞, 因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit,而不会一直持有事务资源并处于阻塞状态。
      但是这种机制也会导致数据一致性问题,因为由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
  • 3PC相对于2PC阶段到底优化了什么地方
      相比较2PC阶段,3PC对于协调者和参与者都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了什么问题呢?
      这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
2.1.4 XA规范的问题

  XA算分布式事务处理的规范,但在互联网中很少使用,究其原因有以下几个:

  • 性能(阻塞性协议,增加响应时间、锁时间、死锁);
  • 数据库支持完善度(MySQL 5.7之前都有缺陷);
    *协调者依赖独立的J2EE中间件(早期重量级Weblogic、Jboss、后期轻量级Atomikos、Narayana和Bitronix);
  • 运维复杂,DBA缺少这方面经验;
  • 并不是所有资源都支持XA协议。

2.2 柔性事务

  在电商领域等互联网场景下,传统的事务在数据库性能和处理能力上都暴露出了瓶颈。柔性事务有两个特性:基本可用和柔性状态。

  基本可用:指分布式系统出现故障的时候允许损失一部分的可用性。
  柔性状态:指允许系统存在中间状态,这个中间状态不会影响系统整体的可用性,如数据库读写分离的主从同步延迟等。柔性事务的一致性指的是最终一致性。

  柔性事务主要分为补偿型和通知型,补偿型事务又分:TCC、Saga;通知型事务分:MQ事务消息、最大努力通知型。

  补偿型事务都是同步的,通知型事务都是异步的

2.2.1通知型事务*

  通知型事务的主流实现是通过MQ(消息队列)来通知其他事务参与者自己事务的执行状态,引入MQ组件,有效的将事务参与者进行解耦,各参与者都可以异步执行,所以通知型事务又被称为异步事务。
  通知型事务主要适用于那些需要异步更新数据,并且对数据的实时性要求较低的场景,主要包含:异步确保型事务和最大努力通知事务。
  异步确保型事务:主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,如订单和购物车、收货与清算、支付与结算等等场景;
  最大努力通知:主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,如充值平台与运营商、支付对接等等跨网络系统级别对接。

2.2.2 异步确保型

  指将一系列同步的事务操作修改为基于消息队列异步执行的操作,来避免分布式事务中同步阻塞带来的数据操作性能的下降。
  基于MQ的事务消息方案主要依靠MQ的半消息机制来实现投递消息和参与者自身本地事务的一致性保障。半消息机制实现原理其实借鉴的2PC的思路,是二阶段提交的广义拓展。
  半消息:在原有队列消息执行后的逻辑,如果后面的本地逻辑出错,则不发送该消息,如果通过则告知MQ发送。
  流程:

  1. 事务发起首先先发送半消息到MQ;
  2. MQ通知发送方消息发送成功;
  3. 在发送半消息成功后执行本地事务;
  4. 根据本地事务执行结果返回commit或者是rollback;
  5. 如果消息是rollback, MQ将丢弃该消息不投递;如果是commit,MQ将会消息发送给消息订阅方;
  6. 订阅方根据消息执行本地事务;
  7. 订阅方执行本地事务成功后再从MQ中将该消息标记为已消费;
  8. 如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事务状态;
  9. Consumer端的消费成功机制由MQ保证。

  举个例自子,假设存在业务规则:某笔订单成功后,为用户加一定的积分。在这条规则里,管理订单数据源的服务为事务发起方,管理积分数据源的服务为事务跟随者。基于消息队列实现的事务存在以下操作:

订单服务创建订单,提交本地事务;
订单服务发布一条消息;
积分服务收到消息后加积分。


  它的整体流程是比较简单的,同时业务开发工作量也不大:编写订单服务里订单创建的逻辑、编写积分服务里增加积分的逻辑。该事务形态过程简单,性能消耗小,发起方与跟随方之间的流量峰谷可以使用队列填平,同时业务开发工作量也基本与单机事务没有差别,都不需要编写反向的业务逻辑过程。因此基于消息队列实现的事务是我们除了单机事务外最优先考虑使用的形态。
  有一些第三方的MQ是支持事务消息的,这些消息队列,支持半消息机制,如RocketMQ、ActiveMQ。但是有一些常用的MQ也不支持事务消息,如RabbitMQ、Kafka。
  以阿里的 RocketMQ 中间件为例,其思路大致为:

  1. producer(用A系统表示)发送半消息到broker,这个半消息包含完整的消息内容, 在producer端和普通消息的发送逻辑一致;
  2. broker存储半消息,半消息存储逻辑与普通消息一致,只是属性有所不同,topic是固定的RMQ_SYS_TRANS_HALF_TOPIC,queueId也是固定为0,这个tiopic中的消息对消费者是不可见的,所以里面的消息永远不会被消费。这就保证了在半消息提交成功之前,消费者是消费不到这个半消息的;
  3. broker端半消息存储成功并返回后,A系统执行本地事务,并根据本地事务的执行结果来决定半消息的提交状态为提交或者回滚;
  4. A系统发送结束半消息的请求,并带上提交状态(提交 or 回滚);
  5. broker端收到请求后,首先从RMQ_SYS_TRANS_HALF_TOPIC的queue中查出该消息,设置为完成状态。如果消息状态为提交,则把半消息从RMQ_SYS_TRANS_HALF_TOPIC队列中复制到这个消息原始topic的queue中去(之后这条消息就能被正常消费了);如果消息状态为回滚,则什么也不做;
  6. producer发送的半消息结束请求是 oneway 的,也就是发送后就不管了,只靠这个是⽆法保证半消息一定被提交的,rocketMq提供了个兜底方案,这个方案叫消息反查机制,Broker启动时,会启动一个TransactionalMessageCheckService 任务,该任务会定时从半消息队列中读出所有超时未完成的半消息,针对每条未完成的消息,Broker会给对应的Producer发送一个消息反查请求,根据反查结果来决定这个半消息是需要提交还是回滚,或者后面继续来反查;
  7. consumer(用B系统表示)消费消息,执行本地数据变更(至于B是否能消费成功,消费失败是否重试,这属于正常消息消费需要考虑的问题);


  在rocketMq中,不论是producer收到broker存储半消息成功返回后执行本地事务,还是broker向producer反查消息状态,都是通过回调机制完成。
  producer端的代码示例:

  半消息发送时,会传入一个回调类TransactionListener,使用时必须实现其中的两个方法,executeLocalTransaction方法会在broker返回半消息存储成功后执行,我们会在其中执行本地事务;checkLocalTransaction方法会在broker向producer发起反查时执行,我们会在其中查询库表状态。两个方法的返回值都是消息状态,就是告诉broker应该提交或者回滚半消息。
  有时候我们目前的MQ组件并不支持事务消息,或者我们想尽量少的侵入业务方。这时我们需要另外一种方案“基于DB本地消息表”。
  本地消息表是目前业界使用的较多的方案之一,它的核心思想就是将分布式事务拆分成本地事务进行处理。
  本地消息表流程:

  发送消息方:

  1、需要有一个消息表,记录着消息状态相关信息。
  2、业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
  3、在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录。
  4、消息会发到消息消费方,如果发送失败,即进行重试。

  消息消费方:

  1、处理消息队列中的消息,完成自己的业务逻辑。
  2、如果本地事务处理成功,则表明已经处理成功了。
  3、如果本地事务处理失败,那么就会重试执行。
  4、如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。

  生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
  本地消息表的优点:

  本地消息表建设成本较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
  无需提供回查方法,进一步减少的业务的侵入。
  在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。

  本地消息表的缺点:

  本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
  本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的。

  MQ事务消息和本地消息表的共同点:

  1、 事务消息都依赖MQ进行事务通知,所以都是异步的。
  2、 事务消息在投递方都是存在重复投递的可能,需要有配套的机制去降低重复投递率,实现更友好的消息投递去重。
  3、 事务消息的消费方,因为投递重复的无法避免,因此需要进行消费去重设计或者服务幂等设计。

  MQ事务消息和本地消息表的不同点:
  MQ事务消息:

  需要MQ支持半消息机制或者类似特性,在重复投递上具有较好的去重处理;
  具有较大的业务侵⼊性,需要业务方进行改造,提供对应的本地操作成功的回查功能;

  DB本地消息表:

  使用了数据库来存储事务消息,降低了对MQ的要求,但是增加了存储成本;
  事务消息使用了异步投递,增加了消息重复投递的可能性。

2.2.3 最大努力通知型

  最大努力通知方案的目标,就

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值