目录
1.事务的四大特性。
2.分布式服务的事务问题。
3.seata。
3.1理论基础。
3.1.1CAP定理。
3.1.2BASE理论。
3.2初识Seata。
3.2.1Seata的架构。
3.2.2部署TC服务。
3.2.3微服务集成Seata。
3.3 seata提供的四种分布式事务解决方案。
3.3.1 XA模式。
3.3.1.1 XA模式原理。
3.3.1.2 实现XA模式。
3.3.2 AT模式。
3.3.2.1 AT模式原理。
3.3.2.2 AT模式的脏写问题 、AT模式的写隔离。
3.3.2.3 实现AT模式。
3.3.3 TCC模式。
3.3.3.1 TCC模式原理。
3.3.3.2 TCC模式实现。
3.3.4 SAGA模式。
3.4 seata的四种模式对比。
3.5高可用。
ACID 是指数据库事务的四个特性,每个字母代表一个特性:
原子性(Atomicity):原子性要求数据库中的事务是一个不可分割的操作单元,要么全部成功执行,要么全部回滚,不会停留在中间状态。如果一个事务中的某个操作失败,那么整个事务都会被回滚到初始状态,不会对数据库产生任何影响。
一致性(Consistency):一致性要求数据库在进行事务处理后,必须保持数据的一致性状态。这意味着数据库在事务开始和结束时,必须遵循预定义的规则和约束条件,以确保数据的完整性和正确性。(要么全部执行,要么全部不执行,从而保持数据的一致性)(要么全部成功,要么全部失败)
隔离性(Isolation):隔离性要求数据库中的每个事务都是相互隔离的,即每个事务对其他事务的操作应该是不可见的,以避免并发执行时的数据冲突和异常情况。数据库通过各种并发控制机制来实现隔离性,如锁机制、多版本并发控制(MVCC)等。(就是避免事务相互影响,比如两个事务以上对相同数据同时操作,或者其他事务还没成功提交就拿着用了)(事务在操作某些数据库记录时,把其他事务隔离在这些数据库记录外,事务之间不能相互影响。这样比较容易记)
持久性(Durability):持久性要求一旦事务提交成功,数据库中的数据就应该永久保存,即使在系统故障或重启后也能够保持数据的一致性状态。数据库通过将数据写入稳定的存储介质(如磁盘)来实现持久性。
隔离性讲解:如果没有隔离性,可能会出现以下情况:
脏读:如果没有隔离性,一个事务可能会读取到另一个未提交事务的数据,导致读取到不正确的数据,从而产生误导性的结果。(我只是随便写写,你居然当真了?)
不可重复读:缺乏隔离性可能导致同一个事务内多次读取同一数据时得到不一致的结果,影响事务的可靠性和一致性。(前脚读完,后脚就改了,数据还没给用户瞧,你就改了?)
幻读:在缺乏隔离性的情况下,一个事务在读取某个范围内的记录时,另一个事务在该范围内插入了新的记录或删除旧的记录,导致第一个事务再次读取该范围时出现了之前不存在的记录,破坏了数据的一致性。(趁我不注意,你不是把我作业藏起来,就是改成你名字,你是要我怀疑人生?)
数据完整性问题:缺乏隔离性可能导致事务之间相互干扰,从而破坏了数据库中数据的完整性和一致性,进而影响系统的可靠性和稳定性。(你改,我也改,大家一起改?)
C一致性:出现分区问题,访问独立分区节点的请求都被阻塞。
A可用性:出现分区问题,可以正常访问,该分区节点与其他分区节点的数据不一致性。
P分区容错性:在集群出现分区时,整个系统也要持续对外提供服务(如果不使用P,相当于访问所有分区节点都被阻塞)。
提醒:所以P在分布式系统中是必选的,A和C只能单选。
通过采用CP模型,Elasticsearch保证了数据的一致性和分区容错性。当有新的文档需要写入时,它会首先被写入主分片,然后主分片会将变更传播给对应的副本分片。只有当主分片和副本分片都确认写入成功后,写操作才会被认为是成功的,从而确保了数据的一致性。
P只要是分布式服务,使用了网络,就有可能网络故障出现分区的情况,P作用就是出现分区的情况下也能正常进行访问。
低可用性是因为当出现分区的时候,部分节点与其它节点失去连接,这时为了保证数据的一致性,访问该部分节点的请求都会被阻塞或拒绝。(如果是AP恰恰相反,可以访问,但是数据的一致性没了)
最终一致思想:不一致性、软状态(分别执行和提交)和最终一致的结合。
AP模式:不一致性(跟软状态很像) + 软状态 + 最终一致性。(允许暂时不一致性)
CP模式:强一致性 + 基本可用。(不允许暂时一致性)
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布 式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。
部署Seata的tc-server。
1.下载
首先我们要下载seata-server包,地址在http://seata.io/zh-cn/blog/download.html
2.解压
在非中文目录解压缩这个zip包,其目录结构如下:
3.修改配置
修改conf目录下的registry.conf文件:
内容如下:
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" # 改了,用哪个注册中心就写那个的名字,然后只需要修改下面对应注册中心的参数就行 nacos { application = "seata-tc-server" # 改了 serverAddr = "127.0.0.1:80" # 改了 group = "DEFAULT_GROUP" # 改了 namespace = "" cluster = "SH" # 改了,SH代表上海,集群的名称 username = "nacos" # 改了 password = "nacos" # 改了 } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = 0 password = "" cluster = "default" timeout = 0 } zk { cluster = "default" serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { cluster = "default" serverAddr = "127.0.0.1:8500" aclToken = "" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "nacos" nacos { serverAddr = "127.0.0.1:80" # 改了 namespace = "" group = "SEATA_GROUP" username = "nacos" # 改了 password = "nacos" # 改了 dataId = "seataServer.properties" } consul { serverAddr = "127.0.0.1:8500" aclToken = "" } apollo { appId = "seata-server" ## apolloConfigService will cover apolloMeta apolloMeta = "http://192.168.1.204:8801" apolloConfigService = "http://192.168.1.204:8080" namespace = "application" apolloAccesskeySecret = "" cluster = "seata" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" nodePath = "/seata/seata.properties" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
4.在nacos添加配置
特别注意,为了让tc服务的集群可以共享配置,我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好。
格式如下:
配置内容如下:
# 数据存储方式,db代表数据库 store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.cj.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true store.db.user=root store.db.password=123 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 # 事务、日志等配置 server.recovery.committingRetryPeriod=1000 server.recovery.asynCommittingRetryPeriod=1000 server.recovery.rollbackingRetryPeriod=1000 server.recovery.timeoutRetryPeriod=1000 server.maxCommitRetryTimeout=-1 server.maxRollbackRetryTimeout=-1 server.rollbackRetryTimeoutUnlockEnable=false server.undo.logSaveDays=7 server.undo.logDeletePeriod=86400000 # 客户端与服务端传输方式 transport.serialization=seata transport.compressor=none # 关闭metrics功能,提高性能 metrics.enabled=false metrics.registryType=compact metrics.exporterList=prometheus metrics.exporterPrometheusPort=9898
==其中的数据库地址、用户名、密码都需要修改成你自己的数据库信息。==
创建好如下:这里有两个配置文件,第一个是之前sentinel时使用的。第二个就是刚才创建的。
5.创建数据库表
特别注意:tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。
新建一个名为seata的数据库,运行课前资料提供的sql文件:
这些表主要记录全局事务、分支事务、全局锁信息:
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- 分支事务表 -- ---------------------------- DROP TABLE IF EXISTS `branch_table`; CREATE TABLE `branch_table` ( `branch_id` bigint(20) NOT NULL, `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `transaction_id` bigint(20) NULL DEFAULT NULL, `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `status` tinyint(4) NULL DEFAULT NULL, `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `gmt_create` datetime(6) NULL DEFAULT NULL, `gmt_modified` datetime(6) NULL DEFAULT NULL, PRIMARY KEY (`branch_id`) USING BTREE, INDEX `idx_xid`(`xid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- 全局事务表 -- ---------------------------- DROP TABLE IF EXISTS `global_table`; CREATE TABLE `global_table` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `transaction_id` bigint(20) NULL DEFAULT NULL, `status` tinyint(4) NOT NULL, `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `timeout` int(11) NULL DEFAULT NULL, `begin_time` bigint(20) NULL DEFAULT NULL, `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `gmt_create` datetime NULL DEFAULT NULL, `gmt_modified` datetime NULL DEFAULT NULL, PRIMARY KEY (`xid`) USING BTREE, INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE, INDEX `idx_transaction_id`(`transaction_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;
6.启动TC服务
启动TC服务:
进入bin目录,运行其中的seata-server.bat即可:
打开浏览器,访问nacos地址:http://localhost:8848,我配的端口是80,默认的是8848,然后进入服务列表页面,可以看到seata-tc-server的信息:
出现异常解决方法:
解决方法是:打开seata-server.bat文件添加java的jdk8路径。
找到修改前的这一行:
修改后结果:
然后重新启动就能成功了。
启动成功后,seata-server应该已经注册到nacos注册中心了。
添加seata坐标集成微服务出现报错:
添加坐标后,一启动服务就报错,启动失败。
解决方法是添加虚拟机选项:
--add-opens java.base/java.lang=ALL-UNNAMED
微服务集成seata分布式事务:
seata: registry: type: nacos nacos: server-addr: 127.0.0.1:80 namespace: "" # 什么都不写就是public group: DEFAULT_GROUP application: seata-tc-server username: nacos password: nacos tx-service-group: seata-demo # 事务组名称 service: vgroup-mapping: # 事务组与cluster集群的映射关系 seata-demo: SH
Seata提供了四种不同的分布式事务解决方案:
• XA 模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入• TCC 模式:最终一致的分阶段事务模式,有业务侵入• AT 模式:最终一致的分阶段事务模式,无业务侵入,也是 Seata 的默认模式• SAGA 模式:长事务模式,有业务侵入
seata的RM也仅仅是在数据库的接口基础上做了一层简单的封装,实际上seata的RM它也是调用数据库的RM实现。(主流数据库基本都实现了RM)。这种方法依赖数据库底层的实现,如果数据库没有实现XA模式,那么就用不了。
XA模式原理
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
seata的XA模式:
seata的XA模式做了一些调整,但大体相似:
RM一阶段的工作:
① 注册分支事务到 TC② 执行分支业务 sql 但不提交③ 报告执行状态到 TCTC二阶段的工作:
• TC 检测各分支事务执行状态a. 如果都成功,通知所有 RM 提交事务b. 如果有失败,通知所有 RM 回滚事务RM二阶段的工作:
• 接收 TC 指令,提交或回滚事务总结:
XA模式的优点是什么?
• 事务的强一致性,满足 ACID 原则。• 常用数据库都支持,实现简单,并且没有代码侵入XA模式的缺点是什么?
• 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差• 依赖关系型数据库实现事务
实现XA模式
Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:
1. 修改application.yml文件(每个参与事务的微服务),开启XA模式:
seata: data-source-proxy-mode: XA # 开启数据源代理的XA模式
2. 给发起全局事务的入口方法添加@GlobalTransactional注解,本例中是OrderServiceImpl中的create方法:
@Override @GlobalTransactional public Long create(Order order) { // 创建订单 orderMapper.insert(order); // 扣余额 ...略 // 扣减库存 ...略 return order.getId(); }
3. 重启服务并测试
访问的时候出现该异常解决方法:
解决方法:
把数据库驱动类从8.0.27换成8.0.11就能成功启动。指定的url还要加上时区。
url: jdbc:mysql://localhost:3306/seata_demo?serverTimezone=UTC
8.0.11 mysql mysql-connector-java ${mysql.version}
AT模式原理:
总结:
简述AT模式与XA模式最大的区别是什么?
• XA 模式一阶段不提交事务,锁定资源; AT 模式一阶段直接提交,不锁定资源。• XA 模式依赖数据库机制实现回滚; AT 模式利用数据快照实现数据回滚。• XA 模式强一致; AT 模式最终一致
AT模式的脏写问题
AT模式的写隔离(seata管理的全局事务):
全局锁只对seata管理的全局事务有用,非seata管理的事务不需要全局锁就能直接操作。
AT模式的写隔离(非seata管理的全局事务):
全局锁只对seata管理的全局事务有用,非seata管理的事务不需要全局锁就能直接操作。
总结:
AT模式的优点:
• 一阶段完成直接提交事务,释放数据库资源,性能比较好• 利用全局锁实现读写隔离• 没有代码侵入,框架自动完成回滚和提交AT模式的缺点:
• 两阶段之间属于软状态,属于最终一致• 框架的快照功能会影响性能,但比 XA 模式要好很多
实现AT模式:
要注意这里的两个表可能不在同一个库中。
下面是seata-at.sql文件里面的两个表,注意两个表可能不在同一个库中。
-- ---------------------------- -- 这一张表是放在TC服务使用的库中,在之前我们在nacos配置的TC数据库配置指定了使用seata库,所以放在seata库 -- ---------------------------- DROP TABLE IF EXISTS `lock_table`; CREATE TABLE `lock_table` ( `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `transaction_id` bigint(20) NULL DEFAULT NULL, `branch_id` bigint(20) NOT NULL, `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `gmt_create` datetime NULL DEFAULT NULL, `gmt_modified` datetime NULL DEFAULT NULL, PRIMARY KEY (`row_key`) USING BTREE, INDEX `idx_branch_id`(`branch_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- 这一张表是放在集成了seata的微服务的配置文件中指定了的数据库的库,集成的seata的微服务我都是使用seata-demo库,所以就放在这个库上。 -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `undo_log` ( `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id', `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id', `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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', UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
TCC模式原理:
总结:
TCC模式的每个阶段是做什么的?
• Try :资源检查和预留• Confirm :业务执行和提交• Cancel :预留资源的释放TCC的优点是什么?
• 一阶段完成直接提交事务,释放数据库资源,性能好• 相比 AT 模型,无需生成快照,无需使用全局锁,性能最强• 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库TCC的缺点是什么?
• 有代码侵入,需要人为编写 try 、 Confirm 和 Cancel 接口,太麻烦• 软状态,事务是最终一致• 需要考虑 Confirm 和Cancel的失败情况,做好幂等处理(幂等性是指无论调用多少次,对系统的状态都只有一次改变。在 TCC 模式中,如果参与者因为各种原因执行了多次,那么整个事务的状态就会变得不确定,因此需要确保每个参与者操作具有幂等性。
例如,在一个转账事务中,如果在“try”阶段时发生了错误,导致“try”被多次调用,那么在“confirm”或“cancel”阶段也可能会被多次调用。如果这些操作不具有幂等性,就会出现重复的转账操作,导致数据不一致。
)
TCC的空回滚和业务悬挂:
空回滚:就是调用某分支事务的时候因为阻塞导致失败,然后TC通知所有分支事务做回滚,但是失败分支事务都没有执行try(没有预留资源),这时就需要做空回滚。(如果让它执行cancel方法会执行失败,然后继续尝试执行cancel)。
业务悬挂:已经空回滚的业务,继续执行try,就永远不可能confirm或cancel,这就是业务悬挂。,因为该全局事务已经结束了我们这时要阻止它执行。
TCC实现案例:
(@BusinessActionContextParameter(paramName = "param") 注解标记的参数会放到BusinessActionContext上下文对象中,到时候通过该上下文对象可以获取该参数。
根据上面的理解分析来动手实现TCC模式:
1.导入account_freeze_tbl数据库表,放在微服务使用的库中。
DROP TABLE IF EXISTS `account_freeze_tbl`; CREATE TABLE `account_freeze_tbl` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `freeze_money` int(11) UNSIGNED NULL DEFAULT 0, `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel', PRIMARY KEY (`xid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
2.定义account_freeze_tbl表的实体类。
@Data @TableName("account_freeze_tbl") public class AccountFreeze { @TableId(type = IdType.INPUT) private String xid; private String userId; private Integer freezeMoney; private Integer state; public static abstract class State { public final static int TRY = 0; public final static int CONFIRM = 1; public final static int CANCEL = 2; } }
3.定义操作account_freeze_tbl表的mybatis-plus的接口类。
@Mapper public interface AccountFreezeMapper extends BaseMapper
{ } 4.定义TCC两个阶段的三个接口方法。
@LocalTCC public interface AccountTCCService { @TwoPhaseBusinessAction(name = "deduct",commitMethod = "fonfirm",rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money") int money); boolean confirm(BusinessActionContext context); boolean cancel(BusinessActionContext context); }
我们要做的业务操作接口:
@Mapper public interface AccountMapper extends BaseMapper
{ @Update("update account_tbl set money = money - ${money} where user_id = #{userId}") int deduct(@Param("userId") String userId, @Param("money") int money); @Update("update account_tbl set money = money + ${money} where user_id = #{userId}") int refund(@Param("userId") String userId, @Param("money") int money); } 5.实现TCC两个阶段的三个方法。
由于
confirm
和cancel
方法的执行不会对数据进行修改,因此不需要使用@Transactional
注解。这些方法通常只需要保证幂等性即可,确保多次调用不会产生副作用。@Service @Slf4j public 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.判断freeze中是否有冻结记录,如果有,一定是cancel执行过,我要拒绝业务。 AccountFreeze oldFreeze = freezeMapper.selectById(xid); if (oldFreeze != null){ //cancel执行过,我要拒绝业务 return; } //1.扣减可用余额(数据库中的字段使用unsigned关键词修饰,如果为负会报错,然后回滚,所以可以省略余额判断) 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 context) { //1.获取事务id String xid = context.getXid(); //2.根据id删除冻结及记录 int count = freezeMapper.deleteById(xid); return count == 0; } @Override public boolean cancel(BusinessActionContext context) { //注意:这里要保留记录,做空回滚和业务悬挂判断时需要用。 String xid = context.getXid(); String userId = context.getActionContext("userId").toString(); //0.查询冻结记录 AccountFreeze freeze = freezeMapper.selectById(xid); //1.空回滚的判断,判断freeze是否为null,为null证明try没执行,需要空回滚 if (freeze == null){ //证明try没执行,需要空回滚 freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); freeze.setXid(xid); freezeMapper.insert(freeze); return true; } //2.幂等判断 if (freeze.getState() == AccountFreeze.State.CANCEL){ //已经处理过一次CANCEL了,无需重复处理 return true; } //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; } }
6.修改控制类。
@RestController @RequestMapping("account") public class AccountController { @Autowired private AccountTCCService accountTCCService;//原本是直接使用的AccountService接口的方法 @PutMapping("/{userId}/{money}") public ResponseEntity
deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money){ accountTCCService.deduct(userId, money); return ResponseEntity.noContent().build(); } } 业务执行失败,回滚后的记录。
TC服务的高可用和异地容灾(异地容灾就是把微服务部署到不同机房。)
1.模拟异地容灾的TC集群
计划启动两台seata的tc服务节点:
节点名称 ip地址 端口号 集群名称 seata 127.0.0.1 8091 SH seata2 127.0.0.1 8092 HZ 之前我们已经启动了一台seata服务,端口是8091,集群名为SH。
现在,将seata目录复制一份,起名为seata2
修改seata2/conf/registry.conf内容如下:
registry { # tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等 type = "nacos" nacos { # seata tc 服务注册到 nacos的服务名称,可以自定义 application = "seata-tc-server" serverAddr = "127.0.0.1:8848" group = "DEFAULT_GROUP" namespace = "" cluster = "HZ" username = "nacos" password = "nacos" } } config { # 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置 type = "nacos" # 配置nacos地址等信息 nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } }
进入seata2/bin目录,然后运行命令:
seata-server.bat -p 8092
打开nacos控制台,查看服务列表:
点进详情查看:
2.将事务组映射配置到nacos
接下来,我们需要将tx-service-group与cluster的映射关系都配置到nacos配置中心。
新建一个配置:
配置的内容如下:
# 事务组映射关系 service.vgroupMapping.seata-demo=SH service.enableDegrade=false service.disableGlobalTransaction=false # 与TC服务的通信配置 transport.type=TCP transport.server=NIO transport.heartbeat=true transport.enableClientBatchSendRequest=false transport.threadFactory.bossThreadPrefix=NettyBoss transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler transport.threadFactory.shareBossWorker=false transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector transport.threadFactory.clientSelectorThreadSize=1 transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread transport.threadFactory.bossThreadSize=1 transport.threadFactory.workerThreadSize=default transport.shutdown.wait=3 # RM配置 client.rm.asyncCommitBufferLimit=10000 client.rm.lock.retryInterval=10 client.rm.lock.retryTimes=30 client.rm.lock.retryPolicyBranchRollbackOnConflict=true client.rm.reportRetryCount=5 client.rm.tableMetaCheckEnable=false client.rm.tableMetaCheckerInterval=60000 client.rm.sqlParserType=druid client.rm.reportSuccessEnable=false client.rm.sagaBranchRegisterEnable=false # TM配置 client.tm.commitRetryCount=5 client.tm.rollbackRetryCount=5 client.tm.defaultGlobalTransactionTimeout=60000 client.tm.degradeCheck=false client.tm.degradeCheckAllowTimes=10 client.tm.degradeCheckPeriod=2000 # undo日志配置 client.undo.dataValidation=true client.undo.logSerialization=jackson client.undo.onlyCareUpdateColumns=true client.undo.logTable=undo_log client.undo.compress.enable=true client.undo.compress.type=zip client.undo.compress.threshold=64k client.log.exceptionRate=100
3.微服务读取nacos配置
接下来,需要修改每一个微服务的application.yml文件,让微服务读取nacos中的client.properties文件:
seata: config: type: nacos # 必须要学这个,不然会报错 nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos group: SEATA_GROUP data-id: client.properties
重启微服务,现在微服务到底是连接tc的SH集群,还是tc的HZ集群,都统一由nacos的client.properties来决定了。