1 分布式事务理论基础参考

较好参考
分布式事务概述
分布式事务参考文件
分布式事务的概念及实现方案
分布式事务有这一篇就够了!
分布式事务七种解决方案,最后一种经典了!
用Java轻松完成一个分布式事务TCC,自动处理空补偿、悬挂、幂等
面试必问:分布式事务六种解决方案

次之参考
漫画:什么是分布式事务? 什么是Oracle Tuxedo
分布式事务?No, 最终一致性
分布式事务与Seate框架(1)——分布式事务理论
干货|深入理解分布式事务,这一篇就够了!

次之参考
常用的分布式事务解决方案介绍有多少种?
6 张图带你彻底搞懂分布式事务 XA 模式
springcloud+eureka整合阿里seata-saga模式
分布式事务的四种解决方案
分布式事务 CAP 理解论证 解决方案

seata介绍参考
Seata入门系列(9)-事务分组原理及应用案例详解

2 什么是事务?

事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。

2.1 本地事务

在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。

数据库事务的四大特性:ACID
A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失败的情况。
C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转 100 元,转账前和转账后的数据是正确状态这叫一致性,如果出现张三转出 100 元,李四账户没有增加 100 元这就出现了数 据错误,就没有达到一致性。
I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务的运行过程的中间状态。通过配置事务隔离级别可以比避免脏读、重复读问题。
D(Durability):持久性,事务完成之后,该事务对数据的更改会持久到数据库,且不会被回滚。就算数据库宕机重启之后数据也不会丢失。

参考:
分布式事务有这一篇就够了!

3 什么是分布式事务?

分布式事务也是一种事务,也最终会满足ACID。

  • A:最终都会进行全局提交或全局回滚。
  • C:最终都会进行全局提交或全局回滚。
  • I:分布式事务的全局事务之间也会有隔离性,主要看分布式事务实现方案如何实现,比如Seata AT模式默认全局事务隔离级别为读未提交。
  • D:分布式事务提交之后数据都会进行持久化。

只不过C(consistency)这个特性可以分成2种实现:

  1. 强一致性
  2. 最终一致性(弱一致性的特殊情况)

分布式系统一致性问题:
如何在分布式事务解决方案中来实现一致性C(Consistency),这可能就要用到Paxos算法或者简单一点的Raft算法来实现一致性C(Consistency)。
但是实际的分布式事务解决方案和方案实现(比如Seata)中没有使用这2种算法,但是我们要需要了解到这2种算法可以实现分布式系统中的一致性C(Consistency)。

4 分布式系统的理论基础

4.1 CAP理论

CAP理论是经典的分布式系统理论。
2000年7月,加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。Brewer认为在设计一个大规模的分布式系统时会遇到三个特性:一致性(consistency)、可用性(Availability)、分区容错(partition-tolerance),而一个分布式系统最多只能满足其中的2项。2年后,麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。
分布式事务 - 图1
对于CAP的理解:
1. C 一致性
一致性指“all nodes see the same data at the same time”,指的是强一致性,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态。例如对于电商系统用户下单操作,库存减少、用户资金账户扣减、积分增加等操作必须在用户下单操作完成后必须是一致的。不能出现类似于库存已经减少,而用户资金账户尚未扣减,积分也未增加的情况。如果出现了这种情况,那么就认为是不一致的。
关于一致性,如果的确能像上面描述的那样时刻保证客户端看到的数据都是一致的,那么称之为强一致性。如果允许存在中间状态,只要求经过一段时间后,数据最终是一致的,则称之为最终一致性。(可以把最终一致性看成弱一致性的特殊情况)
2. A 可用性
可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。“有限的时间内”是指,对于用户的一个操作请求,系统必须能够在指定的时间内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的。试想,如果一个下单操作,为了保证分布式事务的一致性,需要10分钟才能处理完,那么用户显然是无法忍受的。“返回结果”是可用性的另一个非常重要的指标,它要求系统在完成对用户请求的处理后,返回一个正常的响应结果,不论这个结果是成功还是失败。
3. P 分区容错性
分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

小结:一个分布式系统无法同时满足CAP三个特点,P是一个最基本的要求。因为既然是一个分布式系统,那么分布式系统中的组件必然需要被部署到不同的节点,否则也就无所谓分布式系统了。而对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。因此系统架构师往往需要把精力花在如何根据业务特点在C和A之间寻求平衡。
下面我们要提到的X/Open XA 两阶段提交协议的分布式事务方案,强调的就是强一致性;由于可用性较低,实际应用的并不多。而基于BASE理论的柔性事务,强调的是可用性和最终一致性,目前大行其道,大部分互联网公司采可能会优先采用这种方案。

参考:分布式事务概述

CAP的组合

在生产中对分布式事务处理时要根据需求来确定满足 CAP 的哪两个方面。
AP:放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。
例如:商品库存管理,可以实现 AP,前提是只要用户可以接受所查询到的数据在一定时间内不是最新的即可。
通常实现 AP 都会保证最终一致性,后面将的 BASE 理论就是根据 AP 来扩展的,一些业务场景比如:订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定的时间内到账即可。
CP:放弃可用性,追求一致性和分区容错性,zookeeper 其实就是追求的强一致,又比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。
CA:放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。那么系统将不是一个分布式系统,最常用的关系型数据的本地事务就满足了 CA。

小结:CAP是一个已经被证实的理论,一个分布式系统最多只能同时满足:CAP中的两项。它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,结点众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 N 个 9(99.99..%),并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证AP,舍弃 C强一致性,保证最终一致性。
其中AP组合在实际应用中较多,AP即舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如主数据库向从数据库同步数据的场景,即使不要强一致性,但是最终也要将数据同步成功来保证数据一致,这种是最终一致性和 CAP 中的强一致性不同,CAP中的一致性要求在任何时间查询每个结点数据都必须一致,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。

参考:分布式事务有这一篇就够了!

4.2 BASE理论

eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论。文章链接:https://queue.acm.org/detail.cfm?id=1394128
BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。
1. 基本可用(Basically Available)
指分布式系统在出现不可预知故障的时候,允许损失部分可用性。
2. 软状态( Soft State)
指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。
3. 最终一致( Eventual Consistency)
强调的是所有的数据更新操作,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事务ACID特性是不一致的。它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性(强一致性)和BASE理论(最终一致性)往往又会结合在一起。

参考:分布式事务概述

4.3 一致性模型

  1. 强一致性
  2. 弱一致性
  3. 最终一致性(弱一致性特殊情况)

    强一致性事务也可称之为刚性事务,最终一致性事务也可成为柔性事务。

4.4 CAP理论、BASE理论和一致性模型的关系

CAP理论和BASE理论都包含一致性的问题。

  • CAP中CP组合要求的是实现强一致性;
  • CAP中AP组合要求最终都会实现一致性,可以认为实现的是最终一致性;
  • BASE理论要求的是实现最终一致性;

从上述关系可以看出CAP的AP组合和BASE理论之间是比较接近的。也可以看成BASE理论是AP组合的一种扩展延伸。

4.5 分布式应用选择分布式事务解决方案

我们的应用系统从单体架构升级为微服务架构之后,就变成了一个分布式系统应用,那原来依赖的本地事务就需要升级成分布式事务。
根据应用的一致性需求选择分布式事务解决方案。选择强一致性的的分布式事务解决方案还是选择最终一致性的分布式事务解决方案还是需要依赖于本身的分布式应用。那么该如何选择分布式事务解决方案:

  1. 分布式应用如果对一致性要求十分严格的(此时我们应用满足就是CAP理论中的CP组合,即未达成C,应用就不可A-Available) ,比如金融转账系统,就建议选择强一致性的分布式事务解决方案。
  2. 分布式应用如果对一致性要求不严格(此时我们应用满足就是CAP理论中的AP组合,即应用一定A-Available,但最终都会保证C-Consistency,或者也可以认为满足的是BASE理论),也就是允许分布式应用中一段时间内出现数据不一致的情况,这个情况出现也基本不会影响用户的使用。比如一个分布式应用中分别有一个点赞服务、一个文章服务,用户在应用中对一篇文章进行了点赞,这个时候应用的处理逻辑是点赞服务中新增一条点赞数据、文章服务中更新这篇文章的点赞总数。上述业务逻辑对一致性要求不严格,点赞服务和文章服务数据不一致也不会影响到业务逻辑,只要最终(可能是10分钟内)能到达数据一致性就可以。这种就建议选择最终一致性的分布式事务解决方案。
  3. 分布式应用如果业务比较复杂,一部分业务对一致性要求十分严格,一部分业务对一致性要求不严格(此时我们应用满足就是CAP理论中的CP组合 && (CAP理论中的AP组合 || BASE理论))。这种情况的话我们可以选择强一致性的分布式事务解决方案和最终性一致性的分布式事务解决方案,但是这种组合有点复杂了,实际开发过程中肯定是需要经过充分考虑和计划的。

    5 分布式事务解决方案及方案实现

    前面学习了分布式事务的基础理论,以理论为基础,针对不同的分布式场景业界常见的解决方案有 2PC、3PC、DTP模型与XA规范、TCC、Saga、本地消息表、可靠消息最终一致性、事务消息、最大努力通知这几种。
    给出从所属的一致性模型和所依赖的分布式系统原理。

    X/Open DTP模型与XA规范

    X/Open,即现在的open group,是一个独立的组织,主要负责制定各种行业技术标准。官网地址:http://www.opengroup.org/。X/Open组织主要由各大知名公司或者厂商进行支持,这些组织不光遵循X/Open组织定义的行业技术标准,也参与到标准的制定。下图展示了open group目前主要成员(官网截图):
    分布式事务 - 图2就分布式事务处理(Distributed Transaction Processing,简称DTP)而言,X/Open主要提供了以下参考文档:
    DTP 参考模型:<>
    DTP XA规范: << Distributed Transaction Processing: The XA Specification>>

    DTP模型

    一致性模型:强一致性

    模型元素

    在<>第3版中,规定了构成DTP模型的5个基本元素:
    应用程序(Application Program ,简称AP):用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作。
    资源管理器(Resource Manager,简称RM):如数据库、文件系统等,并提供访问资源的方式。
    事务管理器(Transaction Manager ,简称TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。
    通信资源管理器(Communication Resource Manager,简称CRM):控制一个TM域(TM domain)内或者跨TM域的分布式应用之间的通信。
    通信协议(Communication Protocol,简称CP):提供CRM提供的分布式应用节点之间的底层通信服务。
    其中由于通信资源管理器(Communication Resource Manager)和通信协议(Communication Protocol)是一对好基友,从Communication Protocol的简称CP上就可以看出来,两个元素的关系不一般,因此有的文章在介绍DTP模型元素时,只提到了通信资源管理器….

    模型实例(Instance of the Model)

    一个DTP模型实例,至少有3个组成部分:AP、RMs、TM。如下所示:
    分布式事务 - 图3
    这张图类似于我们之前提到的跨库事务的概念,即单个应用需要操作多个库。在这里就是一个AP需要操作多个RM上的资源。AP通过TM来声明一个全局事务,然后操作不同的RM上的资源,最后通知TM来提交或者回滚全局事务。
    特别的,如果分布式事务需要跨多个应用,类似于我们前面的提到的分布式事务场景中的服务化,那么每个模型实例中,还需要额外的加入一个通信资源管理器CRM。
    下图中演示了2个模型实例,如何通过CRM来进行通信:
    分布式事务 - 图4
    CRM作为多个模型实例之间通信的桥梁,主要作用如下:
    基本的通信能力:从这个角度,可以将CRM类比为RPC框架,模型实例之间通过RPC调用实现彼此的通信。这一点体现在AP、CRM之间的连线。
    事务传播能力:与传统RPC框架不同的是,CRM底层采用OSI TP(Open Systems Interconnection — Distributed Transaction Processing)通信服务,因此CRM具备事务传播能力。这一点体现TM、CRM之间的连线。

    事务管理器作用域 (TM domain)

    1. 一个TM domain中由一个或者多个模型实例组成,这些模型实例使用的都是同一个TM,但是操作的RMs各不相同,由TM来统一协调这些模型实例共同参与形成的全局事务(global transaction)。<br />下图展示了一个由四个模型实例组成的TM Domain,这四个模型实例使用的都是同一个事务管理器TM1 <br />![](https://cdn.nlark.com/yuque/0/2022/png/252847/1649830420890-7f505d1c-b692-4776-b66a-e7711216a593.png#clientId=u182b57d9-f0bd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=280&id=u778a66d6&margin=%5Bobject%20Object%5D&originHeight=572&originWidth=816&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ue513bfdb-2d87-4548-bfb7-3c8c5be75cc&title=&width=400)<br /> TM domain只是列出了最终参与到一个全局事务中,有哪些模型实例,并不关心这些模型实例之间的关系。这就好比,有一个班级,我们只是想知道这个班级中每位同学的名字,但是并不是关心谁是班长、谁是学习委员等。<br /> 不过显然的,当一个TM domain中存在多个模型实例时,模型实例彼此之间存在一定的层级调用关系。这就是全局事务的树形结构。

    全局事务树形结构(Global Transaction Tree Structure)

    当一个TM domain中,存在多个模型实例时,会形成一种树形条用关系,如下图所示:
    分布式事务 - 图5
    其中:
    发起分布式事务的模型实例称之为root节点,或者称之为事务的发起者,其他的模型实例可以统称为事务的参与者。事务发起者负责开启整个全局事务,事务参与者各自负责执行自己的事务分支。
    而从模型实例之间的相互调用关系来说,调用方称之为上游节点(Superior Node),被调用方称之为下游节点(Subordinate Node)。
    小结:通过对DTP模型的介绍,我们可以看出来,之前提到的分布式事务的几种典型场景实际上在DTP模型中都包含了,甚至比我们考虑的还复杂。DTP模型从最早提出到现在已经有接近30年,到如今依然适用,不得不佩服模型的设计者是很有远见的。

    XA规范

    讲XA规范前,我们了解到XA规范对2PC进行了优化。

在DTP本地模型实例中,由AP、RMs和TM组成,不需要其他元素。AP、RM和TM之间,彼此都需要进行交互,如下图所示:
分布式事务 - 图6
这张图中(1)表示AP-RM的交互接口,(2)表示AP-TM的交互接口,(3)表示RM-TM的交互接口。关于这张图,XA规范有以下描述:

The subject of this X/Open specification is interface (3) in the diagram above, the XA interface by which TMs and RMs interact. For more details on this model and diagram, including detailed definitions of each component, see the referenced DTP guide.

也就是说XA规范的最主要的作用是,就是定义了RM-TM的交互接口,下图更加清晰了演示了XA规范在DTP模型中发挥作用的位置,从下图中可以看出来,XA仅仅出现在RM和TM的连线上。<br />![](https://cdn.nlark.com/yuque/0/2022/png/252847/1649830568495-9ccc5016-8753-4c67-a666-a9e20661211a.png#clientId=u182b57d9-f0bd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=200&id=ubadb6b38&margin=%5Bobject%20Object%5D&originHeight=304&originWidth=456&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=uea6f055e-1b9c-4ccc-99c2-4eee6a561ff&title=&width=300)<br />XA规范除了定义的RM-TM交互的接口(XA Interface)之外,还对两阶段提交协议进行了优化。 一些读者可能会误认为两阶段提交协议是在XA规范中提出来的。事实上: 两阶段协议(two-phase commit)是在OSI TP标准中提出的;在DTP参考模型(<<Distributed Transaction Processing: Reference Model>>)中,指定了全局事务的提交要使用two-phase commit协议;而XA规范(<< Distributed Transaction Processing: The XA Specification>>)只是定义了两阶段提交协议中需要使用到的接口,也就是上述提到的RM-TM交互的接口,因为两阶段提交过程中的参与方,只有TM和RMs。参见<<Distributed Transaction Processing: Reference Model>> 第3版 2.1节,原文如下: 

Commitment Protocol A commitment protocol is the synchronisation that occurs at transaction completion. The X/Open DTP Model follows the two-phase commit with presumed rollback1 protocol defined in the referenced OSI TP standards. A description of the basic protocol is given in Section 3.4.3 on page 13. In certain cases, a global transaction may be completed heuristically. Heuristic transaction completion is described in Section 3.4.5 on page 14.

XA Interface

XA规范中定义的RM 和 TM交互的接口如下图所示:
分布式事务 - 图7
关于这些接口的详细解释,可以直接参考XA规范。mysql中对XA事务的支持时,也会使用到部分命令。

2PC

一致性模型:强一致性
两阶段提交协议(Two Phase Commit)不是在XA规范中提出,但是XA规范对其进行了优化,因此统一放到这里进行讲解。而从字面意思来理解,Two Phase Commit,就是将提交(commit)过程划分为2个阶段(Phase):
分布式事务 - 图8
阶段1:
TM通知各个RM准备提交它们的事务分支。如果RM判断自己进行的工作可以被提交,那就就对工作内容进行持久化,再给TM肯定答复;要是发生了其他情况,那给TM的都是否定答复。在发送了否定答复并回滚了已经的工作后,RM就可以丢弃这个事务分支信息。
以mysql数据库为例,在第一阶段,事务管理器向所有涉及到的数据库服务器发出prepare”准备提交”请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成”可以提交”,然后把结果返回给事务管理器。
阶段2:
TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare失败的话,则TM通知所有RM回滚自己的事务分支。
以mysql数据库为例,如果第一阶段中所有数据库都prepare成功,那么事务管理器向数据库服务器发出”确认提交”请求,数据库服务器把事务的”可以提交”状态改为”提交完成”状态,然后返回应答。如果在第一阶段内有任何一个数据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会把”可以提交”的事务回撤。

XA规范对两阶段提交协议有2点优化:
1 只读断言
在Phase 1中,RM可以断言“我这边不涉及数据增删改”来答复TM的prepare请求,从而让这个RM脱离当前的全局事务,从而免去了Phase 2。
这种优化发生在其他RM都完成prepare之前的话,使用了只读断言的RM早于AP其他动作(比如说这个RM返回那些只读数据给AP)前,就释放了相关数据的上下文(比如读锁之类的),这时候其他全局事务或者本地事务就有机会去改变这些数据,结果就是无法保障整个系统的可序列化特性——通俗点说那就会有脏读的风险。
2 一阶段提交
如果需要增删改的数据都在同一个RM上,TM可以使用一阶段提交——跳过两阶段提交中的Phase 1,直接执行Phase 2。
这种优化的本质是跳过Phase 1,RM自行决定了事务分支的结果,并且在答复TM前就清除掉事务分支信息。对于这种优化的情况,TM实际上也没有必要去可靠的记录全局事务的信息,在一些异常的场景下,此时TM可能不知道事务分支的执行结果。

2PC流程图示

  1. 准备阶段(Prepare phase):事务管理器给每个参与者发送 Prepare 消息,每个数据库参与者在本地执行事务,并写本地的 Undo/Redo 日志,此时事务没有提交。(Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件)
  2. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。

Commit情况:
分布式事务 - 图9
Rollback情况:
分布式事务 - 图10

2PC存在的问题

二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:

  1. 同步阻塞问题。两阶段提交方案下全局事务的ACID特性,是依赖于RM的。例如mysql5.7官方文档关于对XA分布式事务的支持有以下介绍:

https://dev.mysql.com/doc/refman/5.7/en/xa.html

A global transaction involves several actions that are transactional in themselves, but that all must either complete successfully as a group, or all be rolled back as a group. In essence, this extends ACID properties “up a level” so that multiple ACID transactions can be executed in concert as components of a global operation that also has ACID properties. (As with nondistributed transactions, SERIALIZABLE may be preferred if your applications are sensitive to read phenomena. REPEATABLE READ may not be sufficient for distributed transactions.)

大致含义是说,一个全局事务内部包含了多个独立的事务分支,这一组事务分支要不都成功,要不都失败。各个事务分支的ACID特性共同构成了全局事务的ACID特性。也就是将单个事务分支的支持的ACID特性提升一个层次(up a level)到分布式事务的范畴。<br />    括号中的内容的意思是: 即使在非分布事务中(即本地事务),如果对操作读很敏感,我们也需要将事务隔离级别设置为SERIALIZABLE。而对于分布式事务来说,更是如此,可重复读隔离级别不足以保证分布式事务一致性。<br />    也就是说,如果我们使用mysql来支持XA分布式事务的话,那么最好将事务隔离级别设置为SERIALIZABLE。 地球人都知道,SERIALIZABLE(串行化)是四个事务隔离级别中最高的一个级别,也是执行效率最低的一个级别。
  1. 单点故障。由于协调者的重要性,一旦协调者TM发生故障。参与者RM会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题
  2. 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。

由于二阶段提交存在着诸如同步阻塞、单点问题等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。

XA规范和2PC的实现

XA规范实现

XA规范是在数据库层面实现的,如 Oracle、MySQL 都支持 2PC 协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织 Open Group 定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。
为了让大家更明确 XA 方案的内容,下面以新用户注册送积分为例来说明:
分布式事务 - 图11
执行流程如下:
应用程序(AP)持有用户库和积分库两个数据源。
应用程序(AP)通过 TM 通知用户库 RM 新增用户,同时通知积分库RM为该用户新增积分,RM 此时并未提交事务,此时用户和积分资源锁定。
TM 收到执行回复,只要有一方失败则分别向其他 RM 发起回滚事务,回滚完毕,资源锁释放。
TM 收到执行回复,全部成功,此时向所有 RM 发起提交事务,提交完毕,资源锁释放。

DTP 模型定义如下角色:
AP(Application Program):即应用程序,可以理解为使用 DTP 分布式事务的程序。
RM(Resource Manager):即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
TM(Transaction Manager):事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个 RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
DTP 模型定义TM和RM之间通讯的接口规范叫 XA,简单理解为数据库提供的 2PC 接口协议,基于数据库的 XA 协议来实现 2PC 又称为 XA 方案

以上三个角色之间的交互方式如下:
TM 向 AP 提供 应用程序编程接口,AP 通过 TM 提交及回滚事务。
TM 交易中间件通过 XA 接口来通知 RM 数据库事务的开始、结束以及提交、回滚等。

总结
整个 2PC 的事务流程涉及到三个角色 AP、RM、TM。AP 指的是使用 2PC 分布式事务的应用程序;RM 指的是资源管理器,它控制着分支事务;TM 指的是事务管理器,它控制着整个全局事务。
(1)在准备阶段 RM 执行实际的业务操作,但不提交事务,资源锁定
(2)在提交阶段 TM 会接受 RM 在准备阶段的执行回复,只要有任一个RM执行失败,TM 会通知所有 RM 执行回滚操作,否则,TM 将会通知所有 RM 提交该事务。提交阶段结束资源锁释放。

XA方案的问题

  • 需要本地数据库支持XA协议。
  • 资源锁需要等到两个阶段结束才释放,性能较差。

分布式事务有这一篇就够了!
mysql 对XA事务的支持

2PC实现

Seata 是由阿里中间件团队发起的开源项目 Fescar,后更名为 Seata,它是一个是开源的分布式事务框架。
传统 2PC 的问题在 Seata 中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务 0 侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供 AT 模式(即 2PC)、 TCC 模式、Saga模式、XA模式的分布式事务解决方案。

Seata的设计思想如下:
Seata 的设计目标其一是对业务无侵入,因此从业务无侵入的 2PC 方案着手,在传统 2PC的基础上演进,并解决 2PC 方案面临的问题。
Seata 把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下图是全局事务与分支事务的关系图:
分布式事务 - 图12
与传统 2PC 的模型类似,Seata 定义了 3 个组件来协议分布式事务的处理过程:
分布式事务 - 图13

  • Transaction Coordinator(TC):事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收 TM 指令发起全局事务的提交与回滚,负责与 RM 通信协调各各分支事务的提交或回滚。
  • Transaction Manager(TM):事务管理器,TM 需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向 TC 发起全局提交或全局回滚的指令。
  • Resource Manager(RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器 TC 的指令,驱动分支(本地)事务的提交和回滚。

还拿新用户注册送积分举例Seata的分布式事务过程:
分布式事务 - 图14
具体的执行流程如下:

  1. 用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
  2. 用户服务的 RM 向 TC 注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局事务的管辖。
  3. 用户服务执行分支事务,向用户表插入一条记录。
  4. 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的 RM 向 TC 注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
  5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
  6. 用户服务分支事务执行完毕。
  7. TM 向 TC 发起针对 XID 的全局提交或回滚决议。
  8. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

    Seata实现2PC与XA规范的差别

    架构层次方面:
    XA 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身(通过提供支持 XA 的驱动程序来供应用使用)。
    而 Fescar 的 RM 是以二方包的形式作为中间件层部署在应用程序这一侧的,不依赖与数据库本身对协议的支持,当然也不需要数据库支持 XA 协议。这点对于微服务化的架构来说是非常重要的:应用层不需要为本地事务和分布式事务两类不同场景来适配两套不同的数据库驱动。
    这个设计,剥离了分布式事务方案对数据库在 协议支持 上的要求。
    分布式事务 - 图15
    两阶段提交方面:
    先来看一下 XA 的 2PC 过程。
    分布式事务 - 图16
    无论 Phase2 的决议是 commit 还是 rollback,事务性资源的锁都要保持到 Phase2 完成才释放。
    设想一个正常运行的业务,大概率是 90% 以上的事务最终应该是成功提交的,我们是否可以在 Phase1 就将本地事务提交呢?这样 90% 以上的情况下,可以省去 Phase2 持锁的时间,整体提高效率。
    分布式事务 - 图17
  • 分支事务中数据的 本地锁 由本地事务管理,在分支事务 Phase1 结束时释放。
  • 同时,随着本地事务结束,连接 也得以释放。
  • 分支事务中数据的 全局锁 在事务协调器侧管理,在决议 Phase2 全局提交时,全局锁马上可以释放。只有在决议全局回滚的情况下,全局锁 才被持有至分支的 Phase2 结束。

这个设计,极大地减少了分支事务对资源(数据和连接)的锁定时间,给整体并发和吞吐的提升提供了基础。

小结

本节讲解了传统 2PC(基于数据库 XA 规范)和 Seata 实现 2PC 的两种 2PC 方案,由于 Seata 的 0 侵入性并且解决了传统 2PC 长期锁资源的问题,推荐采用 Seata 实现 2PC。

3PC

一致性模型:强一致性

3PC目前还未找到具体的实现,所以3PC目前只是纯理论上的东西,而且可以看到相比于 2PC 它是做了一些努力但是效果甚微,所以只做了解即可。

三阶段提交(3PC),是二阶段提交(2PC)的改进版本。参考维基百科:https://en.wikipedia.org/wiki/Three-phase_commit_protocol
与两阶段提交不同的是,三阶段提交有两个改动点。
1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
分布式事务 - 图18
CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
1.事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
2.响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
PreCommit阶段
协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
1.发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
2.事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
3.响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
1.发送中断请求 协调者向所有参与者发送abort请求。
2.中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
doCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况。
Case 1:执行提交
1.发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
2.事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
3.响应反馈 事务提交完之后,向协调者发送Ack响应。
4.完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
Case 2:中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
1.发送中断请求 协调者向所有参与者发送abort请求
2.事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
4.中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )
小结:2PC与3PC的区别
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版。后面的文章会介绍这个公认为难于理解但是行之有效的Paxos算法。

3PC流程图示

image.png

TCC 2阶段补偿型

分布式事务有这一篇就够了!

Saga

本地消息表

可靠消息最终一致性

一致性模型:最终一致性
分布式事务有这一篇就够了!

事务消息

最大努力通知

一致性模型:最终一致性
最大努力通知

6 分布式事务中的网络异常

分布式事务七种解决方案,最后一种经典了!

7 JTA规范

JTA规范
JAVA的分布式事务(JTA和XA)
SpringBoot2 整合JTA组件,多数据源事务管理

Paxos算法和Raft算法

Paxos算法和Raft算法介绍
分布式系统中一致性问题如何解决,这就轮到分布式一致性算法Paxos算法和Raft算法登场了。

Paxos、Raft分布式一致性最佳实践
分布式系列文章——Paxos算法原理与推导
浅显易懂地解读Paxos算法
paxos算法详解
微信自研生产级paxos类库PhxPaxos实现原理介绍
PhxPaxos源码解析(1)之概述篇

Raft 协议原理详解,10 分钟带你掌握!
Raft算法动画演示
Raft算法原理和解析

结论:Paxos没看懂⊙﹏⊙|||。Raft看起来比较容易理解,可以理解成一种分布式系统一致性选举算法!

8 Seata-分布式事务开源框架

Seata

https://github.com/seata/seata

Seata github 文档
概览
原理与设计 > AT 模式核心原理与设计

Seata官网
Seata官网文档

Seata提供以下模式的分布式事务解决方案:

  • AT(Automatic Transaction)模式(2PC的实现),对业务0侵入
  • TCC(Try Confirm Cancel) 模式,2阶段补偿型
  • Saga模式
  • XA模式(XA规范实现,XA规范对2PC进行了优化),对业务0侵入

Seata示例仓库
springcloud-seata-nacos示例 这个示例有问题,仅有里面的pom依赖可以参考,整合部署请参考下面的链接
spring-cloud-alibaba-seata-demo
七步带你集成Seata 1.2 高可用搭建
seata 1.2.0整合教程

建议Seata github 文档Seata官网文档结合起来看,想要hands-on的话参考Seata示例仓库

集成Seata过程中遇到的问题

  1. 启动Seata Server的时候抛出错误:create connection SQLException

    版本:1.4.2

    启动Seata Server的时候抛出错误:create connection SQLException, url: jdbc:mysql://10.60.44.194:3306/seata?useUnicode=true&rewriteBatchedStatements=true, errorCode 0, state 08001

    错误原因:Seata服务端配置seataServer.properties(使用nacos作为配置中心)中的正确的配置项应该为:store.db.driverClassName=com.mysql.cj.jdbc.Driver。这个配置取决于mysql使用的版本,如果使用的是8.0版本那么正确的配置为store.db.driverClassName=com.mysql.cj.jdbc.Driver;如果使用的是5.x版本那么正确的配置为store.db.driverClassName=com.mysql.jdbc.Driver。

  2. 事务未回滚

    版本:1.4.2

    场景描述: 有这样一个场景,客户端1开启了全局事务,客户端1调用客户端2的接口,客户端1执行成功了,注册了branch;但是客户端2在执行sql之前业务代码抛出了异常,没有注册branch。这个情况我以为会全局回滚,但是实际情况是进行了全局提交。 请问下这中场景改如何解决。

    没有回滚解决方案1: https://seata.io/zh-cn/docs/overview/faq.html image.png

    没有回滚解决方案2: 1、先检查异常是否被catch或者有无熔断降级 异常被catch后或熔断降级后,全局事务便不感知抛出的异常。建议catch异常后返回异常码给事务发起方(TM),然后事务发起方 (TM) 。通过API的方式回滚全局事务。 2、否则请自行debug以下方法: ExecuteTemplate#execute(List, StatementProxy, StatementCallback, java.lang.Object…)如果没进入这个方法,说明数据源被没有代理。 如果没进入 if (CollectionUtils.isEmpty(sqlRecognizers)) { sqlRecognizers = SQLVisitorFactory.get(statementProxy.getTargetSQL(),dbType); }没进去说明xid没有传递

Seata集成文档

为什么要整理集成文档?

  1. Seata是个开源项目,目前处于不断的迭代中,网上搜索的集成教程可能不完全正确
  2. Seata官网的文档整理也比较混乱、文档所处的位置有:github、seata.io、gitee、微信公众号
  3. 钉钉交流群是一个不错的交流地方,但是并不会每一个问题都回复你,且回复时间也不是立即的
  4. luna给出的seata文档也不是完全正确的,仔细看过之后,其实无法真正的集成成功

基于以上原因,集成成功之后我会整理出一份正确、适用于小贝的集成文档。

版本信息:
服务器 CentOS 7.5
MySQL 8.0.x
JDK 1.8
Seata 1.4.2
Spring Boot 2.3.9.RELEASE
Spring Cloud Hoxton.SR10
Spring Cloud Alibaba 2.2.5.RELEASE
Nacos 2.0.3 && Nacos Client 1.3.2
Mybatis 3.5.6
Mybatis Plus 3.4.2

集成方式:MySQL + Seata + Nacos + Spring Boot + Spring Cloud
小贝需要集成Seata的工程:xiaobei-base,xiaobei-share

第一步:
首先访问: https://github.com/seata/seata/releases
image.png
下载我们需要使用的Seata1.4.2 binary文件源代码文件
解压binary文件后,在conf目录下有README-zh.md文件,我们可以看到Seata Server,Seata Client配置文件的信息。这些配置文件都可以在源代码文件中script目录下找到。
image.png
图 binary文件conf目录
image.png
图 binary文件conf目录下README-zh.md
image.png
图 源文件script目录

第二步:

  1. 在xiaobei-base,xiaobei-share工程对应的数据库中加入undo_log这张表,若xiaobei-base,xiaobei-share使用的是同一个数据库,则只需要加入一张undo_log表即可。

    注意:

    • sql脚本可以在源文件script/client/at/db目录下找到
    • id字段一定要加且为自增主键
    • xiaobei-base,xiaobei-share工程就是2个Seata Client
CREATE TABLE IF NOT EXISTS `undo_log`
(
        `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
        PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
  1. 在我们使用的mysql数据库中创建名为seata的库,并执行以下sql

    注意:

    1. sql脚本可以在源文件script/server/db目录下找到
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

第三步:
在你的项目中引入seata依赖

注意:

  1. 小贝2.0工程中seata的依赖完全已经由luna框架维护了,我们不需要手动引入seata依赖

如果你的微服务是dubbo:

            <dependency>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
                <version>1.4.2</version>
            </dependency>

如果你的微服务是Spring Cloud:

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
                <version>2.2.5.RELEASE</version>
                <exclusions>
                    <exclusion>
                        <groupId>io.seata</groupId>
                        <artifactId>seata-spring-boot-starter</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
                <version>1.4.2</version>
            </dependency>

第四步:
xiaobei-base/base-provider工程和xiaobei-share/share-provider工程添加seata相关配置:

  1. bootstrap.yml添加以下配置:

    luna.framework.middleware.seata.enable: true
    
  2. bootstrap.yml添加以下配置:

    注意: 项目中如果手动代理了spring boot数据源或者使用了其他动态数据源代理,这个配置需要配置成false: seata.enable-auto-data-source-proxy: false

seata:
  enabled: true
  application-id: XIAOBEI-BASE # xiaobei-share工程中这里需要改值
  tx-service-group: xiaobei_tx_group
  enable-auto-data-source-proxy: false
  data-source-proxy-mode: AT
  use-jdk-proxy: false
  # 这些配置用来让seata client定位seata server
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 10.60.44.194:9000
      group: xiaobei
      namespace: xiaobei-luna-dev
      username: "treasurycloud"
      password: "treasurycloud"
  # seata client配置放在nacos上
  config:
    type: nacos
    nacos:
      namespace: xiaobei-luna-dev
      serverAddr: 10.60.44.194:9000
      group: xiaobei
      username: "treasurycloud"
      password: "treasurycloud"
      data-id: seata.properties

第五步:
更改Seata Server中的registry.conf

  1. 服务器CentOS7.5 下载Seata Server:

    # 下载 Seata Server
    cd /root/seata
    wget https://github.com/seata/seata/releases/download/v1.4.2/seata-server-1.4.2.tar.gz
    tar -zxvf seata-server-1.4.2.tar.gz
    cd seata/seata-server-1.4.2/conf/
    
  2. 更改Seata Server中的registry.conf

image.png

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  # 使用Nacos作为注册中心
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "10.60.44.194:9000"
    group = "xiaobei"
    namespace = "xiaobei-luna-dev"
    # Seata Server集群名称
    cluster = "default"
    username = "treasurycloud"
    password = "treasurycloud"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  # 使用Nacos作为配置中心
  type = "nacos"

  nacos {
    serverAddr = "10.60.44.194:9000"
    namespace = "xiaobei-luna-dev"
    group = "xiaobei"
    username = "treasurycloud"
    password = "treasurycloud"
    dataId = "seataServer.properties"
  }
}

第六步:
Nacos添加配置

  1. 添加seata.properties,这是Seata Client(即xiaobei-base、xiaobei-share工程)的配置

image.png

# xiaobei_tx_group是Seata Client(TM&RM)事务分组,default是Seata Server(TC)集群名称
service.vgroupMapping.xiaobei_tx_group=default

# 默认配置,目的是为了列出配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
  1. 添加seataServer.properties,这是Seata Server的配置

image.png

store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://10.60.44.194:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=Fingard@1
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
  1. Seata Server和Seata Client参数配置详解

Seata参数配置详解

注意:

  1. 完整的参数配置也可以在源文件script/config-center/config.txt中找到

第七步:
运行Seata Server

cd /root/seata/seata/seata-server-1.4.2/bin/
sh seata-server.sh -h 10.60.44.194 -m db

启动成功后可看到以下日志:
image.png
也可以在Nacos看到已经注册的seata-server服务:
image.png
第八步:
运行Seata Client(即xiaobe-base、xiaobei-share工程)

  1. 在全局事务调用者(发起全局事务的服务)的接口上加入@GlobalTransactional注解,启动工程即可。

    注意: xiaobei-base调用xiaobei-share feign接口,调用xiaobei-share feign接口出现异常,必须在xiaobei-base中手动抛出RuntimeException,必须要让@GlobalTransactional感知到异常进行全局事务的回滚。
    为什么feign接口调用出现异常,必须手动抛出RuntimeException? 如果feign接口调用发生异常,但是却未抛出而将异常吞掉自行处理,此时就会发生一个现象就是:xiaobei-base发起全局事务TM,xiaobei-base本地事务RM1注册至TC且提交,xiaobei-share因为发生异常未注册本地事务RM2,这个时候因为TM中只注册了RM1且提交了,TC就会驱动TM做全局提交且提交成功。最终导致的结果是xiaobei-base更新了数据,xiaobei-share中未更新数据导致了数据的不一致。

image.png
图 @Transactional注解使用

  1. 可在Seata Client端和Seata Server端日志中看到TM和RM注册成功。

image.png
图 Seata Client端日志
image.png
图 Seata Server端日志
Seata集成完成。

Seata Server配置守护进程运行:
systemctl自定义systemd.service服务设置守护进程,seata-server可以作为守护进程运行在CentOS上。

vim /root/seata/seata/seata-server-1.4.2/startup.sh
# 文件中填入
#!/bin/bash
sh /root/seata/seata/seata-server-1.4.2/bin/seata-server.sh -p 8091 -h 10.60.44.194 -m db
vim /lib/systemd/system/seata-server.service
# 文件中填入
[Unit]
Description=seata-server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=simple
ExecStart=/root/seata/seata/seata-server-1.4.2/startup.sh
Restart=always
PrivateTmp=true

[Install]
WantedBy=multi-user.target
# 赋予权限
chmod 777 /opt/seata-server/startup.sh
chmod 777 /lib/systemd/system/seata-server.service
# 运行或重新运行
systemctl restart seata-server.service
# 运行
systemctl start seata-server.service
# 查看状态
systemctl status seata-server.service

# 设置开机自启动,看需求而定
systemctl enable seata-server.service

# 查看进程
ps -ef|grep seata-server
# 查看seata sever日志
cat /root/logs/seata/seata-server.8091.all.log

参考:
https://www.cnblogs.com/wintersoft/p/10548177.html
systemctl自定义systemd.service服务设置守护进程

Seata集成参考:
spring-cloud-alibaba-seata-demo
七步带你集成Seata 1.2 高可用搭建
seata 1.2.0整合教程

Seata分布式事务执行过程

image.png
image.png
全局提交的场景:

  1. 前端发起请求
  2. xiaobei-base接受请求,开启全局事务TM(生成全局事务XID),注册分支事务RM1至TC;xiaobei-base数据库新增点评数据&undo_log表中新增日志数据,提交数据库本地事务
  3. xiaobei-base调用xiaobei-share feign接口,等待feign接口响应
  4. xiaobei-share接受请求,注册分支事务RM2,xiaobei-base数据库更新热点问题数据&undo_log表中新增日志数据,提交数据库本地事务,返回响应成功
  5. xiaobei-base接收到feign接口响应成功
  6. 此时RM1和RM2都提交成功了,xiaobei-base中的TM向TC发起全局提交的决议
  7. TC调度TM下的RM1, RM2进行全局提交(删除undo_log表中的记录)

全局回滚的场景:

  1. 前端发起请求
  2. xiaobei-base接受请求,开启全局事务TM(生成全局事务XID),注册分支事务RM1至TC;xiaobei-base数据库新增点评数据&undo_log表中新增日志数据,提交数据库本地事务
  3. xiaobei-base调用xiaobei-share feign接口,等待feign接口响应
  4. xiaobei-share接受请求,代码抛出,返回响应失败
  5. xiaobei-base接收到feign接口响应失败,抛出RuntimeException
  6. 此时只有xiaobei-base RM1提交成功了,xiaobei-base中@Transactional感知到了抛出的RuntimeException,TM向TC发起全局回滚的决议
  7. TC调度TM下的RM1进行全局回滚(执行undo_log表中回滚的逻辑且删除undo_log表中的记录)

    Seata原理简介

    Seata概览
    原理与设计 > AT 模式核心原理与设计
    其他模式核心原理与设计,有兴趣可以看看。

Seata配置中心
Seata注册中心
Seata参数
需要关注的参数:

server端 client端
registry.type registry.type
config.type config.type
#store.mode=db需要以下配置 service.vgroupMapping.my_test_tx_group
store.db.driverClassName service.default.grouplist
store.db.url service.disableGlobalTransaction
store.db.user
store.db.password
#store.mode=redis 需要以下配置
store.redis.host
store.redis.port
store.redis.database
store.redis.password

数据源代理相关问题

MyBatisPlus多数据源 | Seata数据源代理
SpringBoot多数据源配置(一)
SpringBoot多数据源配置(二)

接口幂等性

在微服务应用中由多个服务组成,微服务应用服务调用可能会出现接口幂等性的问题。假设我们的微服务应用基于Spring Cloud架构。

幂等性场景1

场景描述:
我们应用中现在由2个服务:s1,s2,数据库分别为d1,d2。微服务应用中非常常见的一个业务场景:s1调用s2 Feign接口。我们知道s1在调用s2 Feign接口的时候会通过Ribbon进行负载均衡选择一个s2服务的一个实例进行请求,Ribbon是可以设置超时重试(默认不重试)的。
在设置了Ribbon超时重试的基础之上(比如超时时间5s,重试次数3),s1需要更新d1中的数据,s2 Feign接口需要更新d2中的数据,前端发起请求:

  1. s1执行业务代码(执行成功,d1数据更新)
  2. s1调用s2 Feign接口
  3. s2 Feign接口执行代码
  4. s1等待s2 Feign接口响应
  5. s2 Feign接口执行时间 > 5s(执行成功,d2数据更新)
  6. s1得到超时响应,因为设置了超时重试,于是又走了第2步调用了一次Feign接口

从上面请求执行步骤可以看出,当s1调用s2 Feign接口发生超时会进行重试,s2 Feign接口被调用执行了2次甚至3次即d2数据被更新了2次甚至3次,这个时候就会出现d2数据错误更新的情况导致s1和s2数据不一致。
解决方法1:
不设置超时重试:首先上述的场景中一定会加上分布式事务(Seata分布式事务中间件),当s1调用s2 Feign接口超时则直接抛出异常,这个时候就会执行全局事务的回滚。这样就可以保证s1和s2数据的一致性。
解决方法2:
设置超时重试,前端将d1、d2需要更新的数据ID分别为id1 & version1、id2 & version2传入到s1方法中:首先d1、d2数据表中更新数据支持乐观锁,s1数据更新时会有乐观锁机制,s1调用s2 Feign接口时将id2 & version2作为参数传递,s2 Feign接口数据更新数据时会有乐观锁机制。此时就算是发生了超时重试,因为传递到s2 Feign接口的id2 & version2都是不变的,且存在乐观锁机制,只有第一次更新才会成功。
其他解决方法:
TODO最好时与代码无耦合的方式。

关于Ribbon、Feign超时和重试配置:
Ribbon超时和重试配置:

ribbon:
  ReadTimeout: 2000
  ConnectTimeout: 2000
  MaxAutoRetries: 1 #同一台实例最大重试次数,不包括首次调用
  MaxAutoRetriesNextServer: 3 #重试负载均衡其他的实例最大重试次数,不包括首次调用
  OkToRetryOnAllOperations: true  #是否所有操作都重试

Feign超时和重试配置:

feign:
  client:
    config:
      default: # 默认服务配置
        connect-timeout: 3000
        read-timeout: 3000
        retryer: com.jiangls.feign.retry.MyRetryer
      s1: # s1服务配置,s1是服务在注册中心的名称
        connect-timeout: 3000
        read-timeout: 3000
        retryer: com.jiangls.feign.retry.MyRetryer
package com.jiangls.feign.retry

public class MyRetryer implements Retryer {
    @Override
    public void continueOrPropagate(RetryableException e) {
        throw e;
    }

    @Override
    public Retryer clone() {
        return new Default(100, TimeUnit.SECONDS.toMillis(1), 5);
    }
}

注意: 因为ribbon的重试机制和Feign的重试机制有冲突,所以源码中默认关闭Feign的重试机制,源码如下:

image.pngimage.png

注意: 如果Ribbon和Feign同时都配置的话,生效的只是Feign的配置。

Feign和Ribbon的重试机制
Spring cloud Ribbon Feign 超时,重试配置
OpenFeign 超时与重试