事物简介
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据
库中,一个事务由一组SQL语句组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。
这四个属性通常称为ACID特性。
原子性(atomicity):个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么
都不做。
一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务
的中间状态不能被观察到的。
隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数
据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性又分为四个级别:读
未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable
read,解决虚读)、串行化(serializable,解决幻读)。
持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库
中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,及时不能
都很好的满足,也要考虑支持到什么程度。
本地事务
@Transational
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务
(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。本地事务应用架构如下所
示:
在JDBC编程中,我们通过java.sql.Connection对象来开启、关闭或者提交事务。代码如下所
示:
1 Connection conn = ... //获取数据库连接
2 conn.setAutoCommit(false); //开启事务
3 try{
4 //...执行增删改查sql
5 conn.commit(); //提交事务
6 }catch (Exception e) {
7 conn.rollback();//事务回滚
8 }finally{
9 conn.close();//关闭链接
10 }
分布式事务典型场景
当下互联网发展如火如荼,绝大部分公司都进行了数据库拆分和服务化(SOA)。在这种情况下,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,用需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性。
典型的分布式事务场景:
跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。笔者见过
一个相对比较复杂的业务,一个业务中同时操作了9个库。
分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如
下图,将数据库B拆分成了2个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对
于sql:insert into user(id,name) values (1,“张三”),(2,“李四”)。这条sql是操作单库的语法,单库情
况下,可以保证事务的一致性。
但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数
据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都
失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。
服务化
微服务架构是目前一个比较一个比较火的概念。例如上面笔者提到的一个案例,某个应用同时操作
了9个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立
服务,以简化业务逻辑。拆分后,独立服务之间通过RPC框架来进行远程调用,实现彼此的通信。下
图演示了一个3个服务之间彼此调用的架构:
Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B
又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要
不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。
小结:上述讨论的分布式事务场景中,无一例外的都直接或者间接的操作了多个数据库。如何保证事务的ACID特性,对于分布式事务实现方案而言,是非常大的挑战。同时,分布式事务实现方案还必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
常见分布式事务解决方案
1、seata 阿里分布式事务框架
2、消息队列
3、saga
4、XA
分布式事物理论基础
目前市面主流的分布式事务解决方案都是2PC协议。这就是文章开始提及的常见分布式事务解决方案里面,那些列举的都有一个共同点“两阶段”的内在原因。有些文章分析2PC时,几乎都会用TCC两阶段的例子,第一阶段try,第二阶段完成confirm或cancel。其实2PC并不是专为实现TCC设计的,2PC具有普适性——协议一样的存在,目前绝大多数分布式解决方案都是以两阶段提交协议2PC为基础的。TCC(Try-Confirm-Cancel) 实际上是服务化的两阶段提交协议。
2PC两阶段提交协议,分为Prepare和Commit
prepare:事物提交事务请求
- 询问 协调者向所有参与者发送事务请求,询问是否可执行事务操作,然后等待各个参与者的响应。
- 执行 各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据库表中的记录),并将
Undo 和 Redo 信息记录事务日志中。 - 响应 如果参与者成功执行了事务并写入 Undo 和 Redo 信息,则向协调者返回 YES 响应,否则返回 NO
响应。当然,参与者也可能宕机,从而不会返回响应
Commit:执行事务提交
执行事务提交分为两种情况,正常提交和回退。
- commit 请求 协调者向所有参与者发送 Commit 请求。
- 事务提交 参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。
- 反馈结果 参与者执行事务提交后向协调者发送 Ack 响应。
- 完成事务 接收到所有参与者的 Ack 响应后,完成事务提交。
中断事务
在执行 Prepare 步骤过程中,如果某些参与者执行事务失败、宕机或与协调者之间的网络中断,那么协调者就无法
收到所有参与者的 YES 响应,或者某个参与者返回了 No 响应,此时,协调者就会进入回退流程,对事务进行回
退。流程如下图红色部分(将 Commit 请求替换为红色的 Rollback 请求):
- rollback 请求 协调者向所有参与者发送 Rollback 请求。
- 事务回滚 参与者收到 Rollback 后,使用 Prepare 阶段的 Undo 日志执行事务回滚,完成后释放事务执行
期占用的所有资源。 - 反馈结果 参与者执行事务回滚后向协调者发送 Ack 响应。
- 中断事务 接收到所有参与者的 Ack 响应后,完成事务中断。
2PC的问题
- 同步阻塞 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进
行其他操作的,也就是阻塞了其运行。 倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信
息,那么会导致参与者一直阻塞下去。 - 单点 在 2PC 中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会
使参与者一直阻塞并一直占用事务资源。
如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服
务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待 Prepare 响应的时长
等),所以也无法顺利处理上一个事务。 - 数据不一致 Commit 事务过程中 Commit 请求/Rollback 请求可能因为协调者宕机或协调者与参与者网
络问题丢失,那么就导致了部分参与者没有收到 Commit/Rollback 请求,而其他参与者则正常收到执行了
Commit/Rollback 操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。
当参与者执行 Commit/Rollback 后会向协调者发送 Ack,然而协调者不论是否收到所有的参与者的 Ack,该事务
也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个“我不确定该事务是否成
功”。 - 环境可靠性依赖 协调者 Prepare 请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络
中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC 中,协调者会等待一定时间,然后超时后,
会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现
实环境来说太苛刻了。
AT模式(auto transcation)分布式事物实现
AT 模式是一种无侵入的分布式事务解决方案。
阿里seata框架,实现了该模式。
在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
AT 模式如何做到对业务的无侵入 :
一阶段:
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数
据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,
在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库
事务内完成,这样保证了一阶段操作的原子性。
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一
阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能
轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
TCC模式
- 侵入性比较强, 并且得自己实现相关事务控制逻辑
2.在整个过程基本没有锁,性能更强
TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶
段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。
TCC 三个方法描述:
Try:资源的检测和预留;
Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
Cancel:预留资源释放;
TCC 的实践经验
蚂蚁金服TCC实践,总结以下注意事项:
➢业务模型分2阶段设计
➢并发控制
➢允许空回滚
➢防悬挂控制
➢幂等控制
1 TCC 设计 – 业务模型分 2 阶段设计:
用户接入 TCC ,最重要的是考虑如何将自己的业务模型拆成两阶段来实现。
以“扣钱”场景为例,在接入 TCC 前,对 A 账户的扣钱,只需一条更新账户余额的 SQL 便能完
成;但是在接入 TCC 之后,用户就需要考虑如何将原来一步就能完成的扣钱操作,拆成两阶段,实
现成三个方法,并且保证一阶段 Try 成功的话 二阶段 Confirm 一定能成功。
如上图所示,Try 方法作为一阶段准备方法,需要做资源的检查和预留。在扣钱场景下,Try 要做的
事情是就是检查账户余额是否充足,预留转账资金,预留的方式就是冻结 A 账户的 转账资金。Try
方法执行之后,账号 A 余额虽然还是 100,但是其中 30 元已经被冻结了,不能被其他事务使用。
二阶段 Confirm 方法执行真正的扣钱操作。Confirm 会使用 Try 阶段冻结的资金,执行账号扣款。
Confirm 方法执行之后,账号 A 在一阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元 。
如果二阶段是回滚的话,就需要在 Cancel 方法内释放一阶段 Try 冻结的 30 元,使账号 A 的回到初
始状态,100 元全部可用。
用户接入 TCC 模式,最重要的事情就是考虑如何将业务模型拆成 2 阶段,实现成 TCC 的 3 个方
法,并且保证 Try 成功 Confirm 一定能成功。相对于 AT 模式,TCC 模式对业务代码有一定的侵入
性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。
2.TCC设计-允许空回滚
Cancel 接口设计时需要允许空回滚。在 Try 接口因为丢包时没有收到,事务管理器会触发回滚,这
时会触发 Cancel 接口,这时 Cancel 执行时发现没有对应的事务 xid 或主键时,需要返回回滚成
功。让事务服务管理器认为已回滚,否则会不断重试,而 Cancel 又没有对应的业务数据可以进行回
滚。
3.TCC设计-防悬挂控制
悬挂的意思是:Cancel 比 Try 接口先执行,出现的原因是 Try 由于网络拥堵而超时,事务管理器生
成回滚,触发 Cancel 接口,而最终又收到了 Try 接口调用,但是 Cancel 比 Try 先到。按照前面允
许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,则此时的 Try 接口不应该执
行,否则会产生数据不一致,所以我们在 Cancel 空回滚返回成功之前先记录该条事务 xid 或业务主
键,标识这条记录已经回滚过,Try 接口先检查这条事务xid或业务主键如果已经标记为回滚成功过,
则不执行 Try 的业务操作。
4.TCC设计 - 幂等控制
幂等性的意思是:对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是
一致的。因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试操作,所以很可能一个业务
操作会被重复调用,为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制,通常我
们可以用事务 xid 或业务主键判重来控制。
Saga模式
saga模式的实现,是长事务解决方案。
Saga 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正
补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
如图:T1T3都是正向的业务流程,都对应着一个冲正逆向操作C1C3
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式
事务提交。如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚
操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga 正向服务与补偿服务也需要业务开发者实现。因此是业务入侵的。
Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事
务解决方案。
Saga 模式使用场景
Saga 模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地
事务,无锁、长流程情况下可以保证性能。
事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,
可以使用 Saga 模式。
Saga模式的优势是:
一阶段提交本地数据库事务,无锁,高性能;
参与者可以采用事务驱动异步执行,高吞吐;
补偿服务即正向服务的“反向”,易于理解,易于实现;
缺点:Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔
离性。后续会讲到对于缺乏隔离性的应对措施。
与TCC实践经验相同的是,Saga 模式中,每个事务参与者的冲正、逆向操作,需要支持:
空补偿:逆向操作早于正向操作时;
防悬挂控制:空补偿后要拒绝正向操作
幂等
XA模式
XA模式
XA是X/Open DTP组织(X/Open DTP group)定义的两阶段提交协议,XA被许多数据库(如
Oracle、DB2、SQL Server、MySQL)和中间件等工具(如CICS 和 Tuxedo)本地支持 。
X/Open DTP模型(1994)包括应用程序(AP)、事务管理器(TM)、资源管理器(RM)。
XA接口函数由数据库厂商提供。XA规范的基础是两阶段提交协议2PC。
JTA(Java Transaction API) 是Java实现的XA规范的增强版 接口。
在XA模式下,需要有一个[全局]协调器,每一个数据库事务完成后,进行第一阶段预提交,并通知协
调器,把结果给协调器。协调器等所有分支事务操作完成、都预提交后,进行第二步;第二步:协调
器通知每个数据库进行逐个commit/rollback。
其中,这个全局协调器就是XA模型中的TM角色,每个分支事务各自的数据库就是RM。
MySQL 提供的XA实现(https://dev.mysql.com/doc/refman/5.7/en/xa.html )
XA模式下的 开源框架有atomikos,其开发公司也有商业版本。
XA模式缺点:事务粒度大。高并发下,系统可用性低。因此很少使用。
(AT、TCC、Saga、XA)模式分析
四种分布式事务模式,分别在不同的时间被提出,每种模式都有它的适用场景
AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学
习成本。
TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,
Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成
层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供
TCC 要求的接口,也可以使用 Saga 模式。
XA模式是分布式强一致性的解决方案,但性能低而使用较少。
Seata分布式事务的使用
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)
Seata的三大角色
在 Seata 的架构中,一共有三个角色:TC (Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,驱动全局事务提交或回滚。TM (Transaction Manager) - 事务管理器定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。
分布式事务的生命周期
在 Seata 中,一个分布式事务的生命周期如下:
1.TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID,会在微服务的调用链路中传播,保证将多个微服务
的子事务关联在一起。
当一进入事务方法中就会生成XID , global_table 就是存储的全局事务信息 ,
2.RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
当运行数据库操作方法,branch_table 存储事务参与者
3.TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
4.TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如图
第一阶段
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undolog,并同时
入库,这是怎么做的呢?先抛出一个概念DataSourceProxy代理数据源,通过名字大家大概也能基本猜到是什么个操作,后面做具体分析
第二阶段
分布式事务操作成功,则TC通知RM异步删除undolog
分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
相比与其它分布式事务框架,Seata架构的亮点主要有几个:
- 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
- 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;
- 通过全局锁实现了写隔离与读隔离
性能损耗
一条Update的SQL,则需要全局事务xid获取(与TC通讯)、before image(解析SQL,查询一次数据库)、after image(查询一次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要一次远程通讯RPC,而且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间。性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?
全局锁热点数据相比XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间,这个开销比XA的prepare低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重。回滚锁释放时间
Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。
死锁问题
Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。
Seata快速使用
https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
下载安装包
Server端存储模式(store.mode)支持三种:
file:(默认)单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认)
db:(5.7+)高可用模式,全局事务会话信息通过db共享,相应性能差些
打开config/file.conf
修改mode=“db”
修改数据库连接信息(URL\USERNAME\PASSWORD)
创建数据库seata_server
新建表: 可以去seata提供的资源信息中下载:
点击查看
\script\server\db\mysql.sql
branch 表 存储事务参与者的信息
1
2 store {
3 mode = "db"
4
5 db {
6 ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hik
ari) etc.
7 datasource = "druid"
8 ## mysql/oracle/postgresql/h2/oceanbase etc.
9 dbType = "mysql"
10 driverClassName = "com.mysql.jdbc.Driver"
11 url = "jdbc:mysql://192.168.65.220:3306/seata_server"
12 user = "root"
13 password = "123456"
14 minConn = 5
15 maxConn = 30
16 globalTable = "global_table"
17 branchTable = "branch_table"
18 lockTable = "lock_table"
19 queryLimit = 100
20 maxWait = 5000
21 }
22 }
redis:Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置
资源目录:https://github.com/seata/seata/tree/1.3.0/script
client
存放client端sql脚本,参数配置
config-center
各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件
server
server端数据库脚本及各个容器配置
步骤五:配置Nacos注册中心 负责事务参与者(微服务) 和TC通信
将Seata Server注册到Nacos,修改conf目录下的registry.conf配置
然后启动注册中心Nacos Server
1 #进入Nacos安装目录,linux单机启动
2 bin/startup.sh ‐m standalone
3 # windows单机启动
4 bin/startup.bat
步骤六:配置Nacos配置中心
图灵课堂
注意:如果配置了seata server使用nacos作为配置中心,则配置信息会从nacos读取,file.conf可以不用配置。 客户端配置registry.conf
使用nacos时也要注意group要和seata server中的group一致,默认group是"DEFAULT_GROUP"
获取/seata/script/config-center/config.txt,修改配置信息
配置事务分组, 要与客户端配置的事务分组一致
#my_test_tx_group需要与客户端保持一致 default需要跟客户端和registry.conf中registry中的cluster保持一致
(客户端properties配置:spring.cloud.alibaba.seata.tx‐service‐group=my_test_tx_group)
事务分组: 异地机房停电容错机制
my_test_tx_group 可以自定义 比如:(guangzhou、shanghai…) , 对应的client也要去设置
1 seata.service.vgroup‐mapping.projectA=guangzhou
default 必须要等于 registry.confi cluster = “default”
配置参数同步到Nacos
shell:
1 sh ${SEATAPATH}/script/config‐center/nacos/nacos‐config.sh ‐h localhost ‐p 8848 ‐g SEATA_GROUP ‐t 5a3c7d6c‐f497‐
4d68‐a71a‐2e5e3340b3ca
参数说明:
-h: host,默认值 localhost
-p: port,默认值 8848
-g: 配置分组,默认值为 ‘SEATA_GROUP’
图灵课堂
-t: 租户信息,对应 Nacos 的命名空间ID字段, 默认值为空 ‘’
精简配置
1 service.vgroupMapping.my_test_tx_group=default
2 service.default.grouplist=127.0.0.1:8091
3 service.enableDegrade=false
4 service.disableGlobalTransaction=false
5 store.mode=db
6 store.db.datasource=druid
7 store.db.dbType=mysql
8 store.db.driverClassName=com.mysql.jdbc.Driver
9 store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
10 store.db.user=root
11 store.db.password=root
12 store.db.minConn=5
13 store.db.maxConn=30
14 store.db.globalTable=global_table
15 store.db.branchTable=branch_table
16 store.db.queryLimit=100
17 store.db.lockTable=lock_table
18 store.db.maxWait=5000
步骤七:启动Seata Server
源码启动: 执行server模块下io.seata.server.Server.java的main方法
命令启动: bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
启动Seata Server
1 bin/seata‐server.sh ‐p 8091 ‐n 1
1 bin/seata‐server.sh ‐p 8092 ‐n 2
1 bin/seata‐server.sh ‐p 8093 ‐n 3
启动成功,默认端口8091
在注册中心中可以查看到seata-server注册成功
Seata Client快速开始
声明式事务实现(@GlobalTransactional)
接入微服务应用
业务场景:
用户下单,整个业务逻辑由三个微服务构成:
订单服务:根据采购需求创建订单。
库存服务:对给定的商品扣除库存数量。
1)启动Seata server端,Seata server使用nacos作为配置中心和注册中心(上一步已完成)
2)配置微服务整合seata
第一步:添加pom依赖
1 <!‐‐ seata‐‐>
2 <dependency>
3 <groupId>com.alibaba.cloud</groupId>
4 <artifactId>spring‐cloud‐starter‐alibaba‐seata</artifactId>
5 </dependency>
第二步: 各微服务对应数据库中添加undo_log表
1 CREATE TABLE `undo_log` (
2 `id` bigint(20) NOT NULL AUTO_INCREMENT,
3 `branch_id` bigint(20) NOT NULL,
4 `xid` varchar(100) NOT NULL,
5 `context` varchar(128) NOT NULL,
6 `rollback_info` longblob NOT NULL,
7 `log_status` int(11) NOT NULL,
8 `log_created` datetime NOT NULL,
9 `log_modified` datetime NOT NULL,
10 PRIMARY KEY (`id`),
11 UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
12 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
第五步:修改register.conf,配置nacos作为registry.type&config.type,对应seata server也使用nacos
注意:需要指定group = “SEATA_GROUP”,因为Seata Server端指定了group = “SEATA_GROUP” ,必须保证一致
1 registry {
2 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
3 type = "nacos"
4
5 nacos {
6 serverAddr = "localhost"
7 namespace = ""
8 cluster = "default"
9 group = "SEATA_GROUP"
10 }
11 }
12 config {
13 # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
14 type = "nacos"
15
16 nacos {
17 serverAddr = "localhost"
18 namespace = ""
19 group = "SEATA_GROUP"
20 }
21 }
22
如果出现这种问题:
一般大多数情况下都是因为配置不匹配导致的:
1.检查现在使用的seata服务和项目maven中seata的版本是否一致
2.检查tx-service-group,nacos.cluster,nacos.group参数是否和Seata Server中的配置一致
跟踪源码:seata/discover包下实现了RegistryService#lookup,用来获取服务列表
1 NacosRegistryServiceImpl#lookup
2 》String clusterName = getServiceGroup(key); #获取seata server集群名称
3 》List firstAllInstances = getNamingInstance().getAllInstances(getServiceName(), getServiceGroup(), cluste
rs)
第六步:修改application.yml配置
配置seata 服务事务分组,要与服务端nacos配置中心中service.vgroup_mapping的后缀对应
1 server:
2 port: 8020
3
4 spring:
5 application:
6 name: order‐service
7 cloud:
8 nacos:
9 discovery:
10 server‐addr: 127.0.0.1:8848
11 alibaba:
12 seata:
13 tx‐service‐group:
14 my_test_tx_group # seata 服务事务分组
15
16 datasource:
17 type: com.alibaba.druid.pool.DruidDataSource
18 druid:
19 driver‐class‐name: com.mysql.cj.jdbc.Driver
20 url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF‐8&serverTimezone=Asia/Shanghai
21 username: root
22 password: root
23 initial‐size: 10
24 max‐active: 100
25 min‐idle: 10
26 max‐wait: 60000
27 pool‐prepared‐statements: true
28 max‐pool‐prepared‐statement‐per‐connection‐size: 20
29 time‐between‐eviction‐runs‐millis: 60000
30 min‐evictable‐idle‐time‐millis: 300000
31 test‐while‐idle: true
32 test‐on‐borrow: false
33 test‐on‐return: false
34 stat‐view‐servlet:
35 enabled: true
36 url‐pattern: /druid/*
37 filter:
38 stat:
39 log‐slow‐sql: true
40 slow‐sql‐millis: 1000
41 merge‐sql: false
42 wall:
43 config:
44 multi‐statement‐allow: true
第七步:微服务发起者(TM 方)需要添加@GlobalTransactional注解
1 @Override
2 //@Transactional
3 @GlobalTransactional(name="createOrder")
4 public Order saveOrder(OrderVo orderVo){
5 log.info("=============用户下单=================");
6 log.info("当前 XID: {}", RootContext.getXID());
7
8 // 保存订单
9 Order order = new Order();
10 order.setUserId(orderVo.getUserId());
11 order.setCommodityCode(orderVo.getCommodityCode());
12 order.setCount(orderVo.getCount());
13 order.setMoney(orderVo.getMoney());
14 order.setStatus(OrderStatus.INIT.getValue());
15
16 Integer saveOrderRecord = orderMapper.insert(order);
17 log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");
18
19 //扣减库存
20 storageFeignService.deduct(orderVo.getCommodityCode(),orderVo.getCount());
21
22 //扣减余额
23 accountFeignService.debit(orderVo.getUserId(),orderVo.getMoney());
24
25 //更新订单
26 Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue());
27 log.info("更新订单id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失败");
28
29 return order;
30
31 }