三、初识Seata
Seata是的 2019 年 1 蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于为用户提供高性能、简单易用的分布式服务,打造一站式的分布式解决方案。
官网地址:http://seata.io/,在文档和播客中提供了大量的使用说明和源代码分析。
3.1.Seata的架构Seata事务管理有三个重要角色:
TC (Transaction Coordinator) - **事务协调员:**维护全局和分支事务状态,协调全局事务提交或回滚。
TM (Transaction Manager) - **事务管理器:**定义全局事务范围,开始全局事务,提交或回滚全局事务。
RM (Resource Manager) - **资源管理器:**管理处理分支事务的资源,与TC谈判,注册分支事务,报告分支事务状态,推动分支事务提交或回滚。
整体架构如图所示:
基于上述结构,Seata提供了四种不同的分布式事务解决方案:
- XA模式:分阶段事务模式的强一致性,牺牲了一定的可用性,没有业务入侵
- TCC模式:最终一致的分阶段业务模式,业务入侵
- AT模式:最终一致的分阶段业务模式,无业务入侵,也是Seata的默认模式
- SAGA模式:长期业务模式,业务入侵
无论哪种方案,都离不开TC,也就是事务的协调者。
3.2.部署TC服务参考博主上一篇文章的部署,链接地址
3.3.Seatatatata集成微服务集成以order-service为例。
3.3.1.引入依赖首先,在order-service中引入依赖:
<!--seata--><dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <!--版本较低,1.3.0,因此排除--> <exclusion> <artifactId>seata-spring-boot-starter</artifactId> <groupId>io.seata</groupId> </exclusion> </exclusions></dependency><dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <!--seata starter 采用1.4.2版本--> <version>${seata.version}</version></dependency>
3.3.2.配置TC地址aplication在order-service中.在yml中,配置TC服务信息,通过注册中心nacos,结合服务名称获取TC地址:
seata: registry: # 配置TC服务注册中心,根据这些信息,微服务到注册中心获取TC服务地址 type: nacos # 注册中心类型 nacos nacos: server-addr: 127.0.0.1:8848 # nacos地址 namespace: "" # namespace,默认为空 group: DEFAULT_GROUP # 分组,默认为DEFAULT_GROUP application: seata-tc-server # seata服务名称 username: nacos password: nacos tx-service-group: seata-demo # 事务组名称 service: vgroup-mapping: # 事务组与cluster的映射关系 seata-demo: SH
根据这些配置,微服务如何找到TC地址?
在Nacos中注册微服务,需要四个信息来确定一个具体的例子:
- namespace:命名空间
- group:分组
- application:服务名
- cluster:集群名
以上四个信息可以在刚才的yaml文件中找到:
namespace是空的,是默认的publice
TC服务的信息相结合:public@DEFAULT_GROUP@seata-tc-server@SH,这样,TC服务集群就可以确定了。然后你可以去Nacos获取相应的实例信息。
3.3.3.其它服务另外两个微服务也参考order-service的步骤,完全一样。
4.动手实践让我们一起学习Seata中的四种不同的事务模式。
4.1.XA模式XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 它描述了TM与局部RM之间的接口,几乎所有主流数据库都是对的 XA 规范 提供支持。
4.1.1.两阶段提交XA是标准。目前,主流数据库已经实现了这一标准,实现的原则是基于两个阶段的提交。
正常情况:
异常情况:
一阶段:
- 事务协调员通知每个事务参与者执行本地事务
- 当地事务执行完成后,向事务协调员报告事务执行状态。此时,如果事务未提交,继续持有数据库锁
二阶段:
- 事务协调员根据第一阶段的报告判断下一步的操作
- 一阶段成功的,通知所有事务参与者提交事务
- 任何一个参与者在第一阶段失败的,通知所有事务参与者回滚事务
为了适应自己的事务模型,Seata对原有的XA模式进行了简单的包装和改造,基本结构如图所示:
RM一阶段工作:
① 向TC注册分支机构
② 执行sql分支业务,但不提交
③ 报告执行到TC
TC二阶段工作:
TC检测各分支事务的执行状态
a.如果一切成功,通知所有RM提交事务
b.如有失败,通知所有RM回滚事务
RM二阶段工作:
- 接收TC指令,提交或回滚事务
XA模式的优点是什么?
- 符合ACID原则的事务强一致性。
- 支持常用数据库,实现简单,没有代码入侵
XA模式的缺点是什么?
- 由于第一阶段需要锁定数据库资源,直到第二阶段结束才释放,性能差
- 依靠关系数据库实现事务
Seata的Starter已经完成了XA模式的自动组装,步骤如下:
修改application).开启XA模式的yml文件(每个参与事务的微服务):
seata: data-source-proxy-mode: XA
2)添加@Globaltransactional注释发起全局事务的入口方法:
本例是OrderServiceimpl中的create方法.
3)重启服务并进行测试
重启order-service,再次测试,发现三个微服务无论如何都能成功回滚。
4.2.AT模式AT模型也是分阶段提交的事务模型,但缺陷弥补了XA模型中资源锁定周期过长的缺陷。
4.2.1.SeataAT模型基本流程图:
阶段一RM工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
提交阶段二时RM的工作:
- 删除undo-log即可
RM在第二阶段回滚时的工作:
- 在更新之前,根据undo-log恢复数据
我们用真正的业务来梳理AT模式的原理。
例如,记录用户余额的另一个数据库表:
SQL的分支业务之一是:
update tb_account set money = money - 10 where id = 1
在AT模式下,当前分支的执行流程如下:
一阶段:
1)TM发起并向TC注册全局事务
2)TM调用分支事务
3)分支业务准备执行SQL业务
4)RM拦截业务SQL,根据where条件查询原始数据,形成快照。
{ "id": 1, "money": 100}
5)RM执行SQL业务,提交本地事务,释放数据库锁。 money = 90
6)RM向TC报告当地事务状态
二阶段:
1)TM通知TC事务结束
2)TC检查分支的事务状态
a)如果都成功了,立即删除快照
b)如果分支失败,需要回滚。阅读快照数据({"id": 1, "money": 100}
),将快照恢复到数据库。此时,数据库再次恢复到100
流程图:
4.2.3.AT和XA的区别AT模式和XA模式最大的区别是什么?
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依靠数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
- XA模式强烈一致;AT模式最终一致;
多线程并发访问AT模式的分布式事务时,可能会出现脏写问题,如图所示:
解决方案是引入全局锁的概念。在释放DB锁之前,先获得全局锁。避免同时操作当前数据的另一项事务。
4.2.5.优缺点AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能更好
- 利用全局锁实现读写隔离
- 无代码入侵,框架自动完成回滚和提交
AT模式的缺点:
- 两个阶段属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式好得多
AT模式中的快照生成、回滚等动作由框架自动完成,无代码入侵,因此实现非常简单。
然而,AT模式需要一个表来记录整体锁,另一个表来记录数据快照undo_log。
1)导入数据库表,记录全局锁定
导入提供的SQL文件:seata-at.sql,lock_table导入TC服务相关数据库,undo_log表导入与微服务相关的数据库:
https://aliyun.chengke.net/resource/sqlFile/account_freeze_tbl.sql
https://aliyun.chengke.net/resource/sqlFile/seata-at.sql
https://aliyun.chengke.net/resource/sqlFile/seata-demo.sql
https://aliyun.chengke.net/resource/sqlFile/seata-saga.sql
https://aliyun.chengke.net/resource/sqlFile/seata-tc-server.sql
2)修改application.yml文件可以将事务模式修改为AT模式:
seata: data-source-proxy-mode: AT # 默认是AT
3)重启服务并进行测试
4.3.TCC模式TCC模式与AT模式非常相似,每个阶段都是独立的。不同之处在于,TCC通过人工编码实现数据恢复。有三种方法可以实现:
Try:检测和预留资源;
Confirm:完成资源运营业务;要求 Try 成功 Confirm 一定要成功。
Cancel:预留资源的释放可以理解为try的反向操作。
例如,一个扣除用户余额的业务。假设账户A的原始余额为100元,则需要扣除30元。
- 阶段一( Try ):检查余额是否充足,冻结金额增加30元,可扣除30元
初识余额:
余额充足,可冻结:
此时,总金额 = 冻结金额 + 可用金额,数量仍为100不变。直接提交事务不需要等待其他事务。
- 阶段二(Confirm):假如要提交(Confirm),冻结金额扣除30
确认可以提交,但之前可用金额已经扣除,这里只需清除冻结金额即可:
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
- 阶段二(Canncel):如果要回滚(Cancel),冻结金额扣除30,可用余额增加30
如需回滚,则需释放冻结金额,恢复可用金额:
4.3.2.TCC模型SeataSeata中的TCC模型仍然延续以前的事务架构,如图所示:
4.3.3.优缺点TCC模式的每个阶段都在做什么?
- Try:检查和预留资源
- Confirm:业务执行和提交
- Cancel:释放预留资源
TCC的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能良好
- 与AT模型相比,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可用于非事务数据库
TCC的缺点是什么?
- 有代码入侵,需要人工编写try、Confirm和Cancel接口太麻烦了
- 软状态,事务是最终一致的
- 要考虑Confirm和Cancel的失败,做好权力处理
当分支事务的try阶段被阻塞时,可能会导致整体事务加班,触发第二阶段的cancel操作。cancel操作在未执行try操作时首先执行。此时,cancel无法回滚,即空回滚。
如图:
在执行cancel操作时,应判断try是否已经执行,如果没有执行,则应空回滚。
2)业务悬挂对于已经空回滚的业务,之前被阻塞的try操作恢复并继续执行try,confirm或cancel将永远不可能。 ,业务一直处于中间状态,这就是业务悬挂。
在执行try操作时,应判断cancel是否已经执行。如果已经执行,应防止空回滚后的try操作,避免悬挂
4.3.5.实现TCC模式为了解决空气回滚和业务悬挂的问题,有必要记录当前的业务状态,即在try、还是cancel?
1)思路分析在这里,我们定义一张表:
CREATE TABLE `account_freeze_tbl` ( `xid` varchar(128) NOT NULL, `user_id` varchar(255) DEFAULT NULL COMMENT 用户id, `freeze_money` int(11) unsigned DEFAULT '0' COMMENT "冻结金额", `state` int(1) DEFAULT NULL COMMENT 事务状态,0:try,1:confirm,2:cancel', PRIMARY KEY (`xid`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
其中:
- xid:是全局事务id
- freeze_money:用来记录用户冻结的金额
- state:用于记录事务状态
那时,我们的业务开业怎么办?
- Try业务:
- 记录冻结金额和事务状态到account_freeze表格
- 扣除account表的可用金额
- Confirm业务
- 根据xid删除account_freeze表的冻结记录
- Cancel业务
- 修改account_freeze表格,冻结金额为0,state为2
- 修改account表,恢复可用金额
- 如何判断是否空回滚?
- 在cancel业务中,根据xid查询account_freeze,如果是null,说明try还没有做,需要空回滚
- 如何避免业务悬挂?
- 在try业务中,根据xid查询account_freeze ,如果已经存在,证明Cancel已经执行,拒绝执行try业务
接下来,我们将改造account-service,利用TCC实现余额扣除功能。
2)声明TCC接口TCC的Try、Confirm、Cancel方法需要在界面中基于注释进行声明,
我们在account-service项目中cn.itcast.account.service
包中新建一个接口,声明TCC三个接口:
package cn.itcast.account.service;import io.seata.rm.tcc.api.BusinessActionContext;import io.seata.rm.tcc.api.BusinessActionContextParameter;import io.seata.rm.tcc.api.LocalTCC;import io.seata.rm.tcc.api.TwoPhaseBusinessAction;@LocalTCCpublic interface AccountTCCService { @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money")int money); boolean confirm(BusinessActionContext ctx); boolean cancel(BusinessActionContext ctx);}
3)编写实现类在account-service服务中cn.itcast.account.service.impl
包下新建一类,实现TCC业务:
package cn.itcast.account.service.impl;import cn.itcast.account.entity.AccountFreeze;import cn.itcast.account.mapper.AccountFreezeMapper;import cn.itcast.account.mapper.AccountMapper;import cn.itcast.account.service.AccountTCCService;import io.seata.core.context.RootContext;import io.seata.rm.tcc.api.BusinessActionContext;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Service@Slf4jpublic class AccountTCCServiceImpl implements AccountTCCService { @Autowired private AccountMapper accountMapper; @Autowired private AccountFreezeMapper freezeMapper; @Override @Transactional public void deduct(String userId, int money) { // 0.获取事务id String xid = RootContext.getXID(); // 1.扣除可用余额 accountMapper.deduct(userId, money); // 2.记录冻结金额,事务状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freeze.setXid(xid); freezeMapper.insert(freeze); } @Override public boolean confirm(BusinessActionContext ctx) { // 1.获取事务ID String xid = ctx.getXid(); // 2.根据id删除冻结记录。 int count = freezeMapper.deleteById(xid); return count == 1; } @Override public boolean cancel(BusinessActionContext ctx) { // 查询冻结记录 String xid = ctx.getXid(); AccountFreeze freeze = freezeMapper.selectById(xid); // 1.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); // 2.清零冻结金额,改为CANCEL的状态 freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; }}
4.4.SAGA模式Saga 模式是 Seata 蚂蚁金服将为即将开源的长期事务解决方案做出主要贡献。
其理论基础是Hector & Kenneth Sagas于1987年发表。
Seata官网Saga指南:https://seata.io/zh-cn/docs/user/saga.html
4.4.1.原理在 Saga 在模式下,分布式事务中有多个参与者,每个参与者都是一个积极的补偿服务,用户需要根据业务场景实现其积极操作和反向回滚操作。
在分布式事务的实施过程中,每个参与者依次进行正向操作。如果所有正向操作都成功实施,则提交分布式事务。如果任何正向操作失败,分布式事务将返回执行前参与者的反向回滚操作。已提交的参与者将分布式事务返回初始状态。
Saga也分为两个阶段:
- 第一阶段:直接提交当地事务
- 第二阶段:成功是什么都不做;失败是通过编写补偿业务来回滚动的
优点:
- 事务参与者可以根据事件驱动实现异步调用,吞吐量高
- 一阶段直接提交事务,无锁,性能好
- 无需在TCC中编写三个阶段,即可实现简单
缺点:
- 软状态持续时间不确定,时效性差
- 没有锁,没有事务隔离,会有脏写
从以下几个方面对四个实现进行比较:
- 一致性:能保证事务的一致性吗?强一致性还是最终一致性?
- 隔离:事务之间的隔离是什么?
- 代码入侵:是否需要对业务代码进行改造?
- 性能:是否有性能损失?
- 场景:常见的商业场景
如图:
5.高可用作为分布式事务的核心,Seata的TC服务必须保证集群的高可用性。
5.1.高可用架构模型建立TC服务集群非常简单,可以启动多个TC服务,注册到nacos。
但是,集群不能保证100%的安全。如果集群所在机房出现故障怎么办?因此,如果要求较高,一般会在不同的地方做多个机房容灾。
比如上海一个TC集群,杭州另一个TC集群:
基于事务组的微服务(tx-service-group)与TC集群的映射关系,找出目前应该使用哪个TC集群。当SH集群出现故障时,只需将Vgroup-maping中的映射关系更改为HZ。所有微服务都将切换到HZ的TC集群。
5.2.实现高可用具体实现请参考:链接地址
第三章节:
包括高质量开源项目在内的博主个人开源博客地址: https://www.chengke.net