【微服务从入门到入土】Seata基础知识
分布式事务
事务概念
在分布式系统中,为了保证数据的高可用,通常,我们会将数据保留多个副本(replica),这些副本会放置在不同的物理的机器上。为了对用户提供正确的CRUD 等语义,我们需要保证这些放置在不同物理机器上的副本是一致的。分布式事务在现在遍地都是分布式部署的系统中几乎是必要的。
我们的项目用到了数据库,也和事务有关,我们先分析一下项目的问题,再描述一下事务。
如上图,如果用户打车成功,需要修改司机状态、下单、记录支付日志,而每个操作都是调用了不同的服务,比如此时 hailtaxi-driver 服务执行成功了,但是 hailtaxi-order 有可能执行失败了,这时候如何实现跨服务事务回滚呢?这就要用到分布式事务。
事务简介
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组SQL语句组成。事务应该具有4个
属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
- 原子性(atomicity):事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。(成功一起成功,失败一起失败)
- 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。(初始2方为10,10,转账5元,则一致性状态为10,10和5,15,不能出现5,10的状态)
- 隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性又分为四个级别:读未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable read,解决虚读)、串行化(serializable,解决幻读)。
- 持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,及时不能都很好的满足,也要考虑支持到什么程度。
本地事务
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务( Local Transaction )。==本地事务的ACID特性是数据库直接提供支持==。本地事务应用架构如下所示:
很多java应用都整合了spring,并使用其声明式事务管理功能来完成事务功能。一般使用的步骤如下:
1、配置事务管理器。spring提供了一个PlatformTransactionManager 接口,其有2个重要的实现类:
DataSourceTransactionManager :用于支持本地事务,事实上,其内部也是通过操作 java.sql.Connection 来开启、提交和回滚事务。
JtaTransactionManager :用于支持分布式事务,其实现了JTA规范,使用XA协议进行两阶段提交。需要注意的是,这只是一个代理,我们需要为其提供一个JTA provider,一般是Java EE容器提供的事务协调器(Java EE server’s transaction coordinator),也可以不依赖容器,配置一个本地的JTA provider。
2、 在需要开启的事务的bean的方法上添加 @Transitional 注解可以看到,spring除了支持本地事务,也支持分布式事务,下面我们先对分布式事务的典型应用场景进行介绍
分布式事务
当下互联网发展如火如荼,绝大部分公司都进行了数据库拆分和服务化(SOA)。在这种情况下,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,用需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性。
跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。下图演示了一个服务同时操作2个库的情况:
分库分表事务
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B拆分成了2个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对于sql: insert into user(id,name) values(1,"gupaoedu"),(2,"gpvp")
。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。
但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题
跨应用事务
微服务架构是目前一个比较一个比较火的概念。例如上面提到的一个案例,某个应用同时操作了9个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立服务,以简化业务逻辑。拆分后,独立服务之间通过RPC框架来进行远程调用,实现彼此的通信。下图演示了一个3个服务之间彼此调用的架构:
Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。
上述讨论的分布式事务场景中,无一例外的都直接或者间接的操作了多个数据库。如何保证事务的ACID特性,对于分布式事务实现方案而言,是非常大的挑战。同时,分布式事务实现方案还必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
分布式理论
分布式事务可以有多种分类,比如柔性事务和强一致性事务,这些事务操作会遵循一定的定理,比如CAP原理、BASE理论。
CAP原理
CAP 定理又被称作布鲁尔定理,是加州大学的计算机科学家布鲁尔在2000 年提出的一个猜想。2002 年,麻省理工学院的赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明,使之成为分布式计算领域公认的一个定理。
布鲁尔在提出CAP猜想时并没有具体定义 Consistency、Availability、Partition Tolerance 这3个词的含义,不同资料的具体定义也有差别,为了更好地解释,下面选择Robert Greiner的文章《CAP Theorem》作为参考基础
CAP定理是这样描述的:在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(PartitionTolerance)三者中的两个,另外一个必须被牺牲。
Consistency、Availability、Partition Tolerance具体解释如下:
C-Consistency 一致性
A read is guaranteed to return the most recent write for a givenclient.
对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。
这里并不是强调同一时刻(实时)拥有相同的数据,对于系统执行事务来说,在事务执行过程中,系统其实处于一个不一致的状态,不同的节点的数据并不完全一致。
==一致性强调客户端读操作能够获取最新的写操作结果==,是因为事务在执行过程中,客户端是无法读取到未提交的数据的,只有等到事务提交后,客户端才能读取到事务写入的数据,而如果事务失败则会进行回滚,客户端也不会读取到事务中间写入的数据。
一般是指最终一致性,不是实时的
A-Availability 可用性
A non-failing node will return a reasonable response within a reasonable amount of time (no error or timeout).
非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
这里强调的是合理的响应,不能超时,不能出错。注意并没有说“正确”的结果,例如,应该返回 100 但实际上返回了 90,肯定是不正确的结果,但可以是一个合理的结果。
类似熔断降级,合理但不一定正确的数据响应
P-Partition Tolerance 分区容忍性
The system will continue to function when network partitions occur.
当出现网络分区后,系统能够继续“履行职责”。
这里网络分区是指:一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障(节点间网络连接断开、节点宕机),使得有些节点之间不连通了,整个网络就分成了几块区域,数据就散布在了这些不连通的区域中。
CAP的选择
虽然 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下来思考,我们会发现必须选择 P(分区容忍)要素,因为网络本身无法做到 100%可靠,有可能出故障,所以分区是一个必然的现象。
为什么必须要选择P:
如果我们选择了 CA(一致性 + 可用性) 而放弃了 P(分区容忍性),那么当发生分区现象时,为了保证 C(一致性),系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和A(可用性) 冲突了,因为 A(可用性)要求返回 no error 和 no timeout。网络一般没法达到100%可靠
因此,分布式系统理论上不可能选择 CA (一致性 + 可用性)架构,只能选择 CP(一致性 + 分区容忍性) 或者 AP (可用性 + 分区容忍性)架构,在一致性和可用性做折中选择。
针对这两种选择再来看一下:
- 1、CP - Consistency + Partition Tolerance (一致性 + 分区容忍性)
如上图所示,因为Node1节点和Node2节点连接中断导致分区现象,Node1节点的数据已经更新到y,但是Node1 和 Node2 之间的复制通道中断,数据 y无法同步到 Node2,Node2 节点上的数据还是旧数据x。
这时客户端C 访问 Node2 时,Node2 需要返回 Error,提示客户端 “系统现在发生了错误”,这种处理方式违背了可用性(Availability)的要求,因此 CAP 三者只能满足 CP。
- 2、AP - Availability + Partition Tolerance (可用性 + 分区容忍性)
同样是Node2 节点上的数据还是旧数据x,这时客户端C 访问 Node2 时,Node2 将当前自己拥有的数据 x 返回给客户端 了,而实际上当前最新的数据已经是 y 了,这就不满足一致性(Consistency)的要求了,因此 CAP 三者只能满足 AP。
注意:这里 Node2 节点返回 x,虽然不是一个“正确”的结果,但是一个“合 理”的结果,因为 x 是旧的数据,并不是一个错乱的值,只是不是最新的数据。值得补充的是:,CAP理论告诉我们分布式系统只能选择AP或者CP,但实际上并不是说整个系统只能选择AP或者CP,在 CAP 理论落地实践时,我们需要将系统内的数据按照不同的应用场景和要求进行分类,每类数据选择不同的策略(CP 还是 AP),而不是直接限定整个系统所有数据都是同一策略。
另外,只能选择CP或者AP是指系统发生分区现象时无法同时保证C(一致性)和A(可用性),但不是意味着什么都不做,当分区故障解决后,系统还是要保持保证CA。也就是说选了AP不意味着放弃了C,选了CP不意味着放弃了A
实际中不会只是实践CAP理论,而是根据取舍,衍生了BASE理论
BASE理论
BASE 是指基本可用(Basically Available)、软状态( SoftState)、最终一致性( Eventual Consistency),是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果;核心思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。
BA-Basically Available基本可用
分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
这里的关键词是“部分”和“核心”,实际实践上,哪些是核心需要根据具体业务来权衡。例如登录功能相对注册功能更加核心,注册不了最多影响流失一部分用户,如果用户已经注册但无法登录,那就意味用户无法使用系统,造成的影响范围更大。
S-Soft State 软状态
允许系统存在中间状态,而该中间状态不会影响系统整体可用性。这里的中间状态就是 CAP 理论中的数据不一致。
E-Eventual Consistency 最终一致性
系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
这里的关键词是“一定时间” 和 “最终”,“一定时间”和数据的特性是强关联的,不同业务不同数据能够容忍的不一致时间是不同的。例如支付类业务是要求秒级别内达到一致,因为用户时时关注;用户发的最新微博,可以容忍30分钟内达到一致的状态,因为用户短时间看不到明星发的微博是无感知的。而“最 终”的含义就是不管多长时间,==最终还是要达到一致性的状态==
BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP方案的一个补充:
- CP 理论是忽略延时的,而实际应用中延时是无法避免的。
这一点就意味着完美的 CP 场景是不存在的,即使是几毫秒的数据 复制延迟,在这几毫秒时间间隔内,系统是不符合 CP 要求的。因 此 CAP 中的 CP 方案,实际上也是实现了最终一致性,只是“一定 时间”是指几毫秒而已。
- AP 方案中牺牲一致性只是指发生分区故障期间,而不是永远放弃一致性。
这一点其实就是 BASE 理论延伸的地方,分区期间牺牲一致性,但 分区故障恢复后,系统应该达到==最终一致性==
刚柔事务
何谓刚柔事务?刚性事务它的事务是原子的,要么都成功要么都失败,也就是需要保障ACID理论,而柔性事务只需要保障数据最终一致即可,需要遵循BASE理论。
- 刚性事务满足ACID理论
- 柔性事务满足BASE理论(基本可用,最终一致)
基于BASE理论的设计思想,柔性事务下,在不影响系统整体可用性的情况下(Basically Available 基本可用),允许系统存在数据不一致的中间状态(Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。并不是完全放弃了ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐。
上面CAP,BASE,都只是理论,基于理论有了下面的规范模型。
常用事务解决方案模型
分布式事务解决方案几乎都是柔性事务,分布式事务的实现有许多种,其中较经典是由Tuxedo提出的XA分布式事务协议,XA协议包含二阶段提交(2PC)和三阶段提交(3PC)两种实现。
其他还有 TCC、MQ 等最终一致性解决方案,至于工作中用哪种方案,需要根据业务场景选取, 2PC/3PC、TCC 数据强一致性高,而MQ是最终数据一致
DTP模型
X/Open DTP(X/Open Distributed Transaction Processing Reference Model) 是X/Open 这个组织定义的一套分布式事务的标准,也就是了定义了规范和API接口,由厂商进行具体的实现
- X/Open DTP中的角色
- AP(Application Program):应用程序,主要是定义事务边界以及那些组成事务的特定于应用程序的操作。
- RM(Resouces Manager):资源管理器,管理一些共享资源的自治域,如提供对诸如数据库之类的共享资源的访问。譬如:数据库、文件系统等,并且提供了这些资源的访问方式。
- TM(Transaction Manager):事务管理器,管理全局事务,协调事务的提交或者回滚,并协调故障恢复。
DTP模型里面定义了XA协议接口,TM 和 RM 通过XA接口进行双向通信
2PC
2PC 、 3PC ,都是基于 XA 协议的
方案简介
二阶段提交协议(Two-phase Commit,即2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶
段。事务的发起者称协调者,事务的执行者称参与者。
在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。
二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
核心思想就是对每一个事务都采用==先尝试后提交==的处理方式,处理后所有的读操作都要能获得最新的数据,因此也可以将二阶段提交看作是一个强一性算法
处理流程
简单一点理解,可以把协调者节点比喻为带头大哥,参与者理解比喻为跟班小弟,带头大哥统一协调跟班小弟的任务执行
- 阶段1:准备阶段
1、协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
2、各参与者执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。
3、如参与者执行成功,给协调者反馈yes,即可以提交;如执行失败,给协调者反馈no,即不可提交。
- 阶段2:提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程。
情况1,当所有参与者均反馈yes,提交事务:
1、协调者向所有参与者发出正式提交事务的请求(即commit请求)。
2、参与者执行commit请求,并释放整个事务期间占用的资源。
3、各参与者向协调者反馈ack(应答)完成的消息。
4、协调者收到所有参与者反馈的ack消息后,即完成事务提交。
情况2,当任何阶段1一个参与者反馈no,中断事务:
1、协调者向所有参与者发出回滚请求(即rollback请求)。
2、参与者使用阶段1中的undo信息执行回滚操作,并释放整个事务期间占用的资源。
3、各参与者向协调者反馈ack完成的消息。
4、协调者收到所有参与者反馈的ack消息后,即完成事务中断
方案总结
2PC是一个强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的刚性事务
2PC方案实现起来简单,实际项目中使用比较少,主要因为以下问题:
- 性能问题
所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
- 可靠性问题
如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
- 数据一致性问题
在阶段2中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
如果协调者发送rollback失败了,那就存在一致性的风险。要看业务对于一致性的容忍度了。
- 只适用于单个服务中
因为使用了XA规范,只支持单体服务的跨库分布式事务,不支持跨服务间的分布式事务
3PC
方案简介
三阶段提交协议,是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制( 2PC 中只有协调者有超时机制)。
三阶段提交将二阶段的准备阶段拆分为2个阶段,插入了一个preCommit阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
处理流程
- 阶段1:canCommit
协调者向参与者发送commit请求,参与者如果可以提交就返回yes响应(参与者不执行事务操作),否则返回no响应:
1、协调者向所有参与者发出包含事务内容的canCommit请求,询问是否可以提交事务,并等待所有参与者答复。
2、参与者收到canCommit请求后,如果认为可以执行事务操作,则反馈yes并进入预备状态,否则反馈no
- 阶段2:preCommit
协调者根据阶段1 canCommit参与者的反应情况来决定是否可以基于事务的preCommit操作。根据响应情况,有以下两种可能。
情况1,阶段1所有参与者均反馈yes,参与者预执行事务:
1、协调者向所有参与者发出preCommit请求,进入准备阶段。
2、参与者收到preCommit请求后,执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。
3、各参与者向协调者反馈ack响应或no响应,并等待最终指令
情况2,阶段1任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:
1、协调者向所有参与者发出abort请求。
2、无论收到协调者发出的abort请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
- 阶段3:do Commit
该阶段进行真正的事务提交,也可以分为以下两种情况:
情况1:阶段2所有参与者均反馈ack响应,执行真正的事务提交:
1、如果协调者处于工作状态,则向所有参与者发出do Commit请求。
2、参与者收到do Commit请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
3、各参与者向协调者反馈ack完成的消息。
4、协调者收到所有参与者反馈的ack消息后,即完成事务提交。
阶段2任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:
1、如果协调者处于工作状态,向所有参与者发出abort请求。
2、参与者使用阶段1中的undo信息执行回滚操作,并释放整个事务期间占用的资源。
3、各参与者向协调者反馈ack完成的消息。
4、协调者收到所有参与者反馈的ack消息后,即完成事务中断
注意:进入阶段3后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的do Commit请求或abort请求。此时,参与者都会在等待超时之后,继续执行事务提交。
==阶段三 只允许成功不允许失败==,如果服务器宕机或者停电,因为记录的阶段二的数据,重启服务后在提交事务,所以,到了阶段三,失败了也不进行回滚,只允许成功。
方案总结
- 优点
相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。
- 缺点
数据不一致问题依然存在,当在参与者收到preCommit请求后等待do commite指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常 通信,会导致参与者继续提交事务,造成数据不一致
TCC
方案简介
TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
==TCC是服务化的二阶段2pc编程模型==,其Try、Confirm、Cancel 3个方法均由业务编码实现;
- Try操作作为一阶段,负责资源的检查和预留。
- Confirm操作作为二阶段提交操作,执行真正的业务。
- Cancel是预留资源的取消。
TCC事务的Try、Confirm、Cancel可以理解为SQL事务中的Lock、Commit、Rollback。
TCC 为在业务层编写代码实现的两阶段提交。 TCC 分别指 Try 、 Confirm 、 Cancel ,一个业务操作要对应的写这三个方法。
- github-demo: https://github.com/changmingxie/tcc-transaction
处理流程
为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建2个步骤,库存服务和订单服务分别在不同的服务器节点上。
- 阶段一:Try 阶段
从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。TCC机制中的Try仅是一个初步操作,它和后续的确认一起才能真正
构成一个完整的业务逻辑,这个阶段主要完成:
1 完成所有业务检查( 一致性 )
2 预留必须业务资源( 准隔离性 )
3 Try 尝试执行业务
4 TCC事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
假设商品库存为100,购买数量为2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认
- 阶段2:Confirm / Cancel 阶段
根据Try阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。Confirm和Cancel操作满足幂等性,如果Confirm或Cancel操作执行失败,将会不断重试直到执行完成。
- Confirm:确认
当Try阶段服务全部正常执行, 执行确认业务逻辑操作
这里使用的资源一定是Try阶段预留的业务资源。在TCC事务机制中认为,如果在Try阶段能正常的预留资源,那Confirm一定能完整正确的提交。Confirm阶段也可以看成是对Try阶段的一个补充,Try+Confirm一起组成了一个完整的业务逻辑。
- Cancel:取消
当Try阶段存在服务执行失败, 进入Cancel阶段
Cancel取消执行,释放Try阶段预留的业务资源,上面的例子中,Cancel操作会把冻结的库存释放,并更新订单状态为取消。
方案总结
TCC事务机制相对于传统事务机制(X/Open XA),TCC事务机制相比于上面介绍的XA事务机制,有以下优点:
- 性能提升
具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性
基于Confirm和Cancel的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
- 可靠性
解决了XA协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点:
TCC的Try、Confirm和Cancel操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
本地消息表
方案简介
本地消息表的方案最初是由ebay提出,核心思路是将分布式事务拆分成本地事务进行处理。
方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样设计可以避免”业务处理成功 + 事务消息发送失败“,或”业务处理失败+事务消息发送成功“的棘手情况出现,保证2个系统事务的数据一致性。
处理流程
下面把分布式事务最先开始处理的事务方成为事务主动方,在事务主动方之后处理的业务内的其他事务成为事务被动方
为了方便理解,下面继续以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建2个步骤,库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。
事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。
整个业务处理流程如下:
步骤1 事务主动方处理本地事务。事务主动方在本地事务中处理业务更新操作和写消息表操作。上面例子中库存服务阶段在本地事务中完成扣减库存和写消息表(图中1、2)。
步骤2 事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。
消息中间件可以基于Kafka、RocketMQ消息队列,事务主动方法主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中3 - 5)。
步骤3 事务被动方通过消息中间件,通知事务主动方事务已处理的消息。上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中6 - 8)
为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。具体保存一致性的容错处理如下:
1、当步骤1处理出错,事务回滚,相当于什么都没发生。
2、当步骤2、步骤3处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询为超时消息数据,再次发送的消息中间件进行处理。事务被动方消费事务消息重试处理。
3、如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
4、如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。
方案总结
方案的优点如下:
从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对MQ中间件特性的依赖。
方案轻量,容易实现。
缺点如下:
与具体的业务场景绑定,耦合性强,不可公用。
消息数据与业务数据同库,占用业务系统资源。
业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限
MQ事务
MQ事务保证最终一致性
方案简介
基于MQ的分布式事务方案其实是对本地消息表的封装,将本地消息表存于MQ 内部,其他方面的协议基本与本地消息表一致。
处理流程
下面主要基于RocketMQ4.3之后的版本介绍MQ的分布式事务方案。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ的事务消息相对于普通MQ,相对于提供
了2PC的提交接口,方案如下:
正常情况——事务主动方发消息
这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
1、发送方向 MQ服务端(MQ Server)发送half消息。
2、MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。
3、发送方开始执行本地事务逻辑。
4、发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
5、MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
异常情况——事务主动方消息恢复
在断网或者应用重启等异常情况下,图中第4步提交的二次确认超时未到达MQ Server,此时处理逻辑如下:
5、MQ Server 对该消息发起消息回查。
6、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
7、发送方根据检查得到的本地事务的最终状态再次提交二次确认
8、MQ Server基于commit / rollback 对消息进行投递或者删除
介绍完RocketMQ的事务消息方案后,由于前面已经介绍过本地消息表方案,这里就简单介绍RocketMQ分布式事务:
事务主动方基于MQ通信通知事务被动方处理事务,事务被动方基于MQ返回处理结果。如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等
如果是事务被动方业务上的处理失败,可以通过MQ通知事务主动方进行补偿或者事务回滚。
方案总结
相比本地消息表方案,MQ事务方案优点是:
消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
吞吐量优于使用本地消息表方案。
缺点是:
一次消息发送需要两次网络请求(half消息 + commit/rollback消息)
业务处理服务需要实现消息状态回查接口
Seata
Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,开放以来,广受欢迎,不到一年已经成为最受欢迎的分布式事务解决方案。
- 官方中文网:https://seata.io/zh-cn
- github项目地址:https://github.com/seata/seata
Seata术语
- TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata 致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案
AT模式
Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。其中AT模式最受欢迎,使用也非常简单,但它内在的原理不简单。
AT模式的相关资料请参考官方文档说明:https://seata.io/zh-cn/docs/overview/what-is-seata.html
下图是AT模式的执行流程:
AT模式及工作流程
见官方文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html
Seata-Server安装
我们在选择用Seata版本的时候,可以先参考下官方给出的版本匹配(Seata版本也可以按自己的要求选择):https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
我们当前 SpringCloud Alibaba 的版本是 2.2.5.RELEASE ,对应Seata版本是1.3.0,所以我们首先安装Seata-Server1.3.0
我们直接基于docker启动得到:
1 | docker run --name seata-server -p 8091:8091 -d -e SEATA_IP=192.168.200.129 -e SEATA_PORT=8091 --restart=on-failure seataio/seata-server:1.3.0 |
集成springcloud-alibaba
我们接下来开始在项目中集成使用Seata的AT模式实现分布式事务控制,关于如何集成,官方也给出了很多例子,可以通过
https://github.com/seata/seata-samples
所以各种集成模式需要大家都自行的去翻看对应的 samples 。
集成可以按照如下步骤实现:
1:引入依赖包spring-cloud-starter-alibaba-seata
2:配置Seata
3:创建代理数据源
4:@GlobalTransactional全局事务控制
案例需求:
如上图,如果用户打车成功,需要修改司机状态、下单、记录支付日志,而每个操作都是调用了不同的服务,比如此时 hailtaxi-driver 服务执行成功了,但是 hailtaxi-order 有可能执行失败了,这时候如何实现跨服务事务回滚呢?这就要用到分布式事务。
鉴于我们一般事务都是在 service 层进行的管理,所以,改造一下hailtaxi-order 中的 OrderInfoController#add方法,将业务实现放到对应的 Service 中
1 | /*** |
在 Service 实现中:
1 |
|
案例实现:
0 创建 undo_log 表
在每个数据库中都需要创建该表:
1 | CREATE TABLE `undo_log` ( |
1 依赖引入
我们首先在hailtaxi-driver和hailtaxi-order中引入依赖:
1 | <!--seata--> |
2 配置Seata
依赖引入后,我们需要在项目中配置SeataClient端信息,关于SeataClient端配置信息,官方也给出了很多版本的模板,可以参考官方项目:
https://github.com/seata/seata/tree/1.3.0/script,如下图:
我们可以选择spring,把 application.yml 文件直接拷贝到工程中,文件如下:
完整文件内容见:https://github.com/seata/seata/blob/1.3.0/script/clie nt/spring/application.yml
修改后我们在 hailtaxi-driver 和 hailtaxi-order 项目中配置如下:
1 | seata: |
关于配置文件内容参数比较多,我们需要掌握核心部分:
seata_transaction: default:事务分组,前面的seata_transaction 可以自定义,通过事务分组很方便找到集群节点信息。
tx-service-group: seata_transaction:指定应用的事务分组,和上面 定义的分组前部分保持一致。
default: 192.168.200.129:8091:服务地址,seata-server服务地址。
注意:
现在配置信息都是托管到nacos中的,所以可以直接将配置存储到nacos中
- hailtaxi-order
- hailtaxi-driver
3 代理数据源
通过代理数据源可以保障事务日志数据和业务数据能同步,关于代理数据源早期需要手动创建,但是随着Seata版本升级,不同版本实现方案不一样了,下面是官方的介绍:
1 | 1.1.0: seata-all取消属性配置,改由注解 @EnableAutoDataSourceProxy开启,并可选择jdk proxy或者cglib proxy |
我们当前的版本是1.3.0,所以我们创建代理数据源只需要在启动类上添加@EnableAutoDataSourceProxy 注解即可,
在 hailtaxi-order 及 hailtaxi-driver 的启动类上分别添加该注解:
1 |
|
4 全局事务控制
打车成功创建订单是由客户发起,在 hailtaxi-order 中执行,并且feign调用 hailtaxi-driver ,所以 hailtaxi-order 是全局事务入口,我们在OrderInfoServiceImpl.addOrder() 方法上添加 @GlobalTransactional ,那么此时该方法就是全局事务的入口,
1 |
|
5 分布式事务测试
- 1、测试正常情况,启动测试
将 id=1 的司机状态手动改为1,然后进行测试
- 2、异常测试,在 hailtaxi-order 的service方法中添加一个异常,
1 |
|
测试前,将 id=1 的司机状态手动改为1,将订单表清空,再次测试,看状态是否被更新,订单有没有添加,以此验证分布式事务是否控制成功!
feign降级与事务
feign在做降级处理的时候无法触发seata的事务的回滚:https://github.com/seata/seata/issues/2088
1 |
|
测试,发现driver的status没有被改动,事务回滚成功
Seata TCC模式
一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic(Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
TCC实现原理:
1 | 有一个 TCC 拦截器,它会封装 Confirm 和 Cancel 方法作为资源 (用于后面 TC 来 commit 或 rollback 操作) |
Seata注册中心
参看:https://github.com/seata/seata/tree/1.3.0/script 可以看到 seata支持多种注册中心!
服务端注册中心配置
服务端注册中心(位于seata-server的registry.conf配置文件中的registry.type参数),为了实现seata-server集群高可用不会使用file类型,一般会采用第三方注册中心,例如zookeeper、redis、eureka、nacos等。我们这里使用 nacos ,seata-server的registry.conf配置如下:
由于我们是基于 docker 启动的 seata ,故可以直接进入到容器内部修改配置文件 /resources/registry.conf
1 | registry { |
此时我们再重新启动容器,访问:http://192.168.200.129:8848/nacos 看 seata 是否已注册到nacos中
客户端注册中心配置
项目中,我们需要使用注册中心,添加如下配置即可(在nacos配置中心的 hailtaxi-order.yaml 和 hailtaxi-driver-dev.yaml 都修改)
参看:https://github.com/seata/seata/tree/1.3.0/script
1 | registry: |
此时就可以注释掉配置中的default.grouplist=”192.168.200.129:8091”
完整配置如下:
1 | # seata配置 |
测试:
启动服务再次测试,查看分布式事务是否仍然能控制住!!!
Seata高可用
seata-server 目前使用的是一个单节点,能否抗住高并发是一个值得思考的问题。生产环境项目几乎都需要确保能扛高并发、具备高可用的能力,因此生产环境项目一般都会做集群。
上面配置也只是将注册中心换成了zookeeper,而且是单机版的,如果要想实现高可用,就得实现集群,集群就需要做一些动作来保证集群节点间的数据同步(会话共享)等操作
我们需要准备2个 seata-server 节点,并且 seata-server 的事务日志存储模式,共支持3种方式,
1):file【集群不可用】
2):redis
3):db
我们这里选择redis存储会话信息实现共享。
1、启动第二个 seata-server 节点
1 | docker run --name seata-server-n2 -p 8092:8092 -d -e SEATA_IP=192.168.213.130 -e SEATA_PORT=8092 --restart=on-failure seataio/seata-server:1.3.0 |
2、进入容器修改配置文件 registry.conf ,添加注册中心的配置
1 | registry { |
3、修改 seata-server 事务日志的存储模式, resources/file.conf 改动如下:
我们采用基于redis来存储集群每个节点的事务日志,通过docker允许一个redis
1 | docker run --name redis6.2 --restart=on-failure -p 6379:6379 -d redis:6.2 |
然后修改seata-server的file.conf,修改如下
如果基于DB来存储 seata-server 的事务日志数据,则需要创建数据库 seata ,表信息如下:
https://github.com/seata/seata/blob/1.3.0/script/server/db/mysql.sql
修改完后重启
注意:另一个 seata-server 节点也同样需要修改其存储事务日志的模式
4、再次启动服务测试,查看分布式事务是否依然能控制成功!