分布式事务问题

只要用到分布式,必然会提及分布式的事务。
在分布式之前,一切组件全都在一台机器上。
在使用分布式之后,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源。
业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
image.png
一句话:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。

一、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 典型的分布式控制事务流程

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
  2. XID 在微服务调用链路的上下文中传播;(也就是在多个TM,RM中传播)
  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议;
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

image.png

1.3 Seata-Server的下载与配置

我这里下载了0.9.0版本跟1.4.2版本(配了半天没配好,后面再填坑),差别还是蛮大的。

1.3.1 修改file.conf文件

解压到指定目录并修改conf目录下的file.conf配置文件。

  1. 备份原始file.conf文件。
    1. 主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息。
  2. 修改file.conf文件
    1. service模块(1.4.2里面没有这个模块,需要自己加)
    2. store模块

image.png

  1. ## transaction log store, only used in seata-server
  2. store {
  3. ## store mode: file、db、redis
  4. mode = "db"
  5. ## rsa decryption public key
  6. publicKey = ""
  7. ## file store property
  8. file {
  9. ## store location dir
  10. dir = "sessionStore"
  11. # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
  12. maxBranchSessionSize = 16384
  13. # globe session size , if exceeded throws exceptions
  14. maxGlobalSessionSize = 512
  15. # file buffer size , if exceeded allocate new buffer
  16. fileWriteBufferCacheSize = 16384
  17. # when recover batch read size
  18. sessionReloadReadSize = 100
  19. # async, sync
  20. flushDiskMode = async
  21. }
  22. ## database store property
  23. db {
  24. ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
  25. datasource = "druid"
  26. ## mysql/oracle/postgresql/h2/oceanbase etc.
  27. dbType = "mysql"
  28. ## mysql 5.xx
  29. ## driverClassName = "com.mysql.jdbc.Driver"
  30. ## mysql 8.xx
  31. driverClassName = "com.mysql.cj.jdbc.Driver"
  32. ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
  33. ## url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
  34. url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf-8&useSSL=false&nullCatalogMeansCurrent=true&serverTimezone=UTC"
  35. user = "root"
  36. password = "10086"
  37. minConn = 5
  38. maxConn = 100
  39. globalTable = "global_table"
  40. branchTable = "branch_table"
  41. lockTable = "lock_table"
  42. queryLimit = 100
  43. maxWait = 5000
  44. }
  45. ## redis store property
  46. redis {
  47. ## redis mode: single、sentinel
  48. mode = "single"
  49. ## single mode property
  50. single {
  51. host = "127.0.0.1"
  52. port = "6379"
  53. }
  54. ## sentinel mode property
  55. sentinel {
  56. masterName = ""
  57. ## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
  58. sentinelHosts = ""
  59. }
  60. password = ""
  61. database = "0"
  62. minConn = 1
  63. maxConn = 10
  64. maxTotal = 100
  65. queryLimit = 100
  66. }
  67. }
  68. ## 1.4.2 版本需要自己手动增加service模块
  69. service {
  70. #vgroup->rgroup
  71. vgroup_mapping.my_test_tx_group = "my_group"
  72. #only support single node
  73. default.grouplist = "127.0.0.1:8091"
  74. #degrade current not support
  75. enableDegrade = false
  76. #disable
  77. disable = false
  78. #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  79. max.commit.retry.timeout = "-1"
  80. max.rollback.retry.timeout = "-1"
  81. }

1.3.2 数据库中建库建表

数据库新建库seata,建表db_store.sql在\seata-server-0.9.0\seata\conf目录里面
image.png

1.3.3 修改seata-server-0.9.0\seata\conf目录下的registry.conf配置文件

目的是:指明注册中心为nacos,及修改nacos连接信息
image.png

1.3.4 启动nacos和seata

seata-server-0.9.0\seata\bin\seata-server.bat
启动失败,报错:
image.png
解决:0.9.0默认的mysql是5.1.30版本,将lib文件夹下mysql-connector-java-5.1.30.jar删除,替换成自己mysql版本的jar包,我的是mysql-connector-java-8.0.22.jar。
再次启动:
image.png
出现这些提示信息代表seata启动成功。
nacos中成功注册了seate:
image.png

**Seata 1.4.2版本填坑—-nacos作为seata的注册/配置中心

seata1.2.0 Seata1.4.0+nacos
Seata0.9.0版本不支持集群,生产环境下需要使用1.0.0以上版本。我们这里配置seata的最新版本1.4.2,下载后文件目录如下图所示:
image.png

0. 启动nacos

启动nacos,新建一个命名空间seata用于存放seata的配置信息。
image.png
注意这里的命名空间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版本
image.png
最终完整代码file.conf:file.conf.txt

②修改conf\registry.conf文件

image.png
image.png
思考:这里我们把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文件夹下:
image.png
用这个可以直接下当前页的文件github-directory-downloader
image.png
nacos-config.sh

② config.txt准备及修改

在conf目录下还需要一个config.txt文件,1.4.2版本同样没有,还是去README里面的config-center超链接。
image.png
config.txt需要放在conf的上级目录下。
image.png
修改config.txt文件中的内容,主要是下面这几项:
image.png
改为使用db存储:
image.png
注意这里store.db.url中数据库的名字就是我们之后需要新建的数据库的名字。
image.png相比于其他版本,1.4.2这里多了个distributedLockTable。
整个config.txt文件中,store.publicKey、store.redis.sentinel.masterName、store.redis.sentinel.sentinelHosts、store.redis.password四个属性默认都是空的image.png。所以后面在将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中读取)
导入配置:
image.png
然后在git bash界面输入:

  1. 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就行。
image.png
可以看到共98项,导入失败4项,就是上面没有值的那四项(不影响,如果用到直接在nacos里面新建配置即可)
image.png
可以看到,nacos的seata命名空间中已经导入了配置项。(seata命名空间是我自己创建的,可以按自己的需求创建,不创建默认的就是public。)

3. 数据库中建库建表

我们先创建数据库seata1_4_2(数据库要与config.txt中db设置那里对应),数据库的建表语句在README文件的server连接中:
image.png
然后执行mysql.sql(1.4.2多了个distributed_lock表,和一些插入语句):

  1. -- -------------------------------- The script used when storeMode is 'db' --------------------------------
  2. -- the table to store GlobalSession data
  3. CREATE TABLE IF NOT EXISTS `global_table`
  4. (
  5. `xid` VARCHAR(128) NOT NULL,
  6. `transaction_id` BIGINT,
  7. `status` TINYINT NOT NULL,
  8. `application_id` VARCHAR(32),
  9. `transaction_service_group` VARCHAR(32),
  10. `transaction_name` VARCHAR(128),
  11. `timeout` INT,
  12. `begin_time` BIGINT,
  13. `application_data` VARCHAR(2000),
  14. `gmt_create` DATETIME,
  15. `gmt_modified` DATETIME,
  16. PRIMARY KEY (`xid`),
  17. KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
  18. KEY `idx_transaction_id` (`transaction_id`)
  19. ) ENGINE = InnoDB
  20. DEFAULT CHARSET = utf8;
  21. -- the table to store BranchSession data
  22. CREATE TABLE IF NOT EXISTS `branch_table`
  23. (
  24. `branch_id` BIGINT NOT NULL,
  25. `xid` VARCHAR(128) NOT NULL,
  26. `transaction_id` BIGINT,
  27. `resource_group_id` VARCHAR(32),
  28. `resource_id` VARCHAR(256),
  29. `branch_type` VARCHAR(8),
  30. `status` TINYINT,
  31. `client_id` VARCHAR(64),
  32. `application_data` VARCHAR(2000),
  33. `gmt_create` DATETIME(6),
  34. `gmt_modified` DATETIME(6),
  35. PRIMARY KEY (`branch_id`),
  36. KEY `idx_xid` (`xid`)
  37. ) ENGINE = InnoDB
  38. DEFAULT CHARSET = utf8;
  39. -- the table to store lock data
  40. CREATE TABLE IF NOT EXISTS `lock_table`
  41. (
  42. `row_key` VARCHAR(128) NOT NULL,
  43. `xid` VARCHAR(128),
  44. `transaction_id` BIGINT,
  45. `branch_id` BIGINT NOT NULL,
  46. `resource_id` VARCHAR(256),
  47. `table_name` VARCHAR(32),
  48. `pk` VARCHAR(36),
  49. `gmt_create` DATETIME,
  50. `gmt_modified` DATETIME,
  51. PRIMARY KEY (`row_key`),
  52. KEY `idx_branch_id` (`branch_id`)
  53. ) ENGINE = InnoDB
  54. DEFAULT CHARSET = utf8;
  55. CREATE TABLE IF NOT EXISTS `distributed_lock`
  56. (
  57. `lock_key` CHAR(20) NOT NULL,
  58. `lock_value` VARCHAR(20) NOT NULL,
  59. `expire` BIGINT,
  60. primary key (`lock_key`)
  61. ) ENGINE = InnoDB
  62. DEFAULT CHARSET = utf8mb4;
  63. INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
  64. INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
  65. INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
  66. INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

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

4. 启动seata

运行bin目录下的seata-server.bat。
出现下面字段表示seata启动成功。seata启动日志在C:\Users\admin\logs\seata文件夹下。
image.png
nacos中在seata命名空间内也成功注册,注意这里服务名对应的是registry.conf文件中nacos下面application的值。0.9.0版本好像设置不了这个,默认是serverAddr。
image.png

二、订单/库存/账户业务数据库准备

以下演示都需要先启动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;

  1. <a name="AjDc0"></a>
  2. ### 2. 按照上述3库分别创建对应业务表
  3. seata_order库下建t_order表:
  4. ```sql
  5. CREATE TABLE seata_order.`t_order` (
  6. `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  7. `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
  8. `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
  9. `count` INT(11) DEFAULT NULL COMMENT '数量',
  10. `money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
  11. `status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结'
  12. ) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

seata_storage库下建t_storage 表:

  1. CREATE TABLE `seata_storage`.`t_storage` (
  2. `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  3. `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
  4. `total` INT(11) DEFAULT NULL COMMENT '总库存',
  5. `used` INT(11) DEFAULT NULL COMMENT '已用库存',
  6. `residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
  7. ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
  8. INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
  9. VALUES ('1', '1', '100', '0', '100');

seata_account库下建t_account 表:

  1. CREATE TABLE `seata_account`.t_account (
  2. `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
  3. `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
  4. `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
  5. `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
  6. `residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
  7. ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
  8. 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:
image.pngimage.png

注意:0.9版本跟1.4.2版本的undo_log表的属性有差别,1.4.2版本没有id跟ext这两个属性,个人觉得使用上没有影响

这里的话我还是按0.9.0版本来建表。

  1. # 0.9.0 版本
  2. DROP TABLE IF EXISTS `undo_log`;
  3. CREATE TABLE `undo_log` (
  4. `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  5. `branch_id` BIGINT(20) NOT NULL,
  6. `xid` VARCHAR(100) NOT NULL,
  7. `context` VARCHAR(128) NOT NULL,
  8. `rollback_info` LONGBLOB NOT NULL,
  9. `log_status` INT(11) NOT NULL,
  10. `log_created` DATETIME NOT NULL,
  11. `log_modified` DATETIME NOT NULL,
  12. `ext` VARCHAR(100) DEFAULT NULL,
  13. PRIMARY KEY (`id`),
  14. UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
  15. ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  16. # 1.4.2版本
  17. DROP TABLE IF EXISTS `undo_log`;
  18. CREATE TABLE IF NOT EXISTS `undo_log`
  19. (
  20. `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
  21. `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
  22. `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
  23. `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
  24. `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
  25. `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
  26. `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
  27. UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
  28. ) ENGINE = InnoDB
  29. AUTO_INCREMENT = 1
  30. DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

最终效果: 这里展示了1.4.2版本跟0.9版本,实际中用一个就行,别的版本一样。
image.png

三、订单/库存/账户业务微服务准备

业务需求:下订单->减库存->扣余额->改(订单)状态


版本对应关系——很重要

注意:由于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

新建seata-order-service2001

(1) pom

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>jdk8cloud2021</artifactId>
  7. <groupId>com.atguigu.springcloud</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>seata-order-service2001</artifactId>
  12. <properties>
  13. <maven.compiler.source>8</maven.compiler.source>
  14. <maven.compiler.target>8</maven.compiler.target>
  15. </properties>
  16. <dependencies>
  17. <!--nacos-->
  18. <dependency>
  19. <groupId>com.alibaba.cloud</groupId>
  20. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  21. </dependency>
  22. <!--seata-->
  23. <dependency>
  24. <groupId>com.alibaba.cloud</groupId>
  25. <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  26. <!-- 因为兼容版本问题,所以需要剔除它自带的seata的包 -->
  27. <exclusions>
  28. <exclusion>
  29. <artifactId>seata-all</artifactId>
  30. <groupId>io.seata</groupId>
  31. </exclusion>
  32. </exclusions>
  33. </dependency>
  34. <!-- 要跟我们安装SeaTa的一致! -->
  35. <dependency>
  36. <groupId>io.seata</groupId>
  37. <artifactId>seata-all</artifactId>
  38. <version>0.9.0</version>
  39. </dependency>
  40. <!--feign-->
  41. <dependency>
  42. <groupId>org.springframework.cloud</groupId>
  43. <artifactId>spring-cloud-starter-openfeign</artifactId>
  44. </dependency>
  45. <!--web-actuator-->
  46. <dependency>
  47. <groupId>org.springframework.boot</groupId>
  48. <artifactId>spring-boot-starter-web</artifactId>
  49. </dependency>
  50. <dependency>
  51. <groupId>org.springframework.boot</groupId>
  52. <artifactId>spring-boot-starter-actuator</artifactId>
  53. </dependency>
  54. <!--mysql-druid-->
  55. <dependency>
  56. <groupId>mysql</groupId>
  57. <artifactId>mysql-connector-java</artifactId>
  58. <version>8.0.22</version>
  59. </dependency>
  60. <dependency>
  61. <groupId>com.alibaba</groupId>
  62. <artifactId>druid-spring-boot-starter</artifactId>
  63. <version>1.1.10</version>
  64. </dependency>
  65. <dependency>
  66. <groupId>org.mybatis.spring.boot</groupId>
  67. <artifactId>mybatis-spring-boot-starter</artifactId>
  68. <version>2.0.0</version>
  69. </dependency>
  70. <dependency>
  71. <groupId>org.springframework.boot</groupId>
  72. <artifactId>spring-boot-starter-test</artifactId>
  73. <scope>test</scope>
  74. </dependency>
  75. <dependency>
  76. <groupId>org.projectlombok</groupId>
  77. <artifactId>lombok</artifactId>
  78. <optional>true</optional>
  79. </dependency>
  80. </dependencies>
  81. </project>

(2) application.yml

这里配置的是我们自己微服务的数据源

  1. server:
  2. port: 2001
  3. spring:
  4. application:
  5. name: seata-order-service
  6. cloud:
  7. alibaba:
  8. seata:
  9. #自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应
  10. #vgroup_mapping.my_test_tx_group = "my_group"
  11. tx-service-group: my_group
  12. nacos:
  13. discovery:
  14. server-addr: localhost:8848
  15. datasource:
  16. driver-class-name: com.mysql.cj.jdbc.Driver
  17. url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
  18. username: root
  19. password: 10086
  20. feign:
  21. hystrix:
  22. enabled: false
  23. logging:
  24. level:
  25. io:
  26. seata: info
  27. mybatis:
  28. mapperLocations: classpath:mapper/*.xml

(3) file.conf

程序中依赖的是 seata-all,对应于 *.conf 文件,所以需要在resource新建.conf文件,高版本的支持yml、properties配置。这里仅仅是seata-order-service2001模块的file.conf(配置2001的分布式事务),seata软件那里配置的是总控file.conf。

注意修改这两处:
image.png

  1. transport {
  2. # tcp udt unix-domain-socket
  3. type = "TCP"
  4. #NIO NATIVE
  5. server = "NIO"
  6. #enable heartbeat
  7. heartbeat = true
  8. #thread factory for netty
  9. thread-factory {
  10. boss-thread-prefix = "NettyBoss"
  11. worker-thread-prefix = "NettyServerNIOWorker"
  12. server-executor-thread-prefix = "NettyServerBizHandler"
  13. share-boss-worker = false
  14. client-selector-thread-prefix = "NettyClientSelector"
  15. client-selector-thread-size = 1
  16. client-worker-thread-prefix = "NettyClientWorkerThread"
  17. # netty boss thread size,will not be used for UDT
  18. boss-thread-size = 1
  19. #auto default pin or 8
  20. worker-thread-size = 8
  21. }
  22. shutdown {
  23. # when destroy server, wait seconds
  24. wait = 3
  25. }
  26. serialization = "seata"
  27. compressor = "none"
  28. }
  29. service {
  30. #修改自定义事务组名称,这里跟在seata里配置的不一样,这里只针对2001自己的事务,
  31. #而seata里针对的是整个分布式全局事务
  32. #这里要注意 vgroup_mapping. 后面的值,要跟seata-server安装时 conf 文件夹下file.conf service模块设定的
  33. #vgroup_mapping.my_test_tx_group = "my_group"
  34. vgroup_mapping.my_group = "default"
  35. default.grouplist = "127.0.0.1:8091"
  36. enableDegrade = false
  37. disable = false
  38. max.commit.retry.timeout = "-1"
  39. max.rollback.retry.timeout = "-1"
  40. disableGlobalTransaction = false
  41. }
  42. client {
  43. async.commit.buffer.limit = 10000
  44. lock {
  45. retry.internal = 10
  46. retry.times = 30
  47. }
  48. report.retry.count = 5
  49. tm.commit.retry.count = 1
  50. tm.rollback.retry.count = 1
  51. }
  52. ## transaction log store
  53. store {
  54. ## store mode: filedb
  55. mode = "db"
  56. ## file store
  57. file {
  58. dir = "sessionStore"
  59. # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
  60. max-branch-session-size = 16384
  61. # globe session size , if exceeded throws exceptions
  62. max-global-session-size = 512
  63. # file buffer size , if exceeded allocate new buffer
  64. file-write-buffer-cache-size = 16384
  65. # when recover batch read size
  66. session.reload.read_size = 100
  67. # async, sync
  68. flush-disk-mode = async
  69. }
  70. ## database store
  71. db {
  72. ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
  73. datasource = "dbcp"
  74. ## mysql/oracle/h2/oceanbase etc.
  75. db-type = "mysql"
  76. ## 这里要注意mysql5mysql8不一样
  77. driver-class-name = "com.mysql.cj.jdbc.Driver"
  78. url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC"
  79. user = "root"
  80. password = "10086"
  81. min-conn = 1
  82. max-conn = 3
  83. global.table = "global_table"
  84. branch.table = "branch_table"
  85. lock-table = "lock_table"
  86. query-limit = 100
  87. }
  88. }
  89. lock {
  90. ## the lock store mode: localremote
  91. mode = "remote"
  92. local {
  93. ## store locks in user's database
  94. }
  95. remote {
  96. ## store locks in the seata's server
  97. }
  98. }
  99. recovery {
  100. #schedule committing retry period in milliseconds
  101. committing-retry-period = 1000
  102. #schedule asyn committing retry period in milliseconds
  103. asyn-committing-retry-period = 1000
  104. #schedule rollbacking retry period in milliseconds
  105. rollbacking-retry-period = 1000
  106. #schedule timeout retry period in milliseconds
  107. timeout-retry-period = 1000
  108. }
  109. transaction {
  110. undo.data.validation = true
  111. undo.log.serialization = "jackson"
  112. undo.log.save.days = 7
  113. #schedule delete expired undo_log in milliseconds
  114. undo.log.delete.period = 86400000
  115. undo.log.table = "undo_log"
  116. }
  117. ## metrics settings
  118. metrics {
  119. enabled = false
  120. registry-type = "compact"
  121. # multi exporters use comma divided
  122. exporter-list = "prometheus"
  123. exporter-prometheus-port = 9898
  124. }
  125. support {
  126. ## spring
  127. spring {
  128. # auto proxy the DataSource bean
  129. datasource.autoproxy = false
  130. }
  131. }

几个配置文件对应关系

image.png

(4) registry.conf

指明注册到nacos中:
image.png

  1. registry {
  2. # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  3. type = "nacos"
  4. nacos {
  5. serverAddr = "localhost:8848" # 如果nacos不在本机,就写服务器的IP
  6. namespace = ""
  7. cluster = "default"
  8. }
  9. eureka {
  10. serviceUrl = "http://localhost:8761/eureka"
  11. application = "default"
  12. weight = "1"
  13. }
  14. redis {
  15. serverAddr = "localhost:6379"
  16. db = "0"
  17. }
  18. zk {
  19. cluster = "default"
  20. serverAddr = "127.0.0.1:2181"
  21. session.timeout = 6000
  22. connect.timeout = 2000
  23. }
  24. consul {
  25. cluster = "default"
  26. serverAddr = "127.0.0.1:8500"
  27. }
  28. etcd3 {
  29. cluster = "default"
  30. serverAddr = "http://localhost:2379"
  31. }
  32. sofa {
  33. serverAddr = "127.0.0.1:9603"
  34. application = "default"
  35. region = "DEFAULT_ZONE"
  36. datacenter = "DefaultDataCenter"
  37. cluster = "default"
  38. group = "SEATA_GROUP"
  39. addressWaitTime = "3000"
  40. }
  41. file {
  42. name = "file.conf"
  43. }
  44. }
  45. config {
  46. # file、nacos 、apollo、zk、consul、etcd3
  47. type = "file"
  48. nacos {
  49. serverAddr = "localhost"
  50. namespace = ""
  51. }
  52. consul {
  53. serverAddr = "127.0.0.1:8500"
  54. }
  55. apollo {
  56. app.id = "seata-server"
  57. apollo.meta = "http://192.168.1.204:8801"
  58. }
  59. zk {
  60. serverAddr = "127.0.0.1:2181"
  61. session.timeout = 6000
  62. connect.timeout = 2000
  63. }
  64. etcd3 {
  65. serverAddr = "http://localhost:2379"
  66. }
  67. file {
  68. name = "file.conf"
  69. }
  70. }

(5) domain

domain 就是entity(pojo,bean),对应数据库的表,不同公司习惯不一样。
新建Order类与CommonResult类

Order

  1. package com.atguigu.cloudalibaba.domain;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. import java.math.BigDecimal;
  6. @Data
  7. @AllArgsConstructor
  8. @NoArgsConstructor
  9. public class Order {
  10. private Long id;
  11. private Long userId;
  12. private Long productId;
  13. private Integer count;
  14. private BigDecimal money;
  15. /**
  16. * 订单状态:0:创建中;1:已完结
  17. */
  18. private Integer status;
  19. }

CommonResult

  1. package com.atguigu.cloudalibaba.domain;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. @Data
  6. @AllArgsConstructor
  7. @NoArgsConstructor
  8. public class CommonResult<T> {
  9. private Integer code;
  10. private String message;
  11. private T data;
  12. public CommonResult(Integer code, String message) {
  13. this(code, message, null);
  14. }
  15. }

(6) Dao接口及实现(SQL映射文件)

dao中至少要有两个方法,一个是创建订单,一个是修改订单状态

OrderDao

  1. package com.atguigu.cloudalibaba.dao;
  2. import com.atguigu.cloudalibaba.domain.Order;
  3. import org.apache.ibatis.annotations.Mapper;
  4. import org.apache.ibatis.annotations.Param;
  5. /**
  6. * @author MrLinxi
  7. * @Description
  8. * @create 2021-11-10-22:24
  9. */
  10. @Mapper
  11. public interface OrderDao {
  12. /**
  13. * 创建订单
  14. */
  15. void create(Order order);
  16. /**
  17. * 修改订单状态,从0改为1
  18. */
  19. void update(@Param("userId") Long userId, @Param("status") Integer status);
  20. }

OrderMapper.xml

resources文件夹下新建mapper文件夹后添加OrderMapper.xml。完成dao的具体实现。

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
  3. <mapper namespace="com.atguigu.cloudalibaba.dao.OrderDao">
  4. <!--定义一个结果集和实体类的映射表-->
  5. <resultMap id="BaseResultMap" type="com.atguigu.cloudalibaba.domain.Order">
  6. <id column="id" property="id" jdbcType="BIGINT"/>
  7. <result column="user_id" property="userId" jdbcType="BIGINT"/>
  8. <result column="product_id" property="productId" jdbcType="BIGINT"/>
  9. <result column="count" property="count" jdbcType="INTEGER"/>
  10. <result column="money" property="money" jdbcType="DECIMAL"/>
  11. <result column="status" property="status" jdbcType="INTEGER"/>
  12. </resultMap>
  13. <insert id="create">
  14. INSERT INTO `t_order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`)
  15. VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0);
  16. </insert>
  17. <update id="update">
  18. UPDATE `t_order`
  19. SET status = 1
  20. WHERE user_id = #{userId} AND status = #{status};
  21. </update>
  22. </mapper>

(7) Service接口及实现

Order2001驱动自己,外加调用库存和账户:共3个service

OrderService

  1. package com.atguigu.cloudalibaba.service;
  2. import com.atguigu.cloudalibaba.domain.Order;
  3. public interface OrderService {
  4. // 创建订单
  5. void create(Order order);
  6. }

StorageService

  1. package com.atguigu.cloudalibaba.service;
  2. import com.atguigu.cloudalibaba.domain.CommonResult;
  3. import org.springframework.cloud.openfeign.FeignClient;
  4. import org.springframework.web.bind.annotation.PostMapping;
  5. import org.springframework.web.bind.annotation.RequestParam;
  6. //通过OpenFeign远程调用库存的微服务
  7. @FeignClient(value = "seata-storage-service")
  8. public interface StorageService {
  9. //扣减库存,比如买了5个1号商品:对1号商品库存减5
  10. @PostMapping(value = "/storage/decrease")
  11. CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
  12. }

AccountService

  1. package com.atguigu.cloudalibaba.service;
  2. import com.atguigu.cloudalibaba.domain.CommonResult;
  3. import org.springframework.cloud.openfeign.FeignClient;
  4. import org.springframework.web.bind.annotation.PostMapping;
  5. import org.springframework.web.bind.annotation.RequestParam;
  6. import java.math.BigDecimal;
  7. //通过OpenFeign远程调用账号微服务
  8. @FeignClient(value = "seata-account-service")
  9. public interface AccountService {
  10. //扣减账户余额,需要传入用户ID跟扣除的金额
  11. //@RequestMapping(value = "/account/decrease", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
  12. @PostMapping("/account/decrease")
  13. CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
  14. }

OrderServiceImpl

  1. package com.atguigu.cloudalibaba.service.Impl;
  2. import com.atguigu.cloudalibaba.dao.OrderDao;
  3. import com.atguigu.cloudalibaba.domain.Order;
  4. import com.atguigu.cloudalibaba.service.AccountService;
  5. import com.atguigu.cloudalibaba.service.OrderService;
  6. import com.atguigu.cloudalibaba.service.StorageService;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.stereotype.Service;
  9. import javax.annotation.Resource;
  10. @Service
  11. @Slf4j
  12. public class OrderServiceImpl implements OrderService {
  13. @Resource
  14. private OrderDao orderDao;
  15. @Resource
  16. private StorageService storageService;
  17. @Resource
  18. private AccountService accountService;
  19. /**
  20. * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
  21. * 简单说:
  22. * 下订单->减库存->减余额->改状态
  23. */
  24. @Override
  25. public void create(Order order) {
  26. log.info("------->下单开始");
  27. //本应用创建订单
  28. orderDao.create(order);
  29. //远程调用库存服务扣减库存
  30. log.info("------->订单微服务调用库存微服务,扣减库存开始");
  31. storageService.decrease(order.getProductId(),order.getCount());
  32. log.info("------->订单微服务调用库存微服务,扣减库存结束");
  33. //远程调用账户服务扣减余额
  34. log.info("------->订单微服务调用账户微服务,扣减余额开始");
  35. accountService.decrease(order.getUserId(),order.getMoney());
  36. log.info("------->订单微服务调用账户微服务,减余额结束");
  37. //修改订单状态为已完成
  38. log.info("------->order-service中修改订单状态开始");
  39. // 这里的话是不是应该是orderId?
  40. orderDao.update(order.getUserId(),0);
  41. log.info("------->order-service中修改订单状态结束");
  42. log.info("------->下单结束");
  43. }
  44. }

(8) controller

  1. package com.atguigu.cloudalibaba.controller;
  2. import com.atguigu.cloudalibaba.domain.CommonResult;
  3. import com.atguigu.cloudalibaba.domain.Order;
  4. import com.atguigu.cloudalibaba.service.OrderService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.web.bind.annotation.GetMapping;
  7. import org.springframework.web.bind.annotation.RestController;
  8. @RestController
  9. public class OrderController {
  10. @Autowired
  11. private OrderService orderService;
  12. //创建订单
  13. @GetMapping(value = "/order/create")
  14. public CommonResult create(Order order) {
  15. orderService.create(order);
  16. return new CommonResult(200, "订单创建成功!");
  17. }
  18. }

(9) config

MyBatisConfig

mybatis配置类,绑定实现文件OrderMapper.xml与Dao接口

  1. package com.atguigu.cloudalibaba.config;
  2. import org.mybatis.spring.annotation.MapperScan;
  3. import org.springframework.context.annotation.Configuration;
  4. @Configuration
  5. @MapperScan({"com.atguigu.cloudalibaba.dao"})
  6. public class MyBatisConfig {
  7. }

DataSourceProxyConfig

DataSouce的包是sql下的,DataSourceProxy是seata下的,不要搞错了。
image.png

  1. package com.atguigu.cloudalibaba.config;
  2. import com.alibaba.druid.pool.DruidDataSource;
  3. import io.seata.rm.datasource.DataSourceProxy;
  4. import org.apache.ibatis.session.SqlSessionFactory;
  5. import org.mybatis.spring.SqlSessionFactoryBean;
  6. import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
  7. import org.springframework.beans.factory.annotation.Value;
  8. import org.springframework.boot.context.properties.ConfigurationProperties;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import org.springframework.context.annotation.Primary;
  12. import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
  13. import javax.sql.DataSource;
  14. //使用Seata对数据源进行代理
  15. @Configuration
  16. public class DataSourceProxyConfig {
  17. @Value("${mybatis.mapperLocations}")
  18. private String mapperLocations;
  19. @Bean
  20. @ConfigurationProperties(prefix = "spring.datasource")
  21. public DataSource druidDataSource(){
  22. return new DruidDataSource();
  23. }
  24. @Bean
  25. // @Primary
  26. //DataSourceProxy方法上标注@Primary就可以了,这样就自动用的代理的DataSource(DS的子类),
  27. // 而不是Druid的, 否则就需要自己构造sqlSessionFactory
  28. public DataSourceProxy dataSourceProxy(DataSource dataSource) {
  29. return new DataSourceProxy(dataSource);
  30. }
  31. @Bean
  32. public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
  33. SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  34. sqlSessionFactoryBean.setDataSource(dataSourceProxy);
  35. sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
  36. sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
  37. return sqlSessionFactoryBean.getObject();
  38. }
  39. }

(10) 主启动类

  1. package com.atguigu.cloudalibaba;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  5. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  6. import org.springframework.cloud.openfeign.EnableFeignClients;
  7. @EnableFeignClients
  8. @EnableDiscoveryClient
  9. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消数据源的自动创建
  10. public class SeataOrderMainApp2001 {
  11. public static void main(String[] args) {
  12. SpringApplication.run(SeataOrderMainApp2001.class, args);
  13. }
  14. }

启动测试

先启动nacos-1.1.4和seata-0.9.0,再启动2001。
2001启动成功,成功注册到nacos中
image.png
image.png
测试nacos-2.0.3和seata-0.9.0,再启动2001 也可以启动成功
image.png
image.png

3.2 新建库存Storage-Module——seata-storage-service2002

(1) pom

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>jdk8cloud2021</artifactId>
  7. <groupId>com.atguigu.springcloud</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>seata-storage-service2002</artifactId>
  12. <properties>
  13. <maven.compiler.source>8</maven.compiler.source>
  14. <maven.compiler.target>8</maven.compiler.target>
  15. </properties>
  16. <dependencies>
  17. <!--nacos-->
  18. <dependency>
  19. <groupId>com.alibaba.cloud</groupId>
  20. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  21. </dependency>
  22. <!--seata-->
  23. <dependency>
  24. <groupId>com.alibaba.cloud</groupId>
  25. <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  26. <!-- 因为兼容版本问题,所以需要剔除它自带的seata的包 -->
  27. <exclusions>
  28. <exclusion>
  29. <artifactId>seata-all</artifactId>
  30. <groupId>io.seata</groupId>
  31. </exclusion>
  32. </exclusions>
  33. </dependency>
  34. <!--这里引入的版本要跟安装的版本对应-->
  35. <dependency>
  36. <groupId>io.seata</groupId>
  37. <artifactId>seata-all</artifactId>
  38. <version>0.9.0</version>
  39. </dependency>
  40. <!--feign-->
  41. <dependency>
  42. <groupId>org.springframework.cloud</groupId>
  43. <artifactId>spring-cloud-starter-openfeign</artifactId>
  44. </dependency>
  45. <!--web-actuator-->
  46. <dependency>
  47. <groupId>org.springframework.boot</groupId>
  48. <artifactId>spring-boot-starter-web</artifactId>
  49. </dependency>
  50. <dependency>
  51. <groupId>org.springframework.boot</groupId>
  52. <artifactId>spring-boot-starter-actuator</artifactId>
  53. </dependency>
  54. <!--mysql-druid-->
  55. <dependency>
  56. <groupId>mysql</groupId>
  57. <artifactId>mysql-connector-java</artifactId>
  58. <version>8.0.22</version>
  59. </dependency>
  60. <dependency>
  61. <groupId>com.alibaba</groupId>
  62. <artifactId>druid-spring-boot-starter</artifactId>
  63. <version>1.1.10</version>
  64. </dependency>
  65. <dependency>
  66. <groupId>org.mybatis.spring.boot</groupId>
  67. <artifactId>mybatis-spring-boot-starter</artifactId>
  68. <version>2.0.0</version>
  69. </dependency>
  70. <dependency>
  71. <groupId>org.springframework.boot</groupId>
  72. <artifactId>spring-boot-starter-test</artifactId>
  73. <scope>test</scope>
  74. </dependency>
  75. <dependency>
  76. <groupId>org.projectlombok</groupId>
  77. <artifactId>lombok</artifactId>
  78. <optional>true</optional>
  79. </dependency>
  80. </dependencies>
  81. </project>

(2) application.yml

  1. server:
  2. port: 2002
  3. spring:
  4. application:
  5. name: seata-storage-service
  6. cloud:
  7. alibaba:
  8. seata:
  9. #自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应
  10. #vgroup_mapping.my_test_tx_group = "my_group"
  11. # tx-service-group: my_test_tx_group
  12. tx-service-group: my_group
  13. nacos:
  14. discovery:
  15. server-addr: localhost:8848
  16. datasource:
  17. driver-class-name: com.mysql.cj.jdbc.Driver
  18. url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
  19. username: root
  20. password: 10086
  21. #服务提供端不需要远程调用
  22. #feign:
  23. # hystrix:
  24. # enabled: false
  25. logging:
  26. level:
  27. io:
  28. seata: info
  29. mybatis:
  30. # 扫描类路径下mapper文件夹下的.xml配置文件
  31. mapperLocations: classpath:mapper/*.xml

(3) file.conf & registry.conf

跟2001的一模一样

(4) domain

Storage

  1. package com.atguigu.cloudalibaba.domian;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. @Data
  6. @AllArgsConstructor
  7. @NoArgsConstructor
  8. public class Storage {
  9. private Long id;
  10. //产品ID
  11. private Long productId;
  12. //总库存
  13. private Integer total;
  14. //已用库存
  15. private Integer used;
  16. //剩余库存
  17. private Integer residue;
  18. }

CommonResult

  1. package com.atguigu.cloudalibaba.domain;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. @Data
  6. @AllArgsConstructor
  7. @NoArgsConstructor
  8. public class CommonResult<T> {
  9. private Integer code;
  10. private String message;
  11. private T data;
  12. public CommonResult(Integer code, String message) {
  13. this(code, message, null);
  14. }
  15. }

(5) Dao接口及实现(SQL映射文件)

StorageDao

  1. package com.atguigu.cloudalibaba.dao;
  2. import org.apache.ibatis.annotations.Mapper;
  3. import org.apache.ibatis.annotations.Param;
  4. @Mapper
  5. public interface StorageDao {
  6. //扣减库存:根据产品ID扣除
  7. void decrease(@Param("productId") Long productId, @Param("count") Integer count);
  8. }

StorageMapper.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
  3. <mapper namespace="com.atguigu.cloudalibaba.dao.StorageDao">
  4. <resultMap id="storage" type="com.atguigu.cloudalibaba.domian.Storage">
  5. <id column="id" property="id" jdbcType="BIGINT"></id>
  6. <result column="product_id" property="productId" jdbcType="BIGINT"></result>
  7. <result column="total" property="total" jdbcType="BIGINT"></result>
  8. <result column="used" property="used" jdbcType="INTEGER"></result>
  9. <result column="residue" property="residue" jdbcType="INTEGER"></result>
  10. </resultMap>
  11. <update id="decrease">
  12. update `t_storage`
  13. SET `used` = `used` + #{count}, `residue` = `residue` - #{count}
  14. WHERE `product_id` = #{productId};
  15. </update>
  16. </mapper>

(6) Service接口及实现

StorageService

  1. package com.atguigu.cloudalibaba.service;
  2. public interface StorageService {
  3. /**
  4. * 扣减库存
  5. */
  6. void decrease(Long productId, Integer count);
  7. }

StorageServiceImpl

  1. package com.atguigu.cloudalibaba.service.Impl;
  2. import com.atguigu.cloudalibaba.dao.StorageDao;
  3. import com.atguigu.cloudalibaba.service.StorageService;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Service;
  9. @Service
  10. @Slf4j
  11. public class StorageServiceImpl implements StorageService {
  12. private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
  13. @Autowired
  14. private StorageDao storageDao;
  15. /**
  16. * 扣减库存
  17. * @param productId
  18. * @param count
  19. */
  20. @Override
  21. public void decrease(Long productId, Integer count) {
  22. // log.info("-------->storage-service中扣减库存");
  23. LOGGER.info("-------->storage-service中扣减库存");
  24. storageDao.decrease(productId, count);
  25. }
  26. }

(7) Controller

  1. package com.atguigu.cloudalibaba.controller;
  2. import com.atguigu.cloudalibaba.domian.CommonResult;
  3. import com.atguigu.cloudalibaba.service.StorageService;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.web.bind.annotation.RequestMapping;
  6. import org.springframework.web.bind.annotation.RestController;
  7. @RestController
  8. public class StorageController {
  9. @Autowired
  10. private StorageService storageService;
  11. //RequestMapping默认GET POST请求都支持,根据前端自动适应
  12. @RequestMapping(value = "/storage/decrease")
  13. public CommonResult decrease(Long productId, Integer count) {
  14. storageService.decrease(productId, count);
  15. return new CommonResult(200, "扣减库存成功");
  16. }
  17. }

(8) config配置

与2001的一模一样

(9) 主启动类

  1. package com.atguigu.cloudalibaba;
  2. import org.mybatis.spring.annotation.MapperScan;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  6. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  7. import org.springframework.cloud.openfeign.EnableFeignClients;
  8. @EnableDiscoveryClient
  9. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
  10. @EnableFeignClients
  11. public class SeataStorageMainApp2002 {
  12. public static void main(String[] args) {
  13. SpringApplication.run(SeataStorageMainApp2002.class, args);
  14. }
  15. }

启动测试

启动nacos、seata、2002;启动成功,注册进nacos。
image.png
image.png

3.3 新建账户Account-Module——seata-account-service2003

(1) pom

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>jdk8cloud2021</artifactId>
  7. <groupId>com.atguigu.springcloud</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>seata-account-service2003</artifactId>
  12. <properties>
  13. <maven.compiler.source>8</maven.compiler.source>
  14. <maven.compiler.target>8</maven.compiler.target>
  15. </properties>
  16. <dependencies>
  17. <!--nacos-->
  18. <dependency>
  19. <groupId>com.alibaba.cloud</groupId>
  20. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  21. </dependency>
  22. <!--seata-->
  23. <dependency>
  24. <groupId>com.alibaba.cloud</groupId>
  25. <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  26. <!-- 因为兼容版本问题,所以需要剔除它自带的seata的包 -->
  27. <exclusions>
  28. <exclusion>
  29. <artifactId>seata-all</artifactId>
  30. <groupId>io.seata</groupId>
  31. </exclusion>
  32. </exclusions>
  33. </dependency>
  34. <dependency>
  35. <groupId>io.seata</groupId>
  36. <artifactId>seata-all</artifactId>
  37. <version>0.9.0</version>
  38. </dependency>
  39. <!--feign-->
  40. <dependency>
  41. <groupId>org.springframework.cloud</groupId>
  42. <artifactId>spring-cloud-starter-openfeign</artifactId>
  43. </dependency>
  44. <!--web-actuator-->
  45. <dependency>
  46. <groupId>org.springframework.boot</groupId>
  47. <artifactId>spring-boot-starter-web</artifactId>
  48. </dependency>
  49. <dependency>
  50. <groupId>org.springframework.boot</groupId>
  51. <artifactId>spring-boot-starter-actuator</artifactId>
  52. </dependency>
  53. <!--mysql-druid-->
  54. <dependency>
  55. <groupId>mysql</groupId>
  56. <artifactId>mysql-connector-java</artifactId>
  57. <version>8.0.22</version>
  58. </dependency>
  59. <dependency>
  60. <groupId>com.alibaba</groupId>
  61. <artifactId>druid-spring-boot-starter</artifactId>
  62. <version>1.1.10</version>
  63. </dependency>
  64. <dependency>
  65. <groupId>org.mybatis.spring.boot</groupId>
  66. <artifactId>mybatis-spring-boot-starter</artifactId>
  67. <version>2.0.0</version>
  68. </dependency>
  69. <dependency>
  70. <groupId>org.springframework.boot</groupId>
  71. <artifactId>spring-boot-starter-test</artifactId>
  72. <scope>test</scope>
  73. </dependency>
  74. <dependency>
  75. <groupId>org.projectlombok</groupId>
  76. <artifactId>lombok</artifactId>
  77. <optional>true</optional>
  78. </dependency>
  79. </dependencies>
  80. </project>

(2) application.yml

  1. server:
  2. port: 2003
  3. spring:
  4. application:
  5. name: seata-account-service
  6. cloud:
  7. alibaba:
  8. seata:
  9. #自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应
  10. #vgroup_mapping.my_test_tx_group = "my_group"
  11. # tx-service-group: my_test_tx_group
  12. tx-service-group: my_group
  13. # tx-service-group: default
  14. # tx-service-group: my_test_tx_group
  15. nacos:
  16. discovery:
  17. server-addr: localhost:8848
  18. datasource:
  19. driver-class-name: com.mysql.cj.jdbc.Driver
  20. url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
  21. username: root
  22. password: 10086
  23. #feign:
  24. # hystrix:
  25. # enabled: false
  26. logging:
  27. level:
  28. io:
  29. seata: info
  30. mybatis:
  31. # 扫描类路径下mapper文件夹下的.xml配置文件
  32. mapperLocations: classpath:mapper/*.xml

(3) file.conf & registry.conf

跟2001一模一样

(4) domain

Account

  1. package com.atguigu.cloudalibaba.domain;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. @Data
  6. @AllArgsConstructor
  7. @NoArgsConstructor
  8. public class Account {
  9. private Long id;
  10. //用户ID
  11. private Long userId;
  12. //总额度
  13. private Integer total;
  14. //已用额度
  15. private Integer used;
  16. //剩余额度
  17. private Integer residue;
  18. }

CommonResult

  1. package com.atguigu.cloudalibaba.domain;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. @Data
  6. @AllArgsConstructor
  7. @NoArgsConstructor
  8. public class CommonResult<T> {
  9. private Integer code;
  10. private String message;
  11. private T data;
  12. public CommonResult(Integer code, String message) {
  13. this(code, message, null);
  14. }
  15. }

(5) Dao接口及实现

AccountDao

  1. package com.atguigu.cloudalibaba.dao;
  2. import org.apache.ibatis.annotations.Mapper;
  3. import org.apache.ibatis.annotations.Param;
  4. import java.math.BigDecimal;
  5. @Mapper
  6. public interface AccountDao {
  7. /**
  8. * 扣减账户余额
  9. * @param userId
  10. * @param money
  11. */
  12. void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
  13. }

AccountMapper.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
  3. <mapper namespace="com.atguigu.cloudalibaba.dao.AccountDao">
  4. <resultMap id="BaseResultMap" type="com.atguigu.cloudalibaba.domain.Account">
  5. <id column="id" property="id" jdbcType="BIGINT"></id>
  6. <result column="user_id" property="userId" jdbcType="BIGINT"></result>
  7. <result column="total" property="total" jdbcType="DECIMAL"></result>
  8. <result column="used" property="used" jdbcType="DECIMAL"></result>
  9. <result column="residue" property="residue" jdbcType="DECIMAL"></result>
  10. </resultMap>
  11. <update id="decrease">
  12. UPDATE t_account
  13. SET `used` = `used` + #{money}, `residue` = `residue` - #{money}
  14. WHERE `user_id` = #{userId};
  15. </update>
  16. </mapper>

(6) Service接口及实现

AccountService

  1. package com.atguigu.cloudalibaba.service;
  2. import org.springframework.web.bind.annotation.RequestParam;
  3. import java.math.BigDecimal;
  4. public interface AccountService {
  5. /**
  6. * 扣减账户金额
  7. * @param userId 用户ID
  8. * @param money 金额
  9. */
  10. void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
  11. }

AccountServiceImpl

  1. package com.atguigu.cloudalibaba.service.impl;
  2. import com.atguigu.cloudalibaba.dao.AccountDao;
  3. import com.atguigu.cloudalibaba.service.AccountService;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.stereotype.Service;
  7. import javax.annotation.Resource;
  8. import java.math.BigDecimal;
  9. @Service
  10. public class AccountServiceImpl implements AccountService {
  11. private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
  12. @Resource
  13. AccountDao accountDao;
  14. /**
  15. * 扣减账户余额
  16. * @param userId 用户ID
  17. * @param money 金额
  18. */
  19. @Override
  20. public void decrease(Long userId, BigDecimal money) {
  21. LOGGER.info("------->account-service中扣减账户余额开始");
  22. //模拟超时异常,全局事务回滚
  23. //暂停几秒钟线程
  24. //try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }
  25. accountDao.decrease(userId, money);
  26. LOGGER.info("------->account-service中扣减账户余额结束");
  27. }
  28. }

(7) Controller

  1. package com.atguigu.cloudalibaba.controller;
  2. import com.atguigu.cloudalibaba.domain.CommonResult;
  3. import com.atguigu.cloudalibaba.service.AccountService;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RequestParam;
  6. import org.springframework.web.bind.annotation.RestController;
  7. import javax.annotation.Resource;
  8. import java.math.BigDecimal;
  9. @RestController
  10. public class AccountController {
  11. @Resource
  12. private AccountService accountService;
  13. /**
  14. * 扣减账户余额
  15. */
  16. @RequestMapping(value = "/account/decrease")
  17. public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
  18. accountService.decrease(userId, money);
  19. return new CommonResult(200, "扣减账户余额成功!");
  20. }
  21. }

(8) config配置

和2001一模一样

(9) 主启动类

  1. package com.atguigu.cloudalibaba;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  5. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  6. import org.springframework.cloud.openfeign.EnableFeignClients;
  7. @EnableDiscoveryClient
  8. @EnableFeignClients
  9. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
  10. public class SeataAccountMainApp2003 {
  11. public static void main(String[] args) {
  12. SpringApplication.run(SeataAccountMainApp2003.class, args);
  13. }
  14. }

启动测试

启动nacos、seata、2003;启动成功,成功注册进nacos
image.png
image.png

## 填坑高版本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文件。
image.png
image.png
注意版本对应关系,这里要使用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版本对应。

  1. <!--spring boot 2.3.2-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-dependencies</artifactId>
  5. <version>2.3.2.RELEASE</version>
  6. <type>pom</type>
  7. <scope>import</scope>
  8. </dependency>
  9. <!--spring cloud Hoxton.SR9-->
  10. <dependency>
  11. <groupId>org.springframework.cloud</groupId>
  12. <artifactId>spring-cloud-dependencies</artifactId>
  13. <version>Hoxton.SR9</version>
  14. <type>pom</type>
  15. <scope>import</scope>
  16. </dependency>
  17. <!--spring cloud alibaba 2.2.6.RELEASE-->
  18. <dependency>
  19. <groupId>com.alibaba.cloud</groupId>
  20. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  21. <version>2.2.6.RELEASE</version>
  22. <type>pom</type>
  23. <scope>import</scope>
  24. </dependency>

② 修改微服务模块seata依赖pom

官网上对seata依赖是这么描述的:
image.png
官网推荐依赖配置方式:
image.png
但是经过我测试发现,还是需要排除掉spring-cloud-starter-alibaba-seata里面的seata-all

  1. <!--seata-->
  2. <!--seata-spring-boot-starter 集成了seata-all,版本对应-->
  3. <dependency>
  4. <groupId>io.seata</groupId>
  5. <artifactId>seata-spring-boot-starter</artifactId>
  6. <version>1.4.2</version>
  7. </dependency>
  8. <dependency>
  9. <groupId>com.alibaba.cloud</groupId>
  10. <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  11. <!-- 因为兼容版本问题,所以需要剔除它自带的seata的包 -->
  12. <exclusions>
  13. <exclusion>
  14. <artifactId>seata-all</artifactId>
  15. <groupId>io.seata</groupId>
  16. </exclusion>
  17. <exclusion>
  18. <groupId>io.seata</groupId>
  19. <artifactId>seata-spring-boot-starter</artifactId>
  20. </exclusion>
  21. </exclusions>
  22. </dependency>

实际上我有微服务只配置了seata-spring-boot-starter依赖,spring-cloud-starter-alibaba-seata没有配置,并不影响正常使用。

③ 修改微服务application.yml

直接将seata的相关配置,配置到application.yml文件中,几个微服务的yml类似:

  1. # 这一块是配置seata的
  2. seata:
  3. enabled: true
  4. application-id: ${spring.application.name}
  5. enable-auto-data-source-proxy: true #是否开启数据源自动代理,默认为true
  6. tx-service-group: my_test_tx_group #要与config.txt配置文件中的vgroupMapping一致
  7. registry: #registry根据seata服务端的registry配置
  8. type: nacos #默认为file
  9. nacos:
  10. application: seata-server #配置自己的seata服务,与registry.conf一致
  11. server-addr: 127.0.0.1:8848 #根据自己的seata服务配置
  12. username: nacos #根据自己的seata服务配置
  13. password: nacos #根据自己的seata服务配置
  14. namespace: f0378218-b129-4fd8-839c-9bdfd010205b #根据自己的seata服务配置
  15. cluster: default # 配置自己的seata服务cluster, 默认为 default
  16. group: "SEATA_GROUP" #根据自己的seata服务配置
  17. config:
  18. type: nacos #配置中心设置为nacos,直接从nacos上获取配置
  19. nacos:
  20. server-addr: 127.0.0.1:8848 #配置自己的nacos地址
  21. group: SEATA_GROUP #配置自己的group,这里我配置跟registry.conf一样
  22. username: nacos #配置自己的username
  23. password: nacos #配置自己的password
  24. namespace: f0378218-b129-4fd8-839c-9bdfd010205b #根据自己的seata服务配置
  25. # dataId如果不用,就不需要配置
  26. # dataId: seataServer.properties #配置自己的dataId,由于搭建服务端时把客户端的配置也写在了seataServer.properties,所以这里用了和服务端一样的配置文件,实际客户端和服务端的配置文件分离出来更好
  27. # 这里是配置微服务的端口、注册到哪、数据源等等
  28. server:
  29. port: 2001
  30. spring:
  31. application:
  32. name: seata-order-service
  33. cloud:
  34. # alibaba:
  35. # seata:
  36. # #自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应
  37. # #vgroup_mapping.my_test_tx_group = "my_group"
  38. # tx-service-group: my_group
  39. nacos:
  40. discovery:
  41. server-addr: localhost:8848
  42. datasource:
  43. driver-class-name: com.mysql.cj.jdbc.Driver
  44. url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
  45. username: root
  46. password: 10086
  47. feign:
  48. hystrix:
  49. enabled: false
  50. logging:
  51. level:
  52. io:
  53. seata: info
  54. mybatis:
  55. mapperLocations: classpath:mapper/*.xml

注意对应关系:
image.png

④ 修改主启动类和DataSourceProxyConfig类

主启动类

主启动类加上@EnableAutoDataSourceProxy注解,这里以storage的微服务为例:

  1. package com.atguigu.cloudalibaba;
  2. import io.seata.spring.annotation.datasource.EnableAutoDataSourceProxy;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  6. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  7. import org.springframework.cloud.openfeign.EnableFeignClients;
  8. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
  9. @EnableDiscoveryClient
  10. @EnableFeignClients
  11. @EnableAutoDataSourceProxy
  12. public class StorageMainApp2002 {
  13. public static void main(String[] args) {
  14. SpringApplication.run(StorageMainApp2002.class, args);
  15. }
  16. }

配置类

使用@EnableAutoDataSourceProxy注解后,不再需要DataSourceProxyConfig配置数据源代理。强行写会报错。如果需要自己配置数据源代理的话,在application.yml中设置seata.enable-auto-data-source-proxyfalse,主启动类上去掉@EnableAutoDataSourceProxy注解即可。

参考博客

  1. Seata1.4.2+Nacos搭建使用
  2. Seata1.4.2整合SpringCloud H——Seata安装与搭建
  3. https://blog.csdn.net/ClearCiM/article/details/119953255
  4. spring cloud使用nacos和seata(windows环境)
  5. SEATA配合nacos使用

    四、测试

    Seata全局事务怎么使用

    Spring提供的本地事务:@Transactional
    Seata提供的全局事务:@GlobalTransactional
    image.png

    4.0 数据库初始情况

    下订单->减库存->扣余额->改(订单)状态
    image.png
    image.png
    image.png

    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 of t_order
    Connector/J 5.0.0以后的版本有一个名为useInformationSchema的数据库连接参数,Connector/J 在mysql8.0中默认配置连接属性useInformationSchema为true,使查询table信息时更为有效。用户依然可以配置useInformationSchema为false,但是在8.0.3及其之后的版本中,由于不能支持早期的特性,某些数据字典的查询可能会失败。
    useInformationSchema配置为false的时候,也可能会造成REMARKS信息(对应数据库中各字段的comment)的缺失。
    image.png
    在各微服务的application.yml 文件的spring.datasource.url 后面加上&useInformationSchema=false设置useInformationSchema为false,即可解决该问题。
    参考:https://www.jianshu.com/p/acc99f891e91

    再次测试

    访问成功
    image.png
    image.png
    数据库情况:
    image.png
    image.png
    image.png

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

    AccountServiceImpl添加超时:

    image.png
    我们使用的是Openfeign,默认超时时长是1s,这里我们延迟30s。

    报错超时异常:

    image.png

    数据库情况:

    image.png
    image.png
    image.png
    当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1;而且由于feign的重试机制,账户余额还有可能被多次扣减。

    4.3 测试超时异常:加@GlobalTransactional

    OrderServiceImpl添加@GlobalTransactional注解,注意改注解只能用在方法上!
    image.png
  • 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;

  1. @Resource
  2. private StorageService storageService;
  3. @Resource
  4. private AccountService accountService;
  5. /**
  6. * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
  7. * 简单说:
  8. * 下订单->减库存->减余额->改状态
  9. */
  10. @Override
  11. //全局事务,发生异常进行回滚
  12. @GlobalTransactional(name = "lsp-create-order", rollbackFor = Exception.class)
  13. public void create(Order order) {
  14. log.info("------->下单开始");
  15. //本应用创建订单
  16. orderDao.create(order);
  17. //远程调用库存服务扣减库存
  18. log.info("------->订单微服务调用库存微服务,扣减库存开始");
  19. storageService.decrease(order.getProductId(),order.getCount());
  20. log.info("------->订单微服务调用库存微服务,扣减库存结束");
  21. //远程调用账户服务扣减余额
  22. log.info("------->订单微服务调用账户微服务,扣减余额开始");
  23. accountService.decrease(order.getUserId(),order.getMoney());
  24. log.info("------->订单微服务调用账户微服务,减余额结束");
  25. //修改订单状态为已完成
  26. log.info("------->order-service中修改订单状态开始");
  27. // 这里的话是不是应该是orderId?
  28. orderDao.update(order.getUserId(),0);
  29. log.info("------->order-service中修改订单状态结束");
  30. log.info("------->下单结束");
  31. }

}

  1. 测试:<br />依然超时异常<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636888529834-49d19c5c-0783-40b5-9200-801af4a1135e.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=184&id=uf8202d2f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=184&originWidth=798&originalType=binary&ratio=1&rotation=0&showTitle=false&size=21998&status=done&style=none&taskId=udb7a0d1d-d084-4483-b0e6-29c7dba4fdc&title=&width=798)<br />数据库:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636888594617-08abffce-914c-4c7e-812f-821a487cdac0.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=92&id=u9e46f3c4&margin=%5Bobject%20Object%5D&name=image.png&originHeight=92&originWidth=469&originalType=binary&ratio=1&rotation=0&showTitle=false&size=4739&status=done&style=none&taskId=uef7e8039-ae93-4e73-8ae1-21709b20108&title=&width=469)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636888612571-33a3fca6-5c67-4c45-ab54-07cb3beee91e.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=100&id=u3d837bc8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=100&originWidth=402&originalType=binary&ratio=1&rotation=0&showTitle=false&size=6528&status=done&style=none&taskId=u485865ce-c6a4-42a9-8136-0086181b8c3&title=&width=402)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636888631676-3966be70-025d-446f-99bc-77d1872131b9.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=64&id=u40a1aa7d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=64&originWidth=377&originalType=binary&ratio=1&rotation=0&showTitle=false&size=3167&status=done&style=none&taskId=ued544d02-432d-4b63-8e01-94cd573f0dc&title=&width=377)<br />我们发现数据库中的数据根本就没有变化,记录都添加不进来,说明回滚成功!
  2. <a name="SkeWk"></a>
  3. ## 4.4 小结
  4. 做好配置后,我们只需要使用一个 @GlobalTransactional(name = "lsp-create-order", rollbackFor = Exception.class) 放在业务的入口,即可实现控制全局的事务。注意该注解只能放在方法上。
  5. <a name="GspoX"></a>
  6. # 五、补充说明
  7. SeataSimple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架<br />0.9不支持集群,生产环境请使用1.0以上的版本。
  8. <a name="H2b0X"></a>
  9. ## 5.0 undo_log表的作用
  10. 模块内方法也可以加@Transactional注解,如果一个模块的事务提交了,Seata会把提交了哪些数据记录到undo_log表中,如果这时TC通知全局事务回滚,那么RM就从undo_log表中获取之前修改了哪些资源,并根据这个表回滚。(有待考证)
  11. <a name="nFdla"></a>
  12. ## 5.1 再看TC/TM/RM三大组件
  13. TCseata服务器; (我们电脑上启动的seata )<br />TM:事物的发起者,业务的入口。 哪个微服务使用了**@GlobalTransactional**哪个就是TM<br />RM:事务的参与者,一个数据库就是一个RM。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636890980594-f9db0624-1c09-4055-8c65-5c087d16b36f.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=459&id=u58fcff01&margin=%5Bobject%20Object%5D&name=image.png&originHeight=459&originWidth=960&originalType=binary&ratio=1&rotation=0&showTitle=false&size=79099&status=done&style=none&taskId=u5e641c32-37ed-4a66-99be-60e1cdbb7dd&title=&width=960)
  14. 分布式事务的执行流程:
  15. 1. TM 开启分布式事务(TM TC 注册全局事务记录);
  16. 1. 按业务场景,编排数据库、服务等事务内资源(RM TC 汇报资源准备状态 );
  17. 1. TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
  18. 1. TC 汇总事务信息,决定分布式事务是提交还是回滚;
  19. 1. TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。
  20. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636890679040-20e2d483-549c-459a-b481-693caab0d239.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=513&id=ufe647090&margin=%5Bobject%20Object%5D&name=image.png&originHeight=513&originWidth=736&originalType=binary&ratio=1&rotation=0&showTitle=false&size=166850&status=done&style=none&taskId=u090a7aae-0a0e-4b9b-b3b0-90058cc5773&title=&width=736)
  21. <a name="bn5kX"></a>
  22. ## 5.2 AT模式(默认)如何做到对业务的无侵入
  23. Seata有四大模式:AT(默认)、TCCSAGAXA。(阿里云上的AT叫做GTS,收费)<br />[AT模式](http://seata.io/zh-cn/docs/dev/mode/at-mode.html)<br />AT模式两阶段提交协议的演变:
  24. - 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  25. - 二阶段:
  26. - 提交异步化,非常快速地完成。
  27. - 回滚通过一阶段的回滚日志进行反向补偿(前面insert,后面回滚时就delete)。
  28. 每个数据库除了自身存储数据的表以外,都会有一个事务回滚表:undo_log<br />Seata库中存在:branch_table\global_table\lock_table\distributed_lock(高版本才有)这样一些表
  29. <a name="V317k"></a>
  30. ### 5.2.1 一阶段加载
  31. 在一阶段,Seata 会拦截“业务 SQL”,<br />1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”(前置镜像)<br />2 执行“业务 SQL”更新业务数据,在业务数据更新之后,<br />3 其保存成“after image”,最后生成行锁。<br />以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636892157973-e4a5ccaf-71c5-481e-9417-6b64d389f36f.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=601&id=u5c39c17b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=601&originWidth=984&originalType=binary&ratio=1&rotation=0&showTitle=false&size=206818&status=done&style=none&taskId=u571673f9-3642-427f-b2fb-5de8e6c99c6&title=&width=984)
  32. <a name="EyIuK"></a>
  33. ### 5.2.2 二阶段提交
  34. 因为“业务 SQL”在**一阶段**已经提交至数据库,**二阶段如果顺利提交的话**,那么Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636892304290-bd0f0263-9336-4877-bf03-59f5f86cfe87.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=367&id=u069af116&margin=%5Bobject%20Object%5D&name=image.png&originHeight=367&originWidth=822&originalType=binary&ratio=1&rotation=0&showTitle=false&size=121031&status=done&style=none&taskId=u56352b59-a6b2-44cc-827f-41e3bf5458d&title=&width=822)
  35. <a name="A735i"></a>
  36. ### 5.2.3 二阶段回滚
  37. 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。<br />回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 after image”。如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636892634620-1e6c1596-baba-4aed-b840-8afdfd320e05.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=679&id=u175db16e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=679&originWidth=997&originalType=binary&ratio=1&rotation=0&showTitle=false&size=203546&status=done&style=none&taskId=uc93c576b-2627-4e67-95fd-4c49c735c9d&title=&width=997)
  38. <a name="qGlrj"></a>
  39. ## 5.3 debug查看流程
  40. 最开是seata库中的三张表是没有数据的<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636892970332-138694e4-498e-4c57-82a3-9ed40feea4df.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=268&id=u4db4e633&margin=%5Bobject%20Object%5D&name=image.png&originHeight=268&originWidth=944&originalType=binary&ratio=1&rotation=0&showTitle=false&size=281557&status=done&style=none&taskId=ucc55c48e-80ba-4be1-b254-13e5eb1c24f&title=&width=944)<br />2003打上断点,debug启动<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636892865851-b541d0b7-3e91-463f-9bd2-628c25379099.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=498&id=uc2c6fc13&margin=%5Bobject%20Object%5D&name=image.png&originHeight=498&originWidth=807&originalType=binary&ratio=1&rotation=0&showTitle=false&size=65103&status=done&style=none&taskId=u4d70f6ee-ee9c-4d1c-9db4-3fe91a6c172&title=&width=807)<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 />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636893040133-6768d513-0c8a-4d32-a308-35c88331fce8.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=404&id=u113532da&margin=%5Bobject%20Object%5D&name=image.png&originHeight=404&originWidth=1034&originalType=binary&ratio=1&rotation=0&showTitle=false&size=54494&status=done&style=none&taskId=u00650e28-d916-4fc5-a5ec-cd626e7accc&title=&width=1034)<br />此时seata库中的三个表都是有数据的:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636893186504-d9e4f100-7ad0-4235-b27c-9b0565fefc9a.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=362&id=u729d2653&margin=%5Bobject%20Object%5D&name=image.png&originHeight=362&originWidth=1048&originalType=binary&ratio=1&rotation=0&showTitle=false&size=74446&status=done&style=none&taskId=u0a40e5eb-58e6-48b3-8638-52821e232bf&title=&width=1048)
  41. 看一下branch_table,记录了各个RM的信息,分别对应orderstorageaccount三个微服务<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636893708587-5f6e35c2-bed9-4c95-9927-6c3c45dd1856.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=235&id=uaba407e0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=235&originWidth=1654&originalType=binary&ratio=1&rotation=0&showTitle=false&size=54582&status=done&style=none&taskId=u48476a88-324d-4a09-acd9-334b8113eeb&title=&width=1654)<br />可以看到xid跟global_table中的xid一致。
  42. 再看global_table:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636893900347-9735778c-ccf0-41a4-8b31-701541eb2db2.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=123&id=ua20c31e2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=123&originWidth=1560&originalType=binary&ratio=1&rotation=0&showTitle=false&size=22754&status=done&style=none&taskId=u00e0294a-1f8b-458d-afa9-64724ffd8ea&title=&width=1560)
  43. 查看lock_table:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636894052476-c9a96333-e331-4d37-bdd1-28d92feadb68.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=239&id=ubcd38560&margin=%5Bobject%20Object%5D&name=image.png&originHeight=239&originWidth=976&originalType=binary&ratio=1&rotation=0&showTitle=false&size=32413&status=done&style=none&taskId=ud863358d-8b42-446b-a674-93297285fb9&title=&width=976)
  44. 查看各业务中的undo_log表:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22423156/1636894224215-644fbe92-cec7-4f64-b4d5-9a3aab5daf85.png#clientId=ueace7816-0779-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=154&id=uca91e18f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=154&originWidth=1497&originalType=binary&ratio=1&rotation=0&showTitle=false&size=24444&status=done&style=none&taskId=u3f24034e-d2e9-4626-9b51-923981e3729&title=&width=1497)<br />rollback_info是JSON字符串,存储了beforeimage、afterimage:
  45. ```json
  46. {
  47. "@class": "io.seata.rm.datasource.undo.BranchUndoLog",
  48. "xid": "192.168.190.1:8091:2090602861",
  49. "branchId": 2090602864,
  50. "sqlUndoLogs": [
  51. "java.util.ArrayList",
  52. [
  53. {
  54. "@class": "io.seata.rm.datasource.undo.SQLUndoLog",
  55. "sqlType": "INSERT",
  56. "tableName": "`t_order`",
  57. "beforeImage": {
  58. "@class": "io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords",
  59. "tableName": "`t_order`",
  60. "rows": [
  61. "java.util.ArrayList",
  62. []
  63. ]
  64. },
  65. "afterImage": {
  66. "@class": "io.seata.rm.datasource.sql.struct.TableRecords",
  67. "tableName": "`t_order`",
  68. "rows": [
  69. "java.util.ArrayList",
  70. [
  71. {
  72. "@class": "io.seata.rm.datasource.sql.struct.Row",
  73. "fields": [
  74. "java.util.ArrayList",
  75. [
  76. {
  77. "@class": "io.seata.rm.datasource.sql.struct.Field",
  78. "name": "id",
  79. "keyType": "PrimaryKey",
  80. "type": -5,
  81. "value": [
  82. "java.lang.Long",
  83. 10
  84. ]
  85. },
  86. {
  87. "@class": "io.seata.rm.datasource.sql.struct.Field",
  88. "name": "user_id",
  89. "keyType": "NULL",
  90. "type": -5,
  91. "value": [
  92. "java.lang.Long",
  93. 1
  94. ]
  95. },
  96. {
  97. "@class": "io.seata.rm.datasource.sql.struct.Field",
  98. "name": "product_id",
  99. "keyType": "NULL",
  100. "type": -5,
  101. "value": [
  102. "java.lang.Long",
  103. 1
  104. ]
  105. },
  106. {
  107. "@class": "io.seata.rm.datasource.sql.struct.Field",
  108. "name": "count",
  109. "keyType": "NULL",
  110. "type": 4,
  111. "value": 10
  112. },
  113. {
  114. "@class": "io.seata.rm.datasource.sql.struct.Field",
  115. "name": "money",
  116. "keyType": "NULL",
  117. "type": 3,
  118. "value": [
  119. "java.math.BigDecimal",
  120. 100
  121. ]
  122. },
  123. {
  124. "@class": "io.seata.rm.datasource.sql.struct.Field",
  125. "name": "status",
  126. "keyType": "NULL",
  127. "type": 4,
  128. "value": 0
  129. }
  130. ]
  131. ]
  132. }
  133. ]
  134. ]
  135. }
  136. }
  137. ]
  138. ]
  139. }

查看seata_storage库中的undo_log表的roobal_info信息,可以看到beforeimage和afterimage分别保存了修改前后的信息。
image.png

debug放行,seata库中表中的中间数据和undo_log表的数据都删除了。(我的seata_account表的undo_log中没有被删除,等了半天也没有。)异步任务阶段的分支提交请求将异步和批量地删除相应的undo_log记录。
发现account2003微服务的日志跟2001和2002都不一样
image.png
image.png
image.png

5.4 整体流程图

image.png