目录
前置环境
本文中关于客户端用到的相关组件版本如下:
<java.version>17</java.version>
<spring.boot.version>3.2.4</spring.boot.version>
<spring.cloud.version>2023.0.1</spring.cloud.version>
<alibaba.cloud.version>2023.0.1.0</alibaba.cloud.version>
其他组件版本:
Seata版本:2.0
Nacos版本:2.3.2
Mysql版本:8.0.27
分布式事务
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
在分布式事务中,有全局事务和分支事务之分:
- 全局事务:整个分布式事务。
- 分支事务:分布式事务中包含的每个子系统中的事务。
基于CAP定理和BASE理论,衍生出两种解决分布式事务的思想:
- 最终一致:各分支事务各自提交,如果有不一致的情况,再想办法恢复数据。
- 强一致:各分支事务执行完不要提交,等待彼此结果,然后再统一提交或回滚。
Seata
Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式事务解决方案。
核心概念
Seata 分TC、TM和RM三个角色,TC作为服务端单独服务端部署,TM和RM作为客户端由业务系统集成。
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入,存在严重的资源独占和阻塞问题,对系统性能有较大影响。
- TCC模式:最终一致性的分阶段事务模式,有业务侵入,需要业务方在 try、confirm、cancel 三个阶段进行编程,实现资源的预留、确认提交和回滚。
- AT模式:Seata默认模式,最终一致性的分阶段事务模式,无业务侵入,一阶段直接提交事务,减少了事务的锁定时间,提高了性能。
- SAGA模式:长事务模式,有业务侵入,适合跨多个服务的事务处理。
部署TC服务
Seata TC 端需要一个地方存储事务的数据,因此需要配置存储模式,存储模式现有 file、db、redis、raft 四种,默认是 file 模式。
file 模式为单机模式,全局事务会话信息内存中读写并异步(默认)持久化本地文件 root.data,性能较高;db 模式为高可用模式,全局事务会话信息通过db共享,相应性能差些;redis 模式 Seata-Server 1.3 及以上版本支持,性能较高,存在事务信息丢失风险,需要提前配置合适当前场景的 redis 持久化配置;raft 模式是通过封装无法高可用的 file 模式,利用 Raft 算法实现多个TC之间数据的同步;
Seata 本质上也是一个服务,因此需要配置注册中心和配置中心,Seata 支持包括 nacos 的多种配置中心,具体可以查看官方文档。
本文的TC部署统一使用 Docker 进行部署,存储模式使用 db,数据库使用 mysql,注册中心和配置中心都使用 nacos,部署步骤如下:
-
首先启动一个临时容器,将 resources 目录文件拷出的来
docker run -d -p 8091:8091 -p 7091:7091 --name seata-serve seataio/seata-server:2.0.0 docker cp seata-serve:/seata-server/resources /User/seata/config
-
数据库执行以下命令见表,不同数据库的数据库表可以参考官方Github
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_status_gmt_modified` (`status`, `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- 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 = utf8mb4; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
-
在 nacos 中添加 Seata 的配置文件,表单中填写的信息要和后续步骤保持一致,详细的配置参数可以参考官方说明
配置内容如下,要修改的地方就是数据库那里,其他基本都是默认值,添加在配置中心方便后面修改:server: #二阶段提交重试超时时长 maxCommitRetryTimeout: -1 #二阶段回滚重试超时时长 maxRollbackRetryTimeout: -1 #二阶段回滚超时后是否释放锁 rollbackRetryTimeoutUnlockEnable: false #对于批量请求消息的并行处理开关 enableParallelRequestHandle: true #二阶段并行下发开关 enableParallelHandleBranch: false #防止 XA 分支事务悬挂的重试超时时间 xaerNotaRetryTimeout: 60000 recovery: #二阶段提交未完成状态全局事务重试提交线程间隔时间 committingRetryPeriod: 1000 #二阶段异步提交状态重试提交线程间隔时间 asynCommittingRetryPeriod: 1000 #二阶段回滚状态重试回滚线程间隔时间 rollbackingRetryPeriod: 1000 #超时状态检测重试线程间隔时间 timeoutRetryPeriod: 1000 undo: #undo 保留天数 logSaveDays: 7 #undo 清理线程间隔时间 logDeletePeriod: 86400000 session: #分支事务 Session 异步删除线程池队列大小 branchAsyncQueueSize: 5000 #分支事务 Session 异步删除开关 enableBranchAsyncRemove: false store: mode: db session: mode: db lock: mode: db db: datasource: druid dbType: mysql driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata_server?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true user: root password: 123456 minConn: 2 maxConn: 5 maxWait: 10000 globalTable: global_table branchTable: branch_table lockTable: lock_table distributedLockTable: distributed_lock queryLimit: 100 metrics: #是否启用 Metrics enabled: false #指标注册器类型 registryType: compact #指标结果 Measurement 数据输出器列表 exporterList: prometheus #prometheus 输出器 Client 端口号 exporterPrometheusPort: 9898 transport: #TC 二阶段下发请求超时时间 rpcTcRequestTimeout: 30000 #TC 批量发送回复消息开关 enableTcServerBatchSendResponse: false shutdown: #服务端 Netty 线程池关闭前等待服务下线时间 wait: 3 threadFactory: #Netty 通信模型 Boss group 线程数 bossThreadSize: 1
-
rm掉临时创建的容器,修改拷贝目录的 application.yml 文件,配置注册中心、配置中心等,配置内容如下:
server: port: 7091 spring: application: name: seata-server logging: config: classpath:logback-spring.xml file: path: ${log.home:${user.home}/logs/seata} console: user: username: seata password: seata seata: config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: public group: SEATA_GROUP username: nacos password: 123456 data-id: seataServer.yaml registry: type: nacos nacos: server-addr: 127.0.0.1:8848 application: seata-server namespace: public group: SEATA_GROUP username: nacos password: 123456 security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
-
执行如下命令重新创建容器,并做好映射路径设置
docker run --name seata-server \ -d \ -p 8091:8091 \ -p 7091:7091 \ -v /User/seata/config:/seata-server/resources \ -e SEATA_IP=192.168.12.128 \ --restart=always \ seataio/seata-server:2.0.0
-
使用 docker logs 命令查询容器日志,看到如下的信息就是部署成功了:
apm-skywalking not enabled JMX disabled Affected JVM parameters: -Dlog.home=/root/logs/seata -server -Dloader.path=/lib -Xmx2048m -Xms2048m -Xss640k -XX:SurvivorRatio=10 -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=1024m -XX:-OmitStackTraceInFastThrow -XX:-UseAdaptiveSizePolicy -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/logs/seata/java_heapdump.hprof -XX:+DisableExplicitGC -Xloggc:/root/logs/seata/seata_gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -Dio.netty.leakDetectionLevel=advanced -Dapp.name=seata-server -Dapp.pid=1 -Dapp.home=/ -Dbasedir=/ OpenJDK 64-Bit Server VM warning: Cannot open file /root/logs/seata/seata_gc.log due to No such file or directory ███████╗███████╗ █████╗ ████████╗ █████╗ ██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔══██╗ ███████╗█████╗ ███████║ ██║ ███████║ ╚════██║██╔══╝ ██╔══██║ ██║ ██╔══██║ ███████║███████╗██║ ██║ ██║ ██║ ██║ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ 09:41:41.969 INFO --- [ main] [ta.config.ConfigurationFactory] [ load] [] : load Configuration from :Spring Configuration 09:41:41.994 INFO --- [ main] [onfig.nacos.NacosConfiguration] [ getConfigProperties] [] : Nacos check auth with userName/password. 09:41:42.695 INFO --- [ main] [seata.server.ServerApplication] [ logStarting] [] : Starting ServerApplication using Java 1.8.0_342 on f0393d2f262c with PID 1 (/seata-server/classes started by root in /seata-server) 09:41:42.696 INFO --- [ main] [seata.server.ServerApplication] [ogStartupProfileInfo] [] : No active profile set, falling back to 1 default profile: "default" 09:41:44.571 INFO --- [ main] [mbedded.tomcat.TomcatWebServer] [ initialize] [] : Tomcat initialized with port(s): 7091 (http) 09:41:44.582 INFO --- [ main] [oyote.http11.Http11NioProtocol] [ log] [] : Initializing ProtocolHandler ["http-nio-7091"] 09:41:44.583 INFO --- [ main] [.catalina.core.StandardService] [ log] [] : Starting service [Tomcat] 09:41:44.583 INFO --- [ main] [e.catalina.core.StandardEngine] [ log] [] : Starting Servlet engine: [Apache Tomcat/9.0.82] 09:41:44.684 INFO --- [ main] [rBase.[Tomcat].[localhost].[/]] [ log] [] : Initializing Spring embedded WebApplicationContext 09:41:44.684 INFO --- [ main] [letWebServerApplicationContext] [ebApplicationContext] [] : Root WebApplicationContext: initialization completed in 1813 ms 09:41:45.288 INFO --- [ main] [vlet.WelcomePageHandlerMapping] [ <init>] [] : Adding welcome page: class path resource [static/index.html] 09:41:45.592 INFO --- [ main] [oyote.http11.Http11NioProtocol] [ log] [] : Starting ProtocolHandler ["http-nio-7091"] 09:41:45.627 INFO --- [ main] [mbedded.tomcat.TomcatWebServer] [ start] [] : Tomcat started on port(s): 7091 (http) with context path '' 09:41:45.637 INFO --- [ main] [seata.server.ServerApplication] [ logStarted] [] : Started ServerApplication in 4.945 seconds (JVM running for 6.258) 09:41:46.374 INFO --- [ main] [rver.lock.LockerManagerFactory] [ init] [] : use lock store mode: db 09:41:46.546 INFO --- [ main] [a.server.session.SessionHolder] [ init] [] : use session store mode: db 09:41:48.942 INFO --- [ main] [aba.druid.pool.DruidDataSource] [ init] [] : {dataSource-1} inited 09:41:49.186 INFO --- [ main] [rpc.netty.NettyServerBootstrap] [ start] [] : Server started, service listen port: 8091 09:41:49.412 INFO --- [ main] [io.seata.server.ServerRunner ] [ run] [] : you can visit seata console UI on http://127.0.0.1:7091. log path: /root/logs/seata. 09:41:49.412 INFO --- [ main] [io.seata.server.ServerRunner ] [ run] [] : seata server started in 3773 millSeconds
-
使用 IP + 7091端口访问控制台,可以看到以下界面:
微服务集成 Seata
参与分布式事务的每个微服务首先添加以下依赖:
<!--seata分布式事务-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--这里由于我项目中的其他组件依赖冲突的,所以要额外引入这个-->
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
<version>4.10.1</version>
</dependency>
项目中添加以下配置:
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: 123456
group: SEATA_GROUP
application: seata-server
# 事务组,根据这个获取TC服务的集群名称
tx-service-group: default
service:
vgroup-mapping:
#和tx-service-group对应
default: default
data-source-proxy-mode: XA
XA模式
XA模式是一种由 X/Open 组织提出的分布式事务处理协议,它定义了一组标准接口,允许不同的数据库系统、消息队列等资源管理器参与到同一个分布式事务中。XA模式通过两阶段提交协议来确保事务的原子性、持久性、一致性和隔离性。
在两阶段提交协议中,事务参与者被分为一个协调者(Transaction Coordinator, TC)和多个资源管理器(Resource Manager, RM)。协调者负责协调各个资源管理器之间的事务执行,而资源管理器则负责管理本地数据库或资源的事务操作。由于XA中,需要等待所有资源管理器执行完成才能提交,因此XA是一个强一致性的分布式事务解决方案。
由于XA模式是比较早期的分布式事务标准,几乎所有的主流数据库都实现了XA模式定义的规范。
工作流程
XA模式的事务处理过程主要分为两个阶段:准备阶段和提交/回滚阶段。
- 准备阶段:事务协调者(TC)向所有资源管理器(RM)发送预提交请求。资源管理器执行事务操作,并将事务结果保存在日志中(如 redo 日志或 undo 日志),以便在需要时进行提交或回滚。如果事务执行成功,资源管理器返回一个准备成功的响应给协调者;如果失败,则返回一个准备失败的响应。
- 提交/回滚阶段:如果所有资源管理器的准备阶段都成功,事务协调者向所有资源管理器发送提交请求。资源管理器接收到提交请求后,执行提交操作,并将事务结果写入到持久化存储中。如果有任何一个资源管理器的准备阶段失败或在提交阶段失败,协调者向所有资源管理器发送回滚请求。资源管理器接收到回滚请求后,执行回滚操作,并清理事务相关的日志和资源。
Seata 中的XA模式做了一些调整,主要是加入了TM这一个角色
优点
- 强一致性:XA模式能够确保多个数据库或资源管理器之间的事务同时提交或回滚,保证数据的强一致性。
- 广泛支持:主流的数据库系统都支持XA协议,使得XA模式具有很好的通用性和兼容性,Seata 只是在数据库的基础上封装了XA模式,实现比较简单。
- 无代码侵入性:在XA模式下,用户只需关注自己的业务逻辑,无需修改现有的数据库或中间件代码即可实现分布式事务。
缺点
- 性能开销:XA模式需要额外的通信开销和资源管理器之间的协调,可能会影响系统的性能和吞吐量。
- 单点故障:协调者是XA模式中的关键节点,如果协调者发生故障,整个分布式系统可能会处于不可用状态。
- 并发性能限制:XA模式对事务隔离性的要求较高,可能导致严重的锁竞争和并发性能下降。
使用
所有事务的参与者都需要添加这个配置
seata:
data-source-proxy-mode: XA
在事务的入口加上 @GlobalTransactional 注解,以下单接口为例:
@Override
@GlobalTransactional
public OrderTbl create(OrderTbl orderTbl) {
orderTblMapper.insert(orderTbl);
try {
accountTblClient.deduct(orderTbl.getUserId(), orderTbl.getMoney());
storageTblClient.deduct(orderTbl.getCommodityCode(), orderTbl.getCount());
} catch (FeignException e) {
log.error("下单失败:", e);
throw new CommonException(CommonCodeMsg.BUSINESS_ERROR);
}
return orderTbl;
}
AT模式
AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。
由于在AT模式中,分支事务执行完后会直接提交,所以这是一种最终一致性的分布式事务解决方案,AT模式也是 Seata 的默认模式。
工作流程
AT模式分为两个阶段:
- 一阶段:RM注册分支事务,将当前操作的数据记录到一个 undo-log 表中作为数据快照,执行业务 sql 并提交,最后报告事务状态。
- 二阶段:如果所有事务运行正常,则把 undo-log 里面的记录删除,如果需要回滚,则根据 undo-log 中保存的快照恢复数据。
读写隔离问题
和XA模式不同,AT模式中,分支事务运行完后会直接提交,而不需要等待TC的指令,因此性能会比XA模式高,但同时,会产生读写隔离的问题。
假设有一个全局事务,里面包含两个分支事务,分别是事务A和事务B,事务A先执行完成并提交,事务B还未执行完成,这时候,有另一个线程来操作事务A的表,并对事务A操作的数据进行了修改并提交,此时,事务B执行失败并向TC报告了执行的结果,TC决定让事务A和事务B根据 undo-log 的快照进行数据恢复,这时候,事务A就会将它后面线程操作结果给覆盖了。
为了解决这个问题,Seata 引入了一个全局锁的概念,全局锁就是由TC记录当前正在操作某行数据的事务,当一个事务尝试对一些数据进行操作,首先需要获取这些数据的全局锁,获取成功后才能修改并提交,提交完成后,该事务会释放DB级别的所,但是一直持有全局锁直到整个事务完成。通过这种机制,能保证一些数据同时只能被同一个事务上下文访问。
但是这样还是有问题,要满足上面的模型,事务1和事务2必须都是由 Seata 管理,如果事务1由 Seata 管理,事务2则是外部的时候,那么全局锁是无法限制事务2的。
为了解决这个问题,Seata 在记录快照时,会记录两个版本的快照,一个是更新前的,一个是更新后的,当事务执行失败需要进行数据恢复时,首先回去检查限制的值和快照中的是否相同,如果相同则允许恢复,否则 Seata 会发出告警,这时候需要由程序员介入处理。这一过程和乐观锁的CAS机制有点相似。
优点
- 一阶段直接提交,释放资源早,性能比XA高
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成提交和回滚
缺点
- 两阶段之间属于软状态
- 快照功能也会影响性能,但比XA模式好很多
使用
- 在微服务的数据库中创建 undo-log 表,其他版本或数据库的脚本请参考官方Github
CREATE TABLE IF NOT EXISTS `undo_log` ( `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', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table'; ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
- 修改微服务配置,使用AT模式
seata: data-source-proxy-mode: AT
- 在事务的入口加上 @GlobalTransactional 注解
TCC模式
TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,其分布式事务模型直接作用于服务层,不依赖底层数据库。TCC是一种最终一致性的分布式事务解决方案。
工作流程
TCC 是一种比较成熟的分布式事务解决方案,可用于解决跨数据库、跨服务业务操作的数据一致性问题;TCC 中 Try、Confirm、Cancel 3 个方法均由业务编码实现,故 TCC 可以被称为是服务化的资源管理器。
TCC 的 Try 操作作为一阶段,负责资源的检查和预留;Confirm 操作作为二阶段提交操作,执行真正的业务;Cancel 是二阶段回滚操作,执行预留资源的取消,使资源回到初始状态。以下以订单服务为例,介绍一下各个阶段:
- Try阶段:客户端调用订单服务的下订单接口,订单服务检查库存是否充足,并预留(如冻结)相应数量的库存。
- Confirm阶段(如果Try阶段成功):订单服务释放 Try 阶段预留的库存(即将冻结的库存减去实际购买的数量)。
- Cancel阶段(如果Try阶段失败或全局事务需要回滚):订单服务恢复Try阶段预留的库存(即将冻结的库存恢复到原状态)。
空回滚和业务悬挂
当某分支事务在 try 阶段阻塞,会导致全局事务超时触发二阶段 cancel,在未执行 try 操作时限先执行了 cancel操作,这时 cancel 不能做回滚,就是空回滚。
当发生空回滚时,分支事务已经结束,但是这时候 try 已经完成,由于事务结束了,就不会有 confirm 和 cancel了,导致业务一直被悬挂。
为了解决这个问题,try 和 cancel 需要互相判断对方是否已经被执行。
优点
- TCC性能比AT高
- 不依赖数据库,可用于非事务型的数据库
缺点
- TCC中的三个方法都需要编写,代码侵入性强
- 不是强一致
- confirm 和 cancel 又可能失败,需要做好幂等性处理
使用
TCC 模式不要求所有分支事务都用 TCC,如果没有需要冻结的资源,可以不用 TCC,一个分布式事务中分支事务的模式可以混用,比如订单服务,生成订单是一个 Insert 操作,不需要冻结,所以可以用 AT 模式,其他例如余额,库存这些需要冻结的,就可以用 TCC。
使用 TCC 模式,需要按照以下几个步骤:
- 编写 TCC 接口:Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。在业务接口中核心的注解是 @TwoPhaseBusinessAction,表示当前方法使用 TCC 模式管理事务提交,并标明了 Try,Confirm,Cancel 三个阶段。name属性,给当前事务注册了一个全局唯一的的 TCC bean name。
TCC 模式的三个执行阶段分别是:- Try 阶段,预定操作资源(Prepare) 这一阶段所以执行的方法便是被 @TwoPhaseBusinessAction 所修饰的方法。如示例代码中的 prepare 方法。
- Confirm 阶段,执行主要业务逻辑(Commit) 这一阶段使用 commitMethod 属性所指向的方法,来执行Confirm 的工作。
- Cancel 阶段,事务回滚(Rollback) 这一阶段使用 rollbackMethod 属性所指向的方法,来执行 Cancel 的工作。其次,可以在 TCC 模式下使用 BusinessActionContext 在事务上下文中传递查询参数。
@LocalTCC public interface TccActionOne { @TwoPhaseBusinessAction(name = "prepare", commitMethod = "commit", rollbackMethod = "rollback") public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a); public boolean commit(BusinessActionContext actionContext); public boolean rollback(BusinessActionContext actionContext); }
- 在定义好 TCC 接口之后,我们可以像 AT 模式一样,通过 @GlobalTransactional 开启一个分布式事务。
@GlobalTransactional public String doTransactionCommit(){ tccActionOne.prepare(null,"one"); tccActionTwo.prepare(null,"two"); }
Saga模式
Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
Sega 模式使用于业务流程长、业务流程多,参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口的情况。
优点
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
缺点
- 不保证隔离性