参考资料:
《Spring Microservices in Action》
《Spring Cloud Alibaba 微服务原理与实战》
《B站 尚硅谷 SpringCloud 框架开发教程 周阳》
《Seata 中文官网》
《Seata GitHub 官网》
《Seata 官方示例》
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务;它提供了 AT、TCC、Saga 和 XA 事务模式,为开发者提供了一站式的分布式事务解决方案;
XID;XID 对应全局事务的管辖;XID 的全局提交或回滚决议;XID 下管辖的全部分支事务完成提交或回滚请求;
${SEATA_HOME}\bin\sessionStore\root.data 中,性能较高(file 类型不支持注册中心的动态发现和动态配置功能);globaltable、branchtable、lock_table;vgroup_mapping,它表示事务分组映射,是 Seata 的资源逻辑,类似于服务实例,它的主要作用是根据分组来获取 Seata Serve r的服务实例;txServiceGroup 参数,这个参数有如下几种赋值方式:${spring.application.name}-seata-service-group;spring cloudalibaba.seata.tx-service-group 赋值;seata.tx-service-group 赋值;txServiceGroup 去指定位置(file.conf 或者远程配置中心)查找 service.vgroup_mapping.${txServiceGroup} 对应的配置值,该值代表TC集群(Seata Server)的名称;clusterName.grouplist;
Seata 安装的是 AT 模型中的 TC,为方便理解这里称为服务端;
Seata 作为一个事务中间件,有很多种部署安装方式,有安装包部署、源码部署和 Docker 部署,这里介绍前两种。版本选 1.4.2;

${SEATA_HOME}\conf\file.conf 文件,store.mode="db"。如下图所示:
${SEATA_HOME}\conf\file.conf 文件里的 db 模块为自己需要连接的 MySQL 地址;
${SEATA_HOME}\script/server/db/mysql.sql 文件;-- 判断数据库存在,存在再删除DROP DATABASE IF EXISTS seata; -- 创建数据库,判断不存在,再创建CREATE DATABASE IF NOT EXISTS seata;-- 使用数据库USE seata;-- the table to store GlobalSession dataCREATE 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_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`)) ENGINE = InnoDB DEFAULT CHARSET = utf8;-- the table to store BranchSession dataCREATE 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 = utf8;-- the table to store lock dataCREATE 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), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`)) ENGINE = InnoDB DEFAULT CHARSET = utf8;${SEATA_HOME}\conf\registry.conf 文件里的 registry.type,以及下面的注册中心地址信息;

${SEATA_HOME}/script/config-center/config.txt 文件里,该文件只在源码包里有,笔者是源码安装 Seata 时做的这步;${SEATA_HOME}\script\config-center\nacos 目录下执行以下 nacos-config.sh 脚本即可;
先启动 Nacos,再执行 ${SEATA_HOME}\bin\seata-server.bat 文件;
启动成功后能在 Nacos 服务器里能看见 Seata 服务;


mvm install 将项目安装到本地;Server.run() 方法即可;

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency>Seata 在 1.0 后支持将 ${SEATA_HOME}/script/client/conf 目录下的两个配置文件 file.conf 和 registry.conf 写进 .yml 格式文件里了(1.0 版本前不支持);
.yml 格式的配置文件在 ${SEATA_HOME}script/client/spring 目录下;
需要修改 seata.tx-service-group 和 seata.service.vgroup-mapping 一致,配置中心、注册中心等;
另一种配置方法:
Seata 通过代理数据源的方式实现分支事务;MyBatis 和 JPA 都需要注入 io.seata.rm.datasource.DataSourceProxy, 不同的是,MyBatis 还需要额外注入 org.apache.ibatis.session.SqlSessionFactory;
MyBatis:
@Configurationpublic class DataSourceProxyConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); return sqlSessionFactoryBean.getObject(); }}CREATE TABLE `undo_log`( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `branch_id` BIGINT(20) NOT NULL, `xid` VARCHAR(100) NOT NULL, `context` VARCHAR(128) NOT NULL, `rollback_info` LONGBLOB NOT NULL, `log_status` INT(11) NOT NULL, `log_created` DATETIME NOT NULL, `log_modified` DATETIME NOT NULL, `ext` VARCHAR(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8@GlobalTransactional 开启全局事务,Seata 会将事务的 xid 通过拦截器添加到调用其他服务的请求中,实现分布式事务;在业务流程中执行库存扣减操作的数据库操作时,Seata 会基于数据源代理对原执行的 SQL 进行解析(Seata 在 0.9.0 版本之后支持自动代理);
然后将业务数据在更新前后保存到 undo_log 日志表中,利用本地事务的 ACID 特性,把业务数据的更新和回滚日志写入同一个本地事务中进行提交;
tbl_repo 表中主键值等于 1 的记录的全局锁;UNDO_LOG 一并提交;AT 模式和 XA 最大的不同点:分支的本地事务可以在第一阶段提交完成后马上释放本地事务锁定的资源;AT 模式降低了锁的范围,从而提升了分布式事务的处理效率;
UNDO_LOG 日志即可。这也是和 XA 最大的不同点;
UNDO_LOG 日志;UNDO_LOG 中记录的数据镜像进行补偿;
UNDO_LOG 记录;UNDO_LOG 中的 afterImage 镜像数据与当前业务表中的数据进行比较,如果不同,说明数据被当前全局事务之外的动作做了修改,那么事务将不会回滚;UNDO_LOG中的 beforelmage 镜像数据和业务 SQL 的相关信息生成回滚语句并执行;一阶段本地事务提交前,需要确保先拿到全局锁;
拿不到全局锁 ,不能提交本地事务。
拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁;
举例:


因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题;
SELECT FOR UPDATE 语句的代理;
SELECT FOR UPDATE 语句的执行会申请全局锁 ,如果全局锁被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试;