- 分布式事务问题
- 一、Seata简介与安装
- 二、订单/库存/账户业务数据库准备
- 三、订单/库存/账户业务微服务准备
- 四、测试
分布式事务问题
只要用到分布式,必然会提及分布式的事务。
在分布式之前,一切组件全都在一台机器上。
在使用分布式之后,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源。
业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
一句话:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
一、Seata简介与安装
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。 官网
1.1 相关术语
一个典型的分布式事务过程,可以用分布式处理过程的一ID+三组件模型来描述。
一ID(全局唯一的事务ID):Transaction ID XID,在这个事务ID下的所有事务会被统一控制
三组件:
- Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;(Server端,为单独服务器部署)
- Transaction Manager (TM):事务管理器,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
- Resource Manager (RM):资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
- Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成(微服务)。
1.2 典型的分布式控制事务流程
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;(也就是在多个TM,RM中传播)
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

1.3 Seata-Server的下载与配置
我这里下载了0.9.0版本跟1.4.2版本(配了半天没配好,后面再填坑),差别还是蛮大的。
1.3.1 修改file.conf文件
解压到指定目录并修改conf目录下的file.conf配置文件。
- 备份原始file.conf文件。
- 主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息。
- 修改file.conf文件
- service模块(1.4.2里面没有这个模块,需要自己加)
- store模块

## transaction log store, only used in seata-serverstore {## store mode: file、db、redismode = "db"## rsa decryption public keypublicKey = ""## file store propertyfile {## store location dirdir = "sessionStore"# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmaxBranchSessionSize = 16384# globe session size , if exceeded throws exceptionsmaxGlobalSessionSize = 512# file buffer size , if exceeded allocate new bufferfileWriteBufferCacheSize = 16384# when recover batch read sizesessionReloadReadSize = 100# async, syncflushDiskMode = async}## database store propertydb {## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.datasource = "druid"## mysql/oracle/postgresql/h2/oceanbase etc.dbType = "mysql"## mysql 5.xx## driverClassName = "com.mysql.jdbc.Driver"## mysql 8.xxdriverClassName = "com.mysql.cj.jdbc.Driver"## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param## url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf-8&useSSL=false&nullCatalogMeansCurrent=true&serverTimezone=UTC"user = "root"password = "10086"minConn = 5maxConn = 100globalTable = "global_table"branchTable = "branch_table"lockTable = "lock_table"queryLimit = 100maxWait = 5000}## redis store propertyredis {## redis mode: single、sentinelmode = "single"## single mode propertysingle {host = "127.0.0.1"port = "6379"}## sentinel mode propertysentinel {masterName = ""## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"sentinelHosts = ""}password = ""database = "0"minConn = 1maxConn = 10maxTotal = 100queryLimit = 100}}## 1.4.2 版本需要自己手动增加service模块service {#vgroup->rgroupvgroup_mapping.my_test_tx_group = "my_group"#only support single nodedefault.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disabledisable = false#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanentmax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"}
1.3.2 数据库中建库建表
数据库新建库seata,建表db_store.sql在\seata-server-0.9.0\seata\conf目录里面
1.3.3 修改seata-server-0.9.0\seata\conf目录下的registry.conf配置文件
1.3.4 启动nacos和seata
seata-server-0.9.0\seata\bin\seata-server.bat
启动失败,报错:
解决:0.9.0默认的mysql是5.1.30版本,将lib文件夹下mysql-connector-java-5.1.30.jar删除,替换成自己mysql版本的jar包,我的是mysql-connector-java-8.0.22.jar。
再次启动:
出现这些提示信息代表seata启动成功。
nacos中成功注册了seate:
**Seata 1.4.2版本填坑—-nacos作为seata的注册/配置中心
seata1.2.0 Seata1.4.0+nacos
Seata0.9.0版本不支持集群,生产环境下需要使用1.0.0以上版本。我们这里配置seata的最新版本1.4.2,下载后文件目录如下图所示:
0. 启动nacos
启动nacos,新建一个命名空间seata用于存放seata的配置信息。
注意这里的命名空间ID,后面会用到。这里不新建也可以,seata使用的是public。
我们使用nacos充当seata的注册中心和配置中心!
1. 修改配置文件
①进入conf文件夹,修改file.conf文件
1.4.2版本可以参考file.conf.example(server端)和conf文件夹下README-zh.md中的client端配置
总共需要修改的地方:我这里用seate_1_4_2数据库来对应seata1.4.2版本
最终完整代码file.conf:file.conf.txt
②修改conf\registry.conf文件


思考:这里我们把seata-server端的config设置为了nacos,那么是不是第一步的file.conf文件就不再需要了。因为直接从nacos读取配置?
2. 将配置导入到nacos
① 准备nacos-config.sh脚本
在conf文件夹下,需要有个nacos-config.sh文件,这个文件1.4.2版本没有。README-zh.md文件中访问config-center超链接(https://github.com/seata/seata/tree/develop/script/config-center),nacos文件夹下:
用这个可以直接下当前页的文件github-directory-downloader
nacos-config.sh
② config.txt准备及修改
在conf目录下还需要一个config.txt文件,1.4.2版本同样没有,还是去README里面的config-center超链接。
config.txt需要放在conf的上级目录下。
修改config.txt文件中的内容,主要是下面这几项:
改为使用db存储:
注意这里store.db.url中数据库的名字就是我们之后需要新建的数据库的名字。
相比于其他版本,1.4.2这里多了个distributedLockTable。
整个config.txt文件中,store.publicKey、store.redis.sentinel.masterName、store.redis.sentinel.sentinelHosts、store.redis.password四个属性默认都是空的
。所以后面在将config.txt文件中的配置注册到nacos的时候,会出现四个失败项。
③ 导入seata相应的配置项到Nacos
config.txt就是seata各种详细的配置,执行nacos-config.sh即可将这些配置导入到nacos。这样就不需要将file.conf和registry.conf放到我们的项目中了,需要什么配置就直接从nacos中读取。(这句话是参考博客https://blog.csdn.net/jixieguang/article/details/110621561,我觉不完全对,后面registry.conf里的配置项虽然不需要.conf文件配置,但是需要在yml或properties文件中配置,而file.conf可以直接在nacos中读取)
导入配置:
然后在git bash界面输入:
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t f0378218-b129-4fd8-839c-9bdfd010205b -u nacos -w nacos
注:h表示nacos的地址,p表示端口号,g表示配置的分组,t表示命名空间的ID,u跟w表示nacos的账户密码。如果没有设置命名空间,而且都是默认选项直接 sh nacos-config.sh -h localhost就行。
可以看到共98项,导入失败4项,就是上面没有值的那四项(不影响,如果用到直接在nacos里面新建配置即可)
可以看到,nacos的seata命名空间中已经导入了配置项。(seata命名空间是我自己创建的,可以按自己的需求创建,不创建默认的就是public。)
3. 数据库中建库建表
我们先创建数据库seata1_4_2(数据库要与config.txt中db设置那里对应),数据库的建表语句在README文件的server连接中:
然后执行mysql.sql(1.4.2多了个distributed_lock表,和一些插入语句):
-- -------------------------------- The script used when storeMode is 'db' ---------------------------------- 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 = InnoDBDEFAULT 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 = InnoDBDEFAULT 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 = InnoDBDEFAULT CHARSET = utf8;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 = InnoDBDEFAULT 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);
4. 启动seata
运行bin目录下的seata-server.bat。
出现下面字段表示seata启动成功。seata启动日志在C:\Users\admin\logs\seata文件夹下。
nacos中在seata命名空间内也成功注册,注意这里服务名对应的是registry.conf文件中nacos下面application的值。0.9.0版本好像设置不了这个,默认是serverAddr。
二、订单/库存/账户业务数据库准备
以下演示都需要先启动Nacos后启动Seata,保证两个都OK。Seata没启动报错no available server to connect。
2.1 分布式事务业务说明
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单—->扣库存—->减账户(余额)
2.2 创建业务数据库与表
1. 创建业务数据库
- seata_order:存储订单的数据库;
- seata_storage:存储库存的数据库;
- seata_account:存储账户信息的数据库。 ```sql CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
<a name="AjDc0"></a>### 2. 按照上述3库分别创建对应业务表seata_order库下建t_order表:```sqlCREATE TABLE seata_order.`t_order` (`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',`count` INT(11) DEFAULT NULL COMMENT '数量',`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',`status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结') ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
seata_storage库下建t_storage 表:
CREATE TABLE `seata_storage`.`t_storage` (`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',`total` INT(11) DEFAULT NULL COMMENT '总库存',`used` INT(11) DEFAULT NULL COMMENT '已用库存',`residue` INT(11) DEFAULT NULL COMMENT '剩余库存') ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)VALUES ('1', '1', '100', '0', '100');
seata_account库下建t_account 表:
CREATE TABLE `seata_account`.t_account (`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度') ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
3. 按照上述3库分别建对应的回滚日志表
订单-库存-账户3个库下都需要建各自的回滚日志表,\seata-server-0.9.0\seata\conf目录下的db_undo_log.sql;1.4.2版本的在README_ZH文件中的client:

注意:0.9版本跟1.4.2版本的undo_log表的属性有差别,1.4.2版本没有id跟ext这两个属性,个人觉得使用上没有影响
这里的话我还是按0.9.0版本来建表。
# 0.9.0 版本DROP TABLE IF EXISTS `undo_log`;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;# 1.4.2版本DROP TABLE IF EXISTS `undo_log`;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 = InnoDBAUTO_INCREMENT = 1DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
最终效果: 这里展示了1.4.2版本跟0.9版本,实际中用一个就行,别的版本一样。
三、订单/库存/账户业务微服务准备
业务需求:下订单->减库存->扣余额->改(订单)状态
版本对应关系——很重要
注意:由于seata0.9.0版本跟1.0之后的版本(支持yml、properties配置)区别巨大,这里使用0.9.0版本(跟视频一致),其版本对应关系见版本说明。(seata0.9.0 + nacos 1.1.4 + sentinel 1.7.0 + SpringCloud Alibaba 2.1.1RELEASE)前面用的各组件版本得对应上(头疼)
3.1 新建订单Order-Module——seata-order-service2001
(1) pom
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>jdk8cloud2021</artifactId><groupId>com.atguigu.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>seata-order-service2001</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><!-- 因为兼容版本问题,所以需要剔除它自带的seata的包 --><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><!-- 要跟我们安装SeaTa的一致! --><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--web-actuator--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--mysql-druid--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies></project>
(2) application.yml
这里配置的是我们自己微服务的数据源
server:port: 2001spring:application:name: seata-order-servicecloud:alibaba:seata:#自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应#vgroup_mapping.my_test_tx_group = "my_group"tx-service-group: my_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTCusername: rootpassword: 10086feign:hystrix:enabled: falselogging:level:io:seata: infomybatis:mapperLocations: classpath:mapper/*.xml
(3) file.conf
程序中依赖的是 seata-all,对应于 *.conf 文件,所以需要在resource新建.conf文件,高版本的支持yml、properties配置。这里仅仅是seata-order-service2001模块的file.conf(配置2001的分布式事务),seata软件那里配置的是总控file.conf。
注意修改这两处:
transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true#thread factory for nettythread-factory {boss-thread-prefix = "NettyBoss"worker-thread-prefix = "NettyServerNIOWorker"server-executor-thread-prefix = "NettyServerBizHandler"share-boss-worker = falseclient-selector-thread-prefix = "NettyClientSelector"client-selector-thread-size = 1client-worker-thread-prefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTboss-thread-size = 1#auto default pin or 8worker-thread-size = 8}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"}service {#修改自定义事务组名称,这里跟在seata里配置的不一样,这里只针对2001自己的事务,#而seata里针对的是整个分布式全局事务#这里要注意 vgroup_mapping. 后面的值,要跟seata-server安装时 conf 文件夹下file.conf service模块设定的#vgroup_mapping.my_test_tx_group = "my_group"vgroup_mapping.my_group = "default"default.grouplist = "127.0.0.1:8091"enableDegrade = falsedisable = falsemax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"disableGlobalTransaction = false}client {async.commit.buffer.limit = 10000lock {retry.internal = 10retry.times = 30}report.retry.count = 5tm.commit.retry.count = 1tm.rollback.retry.count = 1}## transaction log storestore {## store mode: file、dbmode = "db"## file storefile {dir = "sessionStore"# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmax-branch-session-size = 16384# globe session size , if exceeded throws exceptionsmax-global-session-size = 512# file buffer size , if exceeded allocate new bufferfile-write-buffer-cache-size = 16384# when recover batch read sizesession.reload.read_size = 100# async, syncflush-disk-mode = async}## database storedb {## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.datasource = "dbcp"## mysql/oracle/h2/oceanbase etc.db-type = "mysql"## 这里要注意mysql5跟mysql8不一样driver-class-name = "com.mysql.cj.jdbc.Driver"url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC"user = "root"password = "10086"min-conn = 1max-conn = 3global.table = "global_table"branch.table = "branch_table"lock-table = "lock_table"query-limit = 100}}lock {## the lock store mode: local、remotemode = "remote"local {## store locks in user's database}remote {## store locks in the seata's server}}recovery {#schedule committing retry period in millisecondscommitting-retry-period = 1000#schedule asyn committing retry period in millisecondsasyn-committing-retry-period = 1000#schedule rollbacking retry period in millisecondsrollbacking-retry-period = 1000#schedule timeout retry period in millisecondstimeout-retry-period = 1000}transaction {undo.data.validation = trueundo.log.serialization = "jackson"undo.log.save.days = 7#schedule delete expired undo_log in millisecondsundo.log.delete.period = 86400000undo.log.table = "undo_log"}## metrics settingsmetrics {enabled = falseregistry-type = "compact"# multi exporters use comma dividedexporter-list = "prometheus"exporter-prometheus-port = 9898}support {## springspring {# auto proxy the DataSource beandatasource.autoproxy = false}}
几个配置文件对应关系
(4) registry.conf
指明注册到nacos中:
registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos"nacos {serverAddr = "localhost:8848" # 如果nacos不在本机,就写服务器的IPnamespace = ""cluster = "default"}eureka {serviceUrl = "http://localhost:8761/eureka"application = "default"weight = "1"}redis {serverAddr = "localhost:6379"db = "0"}zk {cluster = "default"serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}consul {cluster = "default"serverAddr = "127.0.0.1:8500"}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、etcd3type = "file"nacos {serverAddr = "localhost"namespace = ""}consul {serverAddr = "127.0.0.1:8500"}apollo {app.id = "seata-server"apollo.meta = "http://192.168.1.204:8801"}zk {serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}etcd3 {serverAddr = "http://localhost:2379"}file {name = "file.conf"}}
(5) domain
domain 就是entity(pojo,bean),对应数据库的表,不同公司习惯不一样。
新建Order类与CommonResult类
Order
package com.atguigu.cloudalibaba.domain;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import java.math.BigDecimal;@Data@AllArgsConstructor@NoArgsConstructorpublic class Order {private Long id;private Long userId;private Long productId;private Integer count;private BigDecimal money;/*** 订单状态:0:创建中;1:已完结*/private Integer status;}
CommonResult
package com.atguigu.cloudalibaba.domain;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class CommonResult<T> {private Integer code;private String message;private T data;public CommonResult(Integer code, String message) {this(code, message, null);}}
(6) Dao接口及实现(SQL映射文件)
dao中至少要有两个方法,一个是创建订单,一个是修改订单状态
OrderDao
package com.atguigu.cloudalibaba.dao;import com.atguigu.cloudalibaba.domain.Order;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;/*** @author MrLinxi* @Description* @create 2021-11-10-22:24*/@Mapperpublic interface OrderDao {/*** 创建订单*/void create(Order order);/*** 修改订单状态,从0改为1*/void update(@Param("userId") Long userId, @Param("status") Integer status);}
OrderMapper.xml
resources文件夹下新建mapper文件夹后添加OrderMapper.xml。完成dao的具体实现。
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.atguigu.cloudalibaba.dao.OrderDao"><!--定义一个结果集和实体类的映射表--><resultMap id="BaseResultMap" type="com.atguigu.cloudalibaba.domain.Order"><id column="id" property="id" jdbcType="BIGINT"/><result column="user_id" property="userId" jdbcType="BIGINT"/><result column="product_id" property="productId" jdbcType="BIGINT"/><result column="count" property="count" jdbcType="INTEGER"/><result column="money" property="money" jdbcType="DECIMAL"/><result column="status" property="status" jdbcType="INTEGER"/></resultMap><insert id="create">INSERT INTO `t_order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`)VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0);</insert><update id="update">UPDATE `t_order`SET status = 1WHERE user_id = #{userId} AND status = #{status};</update></mapper>
(7) Service接口及实现
Order2001驱动自己,外加调用库存和账户:共3个service
OrderService
package com.atguigu.cloudalibaba.service;import com.atguigu.cloudalibaba.domain.Order;public interface OrderService {// 创建订单void create(Order order);}
StorageService
package com.atguigu.cloudalibaba.service;import com.atguigu.cloudalibaba.domain.CommonResult;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;//通过OpenFeign远程调用库存的微服务@FeignClient(value = "seata-storage-service")public interface StorageService {//扣减库存,比如买了5个1号商品:对1号商品库存减5@PostMapping(value = "/storage/decrease")CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);}
AccountService
package com.atguigu.cloudalibaba.service;import com.atguigu.cloudalibaba.domain.CommonResult;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;import java.math.BigDecimal;//通过OpenFeign远程调用账号微服务@FeignClient(value = "seata-account-service")public interface AccountService {//扣减账户余额,需要传入用户ID跟扣除的金额//@RequestMapping(value = "/account/decrease", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")@PostMapping("/account/decrease")CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);}
OrderServiceImpl
package com.atguigu.cloudalibaba.service.Impl;import com.atguigu.cloudalibaba.dao.OrderDao;import com.atguigu.cloudalibaba.domain.Order;import com.atguigu.cloudalibaba.service.AccountService;import com.atguigu.cloudalibaba.service.OrderService;import com.atguigu.cloudalibaba.service.StorageService;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service@Slf4jpublic class OrderServiceImpl implements OrderService {@Resourceprivate OrderDao orderDao;@Resourceprivate StorageService storageService;@Resourceprivate AccountService accountService;/*** 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态* 简单说:* 下订单->减库存->减余额->改状态*/@Overridepublic void create(Order order) {log.info("------->下单开始");//本应用创建订单orderDao.create(order);//远程调用库存服务扣减库存log.info("------->订单微服务调用库存微服务,扣减库存开始");storageService.decrease(order.getProductId(),order.getCount());log.info("------->订单微服务调用库存微服务,扣减库存结束");//远程调用账户服务扣减余额log.info("------->订单微服务调用账户微服务,扣减余额开始");accountService.decrease(order.getUserId(),order.getMoney());log.info("------->订单微服务调用账户微服务,减余额结束");//修改订单状态为已完成log.info("------->order-service中修改订单状态开始");// 这里的话是不是应该是orderId?orderDao.update(order.getUserId(),0);log.info("------->order-service中修改订单状态结束");log.info("------->下单结束");}}
(8) controller
package com.atguigu.cloudalibaba.controller;import com.atguigu.cloudalibaba.domain.CommonResult;import com.atguigu.cloudalibaba.domain.Order;import com.atguigu.cloudalibaba.service.OrderService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class OrderController {@Autowiredprivate OrderService orderService;//创建订单@GetMapping(value = "/order/create")public CommonResult create(Order order) {orderService.create(order);return new CommonResult(200, "订单创建成功!");}}
(9) config
MyBatisConfig
mybatis配置类,绑定实现文件OrderMapper.xml与Dao接口
package com.atguigu.cloudalibaba.config;import org.mybatis.spring.annotation.MapperScan;import org.springframework.context.annotation.Configuration;@Configuration@MapperScan({"com.atguigu.cloudalibaba.dao"})public class MyBatisConfig {}
DataSourceProxyConfig
DataSouce的包是sql下的,DataSourceProxy是seata下的,不要搞错了。
package com.atguigu.cloudalibaba.config;import com.alibaba.druid.pool.DruidDataSource;import io.seata.rm.datasource.DataSourceProxy;import org.apache.ibatis.session.SqlSessionFactory;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.transaction.SpringManagedTransactionFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import javax.sql.DataSource;//使用Seata对数据源进行代理@Configurationpublic class DataSourceProxyConfig {@Value("${mybatis.mapperLocations}")private String mapperLocations;@Bean@ConfigurationProperties(prefix = "spring.datasource")public DataSource druidDataSource(){return new DruidDataSource();}@Bean// @Primary//DataSourceProxy方法上标注@Primary就可以了,这样就自动用的代理的DataSource(DS的子类),// 而不是Druid的, 否则就需要自己构造sqlSessionFactorypublic DataSourceProxy dataSourceProxy(DataSource dataSource) {return new DataSourceProxy(dataSource);}@Beanpublic SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSourceProxy);sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());return sqlSessionFactoryBean.getObject();}}
(10) 主启动类
package com.atguigu.cloudalibaba;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@EnableFeignClients@EnableDiscoveryClient@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消数据源的自动创建public class SeataOrderMainApp2001 {public static void main(String[] args) {SpringApplication.run(SeataOrderMainApp2001.class, args);}}
启动测试
先启动nacos-1.1.4和seata-0.9.0,再启动2001。
2001启动成功,成功注册到nacos中

测试nacos-2.0.3和seata-0.9.0,再启动2001 也可以启动成功
3.2 新建库存Storage-Module——seata-storage-service2002
(1) pom
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>jdk8cloud2021</artifactId><groupId>com.atguigu.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>seata-storage-service2002</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><!-- 因为兼容版本问题,所以需要剔除它自带的seata的包 --><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><!--这里引入的版本要跟安装的版本对应--><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--web-actuator--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--mysql-druid--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies></project>
(2) application.yml
server:port: 2002spring:application:name: seata-storage-servicecloud:alibaba:seata:#自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应#vgroup_mapping.my_test_tx_group = "my_group"# tx-service-group: my_test_tx_grouptx-service-group: my_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTCusername: rootpassword: 10086#服务提供端不需要远程调用#feign:# hystrix:# enabled: falselogging:level:io:seata: infomybatis:# 扫描类路径下mapper文件夹下的.xml配置文件mapperLocations: classpath:mapper/*.xml
(3) file.conf & registry.conf
(4) domain
Storage
package com.atguigu.cloudalibaba.domian;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class Storage {private Long id;//产品IDprivate Long productId;//总库存private Integer total;//已用库存private Integer used;//剩余库存private Integer residue;}
CommonResult
package com.atguigu.cloudalibaba.domain;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class CommonResult<T> {private Integer code;private String message;private T data;public CommonResult(Integer code, String message) {this(code, message, null);}}
(5) Dao接口及实现(SQL映射文件)
StorageDao
package com.atguigu.cloudalibaba.dao;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;@Mapperpublic interface StorageDao {//扣减库存:根据产品ID扣除void decrease(@Param("productId") Long productId, @Param("count") Integer count);}
StorageMapper.xml
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.atguigu.cloudalibaba.dao.StorageDao"><resultMap id="storage" type="com.atguigu.cloudalibaba.domian.Storage"><id column="id" property="id" jdbcType="BIGINT"></id><result column="product_id" property="productId" jdbcType="BIGINT"></result><result column="total" property="total" jdbcType="BIGINT"></result><result column="used" property="used" jdbcType="INTEGER"></result><result column="residue" property="residue" jdbcType="INTEGER"></result></resultMap><update id="decrease">update `t_storage`SET `used` = `used` + #{count}, `residue` = `residue` - #{count}WHERE `product_id` = #{productId};</update></mapper>
(6) Service接口及实现
StorageService
package com.atguigu.cloudalibaba.service;public interface StorageService {/*** 扣减库存*/void decrease(Long productId, Integer count);}
StorageServiceImpl
package com.atguigu.cloudalibaba.service.Impl;import com.atguigu.cloudalibaba.dao.StorageDao;import com.atguigu.cloudalibaba.service.StorageService;import lombok.extern.slf4j.Slf4j;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service@Slf4jpublic class StorageServiceImpl implements StorageService {private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);@Autowiredprivate StorageDao storageDao;/*** 扣减库存* @param productId* @param count*/@Overridepublic void decrease(Long productId, Integer count) {// log.info("-------->storage-service中扣减库存");LOGGER.info("-------->storage-service中扣减库存");storageDao.decrease(productId, count);}}
(7) Controller
package com.atguigu.cloudalibaba.controller;import com.atguigu.cloudalibaba.domian.CommonResult;import com.atguigu.cloudalibaba.service.StorageService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class StorageController {@Autowiredprivate StorageService storageService;//RequestMapping默认GET POST请求都支持,根据前端自动适应@RequestMapping(value = "/storage/decrease")public CommonResult decrease(Long productId, Integer count) {storageService.decrease(productId, count);return new CommonResult(200, "扣减库存成功");}}
(8) config配置
(9) 主启动类
package com.atguigu.cloudalibaba;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@EnableDiscoveryClient@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)@EnableFeignClientspublic class SeataStorageMainApp2002 {public static void main(String[] args) {SpringApplication.run(SeataStorageMainApp2002.class, args);}}
启动测试
启动nacos、seata、2002;启动成功,注册进nacos。
3.3 新建账户Account-Module——seata-account-service2003
(1) pom
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>jdk8cloud2021</artifactId><groupId>com.atguigu.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>seata-account-service2003</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><!-- 因为兼容版本问题,所以需要剔除它自带的seata的包 --><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--web-actuator--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--mysql-druid--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies></project>
(2) application.yml
server:port: 2003spring:application:name: seata-account-servicecloud:alibaba:seata:#自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应#vgroup_mapping.my_test_tx_group = "my_group"# tx-service-group: my_test_tx_grouptx-service-group: my_group# tx-service-group: default# tx-service-group: my_test_tx_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTCusername: rootpassword: 10086#feign:# hystrix:# enabled: falselogging:level:io:seata: infomybatis:# 扫描类路径下mapper文件夹下的.xml配置文件mapperLocations: classpath:mapper/*.xml
(3) file.conf & registry.conf
(4) domain
Account
package com.atguigu.cloudalibaba.domain;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class Account {private Long id;//用户IDprivate Long userId;//总额度private Integer total;//已用额度private Integer used;//剩余额度private Integer residue;}
CommonResult
package com.atguigu.cloudalibaba.domain;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class CommonResult<T> {private Integer code;private String message;private T data;public CommonResult(Integer code, String message) {this(code, message, null);}}
(5) Dao接口及实现
AccountDao
package com.atguigu.cloudalibaba.dao;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import java.math.BigDecimal;@Mapperpublic interface AccountDao {/*** 扣减账户余额* @param userId* @param money*/void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);}
AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.atguigu.cloudalibaba.dao.AccountDao"><resultMap id="BaseResultMap" type="com.atguigu.cloudalibaba.domain.Account"><id column="id" property="id" jdbcType="BIGINT"></id><result column="user_id" property="userId" jdbcType="BIGINT"></result><result column="total" property="total" jdbcType="DECIMAL"></result><result column="used" property="used" jdbcType="DECIMAL"></result><result column="residue" property="residue" jdbcType="DECIMAL"></result></resultMap><update id="decrease">UPDATE t_accountSET `used` = `used` + #{money}, `residue` = `residue` - #{money}WHERE `user_id` = #{userId};</update></mapper>
(6) Service接口及实现
AccountService
package com.atguigu.cloudalibaba.service;import org.springframework.web.bind.annotation.RequestParam;import java.math.BigDecimal;public interface AccountService {/*** 扣减账户金额* @param userId 用户ID* @param money 金额*/void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);}
AccountServiceImpl
package com.atguigu.cloudalibaba.service.impl;import com.atguigu.cloudalibaba.dao.AccountDao;import com.atguigu.cloudalibaba.service.AccountService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.math.BigDecimal;@Servicepublic class AccountServiceImpl implements AccountService {private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);@ResourceAccountDao accountDao;/*** 扣减账户余额* @param userId 用户ID* @param money 金额*/@Overridepublic void decrease(Long userId, BigDecimal money) {LOGGER.info("------->account-service中扣减账户余额开始");//模拟超时异常,全局事务回滚//暂停几秒钟线程//try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }accountDao.decrease(userId, money);LOGGER.info("------->account-service中扣减账户余额结束");}}
(7) Controller
package com.atguigu.cloudalibaba.controller;import com.atguigu.cloudalibaba.domain.CommonResult;import com.atguigu.cloudalibaba.service.AccountService;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;import java.math.BigDecimal;@RestControllerpublic class AccountController {@Resourceprivate AccountService accountService;/*** 扣减账户余额*/@RequestMapping(value = "/account/decrease")public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {accountService.decrease(userId, money);return new CommonResult(200, "扣减账户余额成功!");}}
(8) config配置
(9) 主启动类
package com.atguigu.cloudalibaba;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@EnableDiscoveryClient@EnableFeignClients@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)public class SeataAccountMainApp2003 {public static void main(String[] args) {SpringApplication.run(SeataAccountMainApp2003.class, args);}}
启动测试
启动nacos、seata、2003;启动成功,成功注册进nacos
## 填坑高版本seata1.4.2client配置
seata1.2.0 Seata1.4.0+nacos
使用seata1.4.2 各个微服务整体上的代码是差不多的,区别的地方在于1.4.2支持yml、properties文件里配置client端的seata,不再需要file.conf/registry.conf文件。同时支持@EnableAutoDataSourceProxy注解开启数据源的自动代理(不需要手动配置数据源)
① 修改父工程版本控制
seata高版本各微服务可以直接通过yml、properties来配置seata,不需要在微服务中加入file.conf和registry.conf文件。

注意版本对应关系,这里要使用SpringCloud Hoxton.SR9+SpringCloud Alibaba 2.2.6.RELEASE+Spring Boot 2.3.2RELEASE+Nacos 1.4.2(我用的2.0.3)+Seata 1.3.0(我用的1.4.2)。
修改父工程的依赖版本,主要是让springboot、SpringCloud、SpringCloud alibaba版本对应。
<!--spring boot 2.3.2--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.3.2.RELEASE</version><type>pom</type><scope>import</scope></dependency><!--spring cloud Hoxton.SR9--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Hoxton.SR9</version><type>pom</type><scope>import</scope></dependency><!--spring cloud alibaba 2.2.6.RELEASE--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2.2.6.RELEASE</version><type>pom</type><scope>import</scope></dependency>
② 修改微服务模块seata依赖pom
官网上对seata依赖是这么描述的:
官网推荐依赖配置方式:
但是经过我测试发现,还是需要排除掉spring-cloud-starter-alibaba-seata里面的seata-all
<!--seata--><!--seata-spring-boot-starter 集成了seata-all,版本对应--><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.4.2</version></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><!-- 因为兼容版本问题,所以需要剔除它自带的seata的包 --><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion><exclusion><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></exclusion></exclusions></dependency>
实际上我有微服务只配置了seata-spring-boot-starter依赖,spring-cloud-starter-alibaba-seata没有配置,并不影响正常使用。
③ 修改微服务application.yml
直接将seata的相关配置,配置到application.yml文件中,几个微服务的yml类似:
# 这一块是配置seata的seata:enabled: trueapplication-id: ${spring.application.name}enable-auto-data-source-proxy: true #是否开启数据源自动代理,默认为truetx-service-group: my_test_tx_group #要与config.txt配置文件中的vgroupMapping一致registry: #registry根据seata服务端的registry配置type: nacos #默认为filenacos:application: seata-server #配置自己的seata服务,与registry.conf一致server-addr: 127.0.0.1:8848 #根据自己的seata服务配置username: nacos #根据自己的seata服务配置password: nacos #根据自己的seata服务配置namespace: f0378218-b129-4fd8-839c-9bdfd010205b #根据自己的seata服务配置cluster: default # 配置自己的seata服务cluster, 默认为 defaultgroup: "SEATA_GROUP" #根据自己的seata服务配置config:type: nacos #配置中心设置为nacos,直接从nacos上获取配置nacos:server-addr: 127.0.0.1:8848 #配置自己的nacos地址group: SEATA_GROUP #配置自己的group,这里我配置跟registry.conf一样username: nacos #配置自己的usernamepassword: nacos #配置自己的passwordnamespace: f0378218-b129-4fd8-839c-9bdfd010205b #根据自己的seata服务配置# dataId如果不用,就不需要配置# dataId: seataServer.properties #配置自己的dataId,由于搭建服务端时把客户端的配置也写在了seataServer.properties,所以这里用了和服务端一样的配置文件,实际客户端和服务端的配置文件分离出来更好# 这里是配置微服务的端口、注册到哪、数据源等等server:port: 2001spring:application:name: seata-order-servicecloud:# alibaba:# seata:# #自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应# #vgroup_mapping.my_test_tx_group = "my_group"# tx-service-group: my_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTCusername: rootpassword: 10086feign:hystrix:enabled: falselogging:level:io:seata: infomybatis:mapperLocations: classpath:mapper/*.xml
④ 修改主启动类和DataSourceProxyConfig类
主启动类
主启动类加上@EnableAutoDataSourceProxy注解,这里以storage的微服务为例:
package com.atguigu.cloudalibaba;import io.seata.spring.annotation.datasource.EnableAutoDataSourceProxy;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)@EnableDiscoveryClient@EnableFeignClients@EnableAutoDataSourceProxypublic class StorageMainApp2002 {public static void main(String[] args) {SpringApplication.run(StorageMainApp2002.class, args);}}
配置类
使用@EnableAutoDataSourceProxy注解后,不再需要DataSourceProxyConfig配置数据源代理。强行写会报错。如果需要自己配置数据源代理的话,在application.yml中设置seata.enable-auto-data-source-proxy为false,主启动类上去掉@EnableAutoDataSourceProxy注解即可。
参考博客
- Seata1.4.2+Nacos搭建使用
- Seata1.4.2整合SpringCloud H——Seata安装与搭建
- https://blog.csdn.net/ClearCiM/article/details/119953255
- spring cloud使用nacos和seata(windows环境)
- SEATA配合nacos使用
四、测试
Seata全局事务怎么使用
Spring提供的本地事务:@Transactional
Seata提供的全局事务:@GlobalTransactional
4.0 数据库初始情况
下订单->减库存->扣余额->改(订单)状态

4.1 测试正常下单
启动nacos、seata、2001、2002、2003;
测试:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100报错
java.sql.SQLException:Failed to fetch schema oft_order
Connector/J 5.0.0以后的版本有一个名为useInformationSchema的数据库连接参数,Connector/J 在mysql8.0中默认配置连接属性useInformationSchema为true,使查询table信息时更为有效。用户依然可以配置useInformationSchema为false,但是在8.0.3及其之后的版本中,由于不能支持早期的特性,某些数据字典的查询可能会失败。
useInformationSchema配置为false的时候,也可能会造成REMARKS信息(对应数据库中各字段的comment)的缺失。
在各微服务的application.yml 文件的spring.datasource.url 后面加上&useInformationSchema=false设置useInformationSchema为false,即可解决该问题。
参考:https://www.jianshu.com/p/acc99f891e91再次测试
访问成功

数据库情况:

4.2 测试超时异常:不加@GlobalTransactional
AccountServiceImpl添加超时:

我们使用的是Openfeign,默认超时时长是1s,这里我们延迟30s。报错超时异常:
数据库情况:



当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1;而且由于feign的重试机制,账户余额还有可能被多次扣减。4.3 测试超时异常:加@GlobalTransactional
OrderServiceImpl添加@GlobalTransactional注解,注意改注解只能用在方法上!
- name:给定全局事务实例的名称,随便取,唯一即可
- rollbackFor:当发生什么样的异常时,进行回滚
- noRollbackFor:发生什么样的异常不进行回滚。 ```java package com.atguigu.cloudalibaba.service.Impl;
import com.atguigu.cloudalibaba.dao.OrderDao; import com.atguigu.cloudalibaba.domain.Order; import com.atguigu.cloudalibaba.service.AccountService; import com.atguigu.cloudalibaba.service.OrderService; import com.atguigu.cloudalibaba.service.StorageService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao;
@Resourceprivate StorageService storageService;@Resourceprivate AccountService accountService;/*** 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态* 简单说:* 下订单->减库存->减余额->改状态*/@Override//全局事务,发生异常进行回滚@GlobalTransactional(name = "lsp-create-order", rollbackFor = Exception.class)public void create(Order order) {log.info("------->下单开始");//本应用创建订单orderDao.create(order);//远程调用库存服务扣减库存log.info("------->订单微服务调用库存微服务,扣减库存开始");storageService.decrease(order.getProductId(),order.getCount());log.info("------->订单微服务调用库存微服务,扣减库存结束");//远程调用账户服务扣减余额log.info("------->订单微服务调用账户微服务,扣减余额开始");accountService.decrease(order.getUserId(),order.getMoney());log.info("------->订单微服务调用账户微服务,减余额结束");//修改订单状态为已完成log.info("------->order-service中修改订单状态开始");// 这里的话是不是应该是orderId?orderDao.update(order.getUserId(),0);log.info("------->order-service中修改订单状态结束");log.info("------->下单结束");}
}
测试:<br />依然超时异常<br /><br />数据库:<br /><br /><br /><br />我们发现数据库中的数据根本就没有变化,记录都添加不进来,说明回滚成功!<a name="SkeWk"></a>## 4.4 小结做好配置后,我们只需要使用一个 @GlobalTransactional(name = "lsp-create-order", rollbackFor = Exception.class) 放在业务的入口,即可实现控制全局的事务。注意该注解只能放在方法上。<a name="GspoX"></a># 五、补充说明Seata:Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架<br />0.9不支持集群,生产环境请使用1.0以上的版本。<a name="H2b0X"></a>## 5.0 undo_log表的作用模块内方法也可以加@Transactional注解,如果一个模块的事务提交了,Seata会把提交了哪些数据记录到undo_log表中,如果这时TC通知全局事务回滚,那么RM就从undo_log表中获取之前修改了哪些资源,并根据这个表回滚。(有待考证)<a name="nFdla"></a>## 5.1 再看TC/TM/RM三大组件TC:seata服务器; (我们电脑上启动的seata )<br />TM:事物的发起者,业务的入口。 哪个微服务使用了**@GlobalTransactional**哪个就是TM<br />RM:事务的参与者,一个数据库就是一个RM。<br />分布式事务的执行流程:1. TM 开启分布式事务(TM 向 TC 注册全局事务记录);1. 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );1. TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);1. TC 汇总事务信息,决定分布式事务是提交还是回滚;1. TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。<a name="bn5kX"></a>## 5.2 AT模式(默认)如何做到对业务的无侵入Seata有四大模式:AT(默认)、TCC、SAGA、XA。(阿里云上的AT叫做GTS,收费)<br />[AT模式](http://seata.io/zh-cn/docs/dev/mode/at-mode.html)<br />AT模式两阶段提交协议的演变:- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。- 二阶段:- 提交异步化,非常快速地完成。- 回滚通过一阶段的回滚日志进行反向补偿(前面insert,后面回滚时就delete)。每个数据库除了自身存储数据的表以外,都会有一个事务回滚表:undo_log<br />Seata库中存在:branch_table\global_table\lock_table\distributed_lock(高版本才有)这样一些表<a name="V317k"></a>### 5.2.1 一阶段加载在一阶段,Seata 会拦截“业务 SQL”,<br />1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”(前置镜像)<br />2 执行“业务 SQL”更新业务数据,在业务数据更新之后,<br />3 其保存成“after image”,最后生成行锁。<br />以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。<br /><a name="EyIuK"></a>### 5.2.2 二阶段提交因为“业务 SQL”在**一阶段**已经提交至数据库,**二阶段如果顺利提交的话**,那么Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。<br /><a name="A735i"></a>### 5.2.3 二阶段回滚二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。<br />回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”。如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。<br /><a name="qGlrj"></a>## 5.3 debug查看流程最开是seata库中的三张表是没有数据的<br /><br />2003打上断点,debug启动<br /><br />访问[http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100](http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100)。<br /><br />此时seata库中的三个表都是有数据的:<br />看一下branch_table,记录了各个RM的信息,分别对应order、storage、account三个微服务<br /><br />可以看到xid跟global_table中的xid一致。再看global_table:<br />查看lock_table:<br />查看各业务中的undo_log表:<br /><br />rollback_info是JSON字符串,存储了beforeimage、afterimage:```json{"@class": "io.seata.rm.datasource.undo.BranchUndoLog","xid": "192.168.190.1:8091:2090602861","branchId": 2090602864,"sqlUndoLogs": ["java.util.ArrayList",[{"@class": "io.seata.rm.datasource.undo.SQLUndoLog","sqlType": "INSERT","tableName": "`t_order`","beforeImage": {"@class": "io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords","tableName": "`t_order`","rows": ["java.util.ArrayList",[]]},"afterImage": {"@class": "io.seata.rm.datasource.sql.struct.TableRecords","tableName": "`t_order`","rows": ["java.util.ArrayList",[{"@class": "io.seata.rm.datasource.sql.struct.Row","fields": ["java.util.ArrayList",[{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "id","keyType": "PrimaryKey","type": -5,"value": ["java.lang.Long",10]},{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "user_id","keyType": "NULL","type": -5,"value": ["java.lang.Long",1]},{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "product_id","keyType": "NULL","type": -5,"value": ["java.lang.Long",1]},{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "count","keyType": "NULL","type": 4,"value": 10},{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "money","keyType": "NULL","type": 3,"value": ["java.math.BigDecimal",100]},{"@class": "io.seata.rm.datasource.sql.struct.Field","name": "status","keyType": "NULL","type": 4,"value": 0}]]}]]}}]]}
查看seata_storage库中的undo_log表的roobal_info信息,可以看到beforeimage和afterimage分别保存了修改前后的信息。
debug放行,seata库中表中的中间数据和undo_log表的数据都删除了。(我的seata_account表的undo_log中没有被删除,等了半天也没有。)异步任务阶段的分支提交请求将异步和批量地删除相应的undo_log记录。
发现account2003微服务的日志跟2001和2002都不一样

5.4 整体流程图

,这四张表跟config.txt文件中的配置对应。
