美文网首页JavaEE
SpringCloud之AlibabaSeata(分布式事务组件

SpringCloud之AlibabaSeata(分布式事务组件

作者: 平安喜乐698 | 来源:发表于2022-11-14 13:05 被阅读0次
  1. Seata简介
  2. 下载Seata服务器
  3. 示例(在项目中集成Seata)

处理分布式(微服务)架构的事务问题。

例(用户在某电商系统下单购买了一件商品,电商系统会执行下4步)
  1. 调用订单服务创建订单数据;
  2. 调用库存服务扣减库存;
  3. 调用账户服务扣减账户金额;
  4. 最后调用订单服务修改订单状态;
在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于单个服务的数据一致性可以交由其自身数据库事务来保证,但对于整个分布式微服务架构,各服务自身的事务特性无法保证全局数据的正确性和一致性(要么全部成功,要么全部失败),这需要通过分布式事务框架(如:Seata)来解决。
  1. Seata简介
Seata(分布式事务框架)
  由阿里巴巴和蚂蚁金服共同开发的开源分布式事务解决方案。
  发展历程
    2014年发布TXC(Taobao Transaction Constructor),为内部应用提供分布式事务服务。
    2016年TXC改造为GTS(Global Transaction Service),作为阿里云的云上分布式事务解决方案,为外部客户提供服务。
    2019年(基于TXC和GTS)创建了开源项目Fescar(Fast & EaSy Commit And Rollback, FESCAR)分布式事务解决方案。Fescar被重命名为了Seata(simple extensiable autonomous transaction architecture)。

分布式事务相关概念
  1. 事务:由一组操作构成的可靠、独立的工作单元,事务具备ACID特性(即:原子性、一致性、隔离性、持久性)。
  2. 本地事务:本地事务由本地资源管理器(通常指数据库管理系统DBMS,如:MySQL、Oracle等)管理,严格支持ACID特性。本地事务不具备分布式事务的处理能力,隔离的最小单位受限于资源管理器,即本地事务只能对自己数据库的操作进行控制,对于其他数据库的操作则无能为力。
  3. 全局事务/分布式事务:协调其管辖的各个分支事务达成一致(要么一起成功提交,要么一起失败回滚)。
  4. 分支事务:受全局事务管辖和协调的本地事务。

Seata对分布式事务的协调和控制,是通过XID和3个相互协作的核心组件(TC、TM、RM)实现的。
  TC以Seata服务器形式独立部署,TM、RM则以SeataClient形式集成在微服务中运行。
  1. XID(全局事务的唯一标识)
    可以在服务的调用链路中传递,绑定到服务的事务上下文中。
  2. TC(Transaction Coordinator事务协调器)
    事务的协调者(即Seata服务器),负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
  3. TM(Transaction Manager事务管理器)
    事务的发起者,负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
  4. RM(Resource Manager资源管理器)
    资源的管理者(各服务使用的数据库),负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。

工作流程:
  1. TM向TC申请开启一个全局事务,全局事务创建成功后,TC会针对这个全局事务生成一个全局唯一的 XID并返回给TM;
  2. XID通过服务的调用链传递到其他服务;
  3. RM向TC注册一个分支事务,并将其纳入XID对应全局事务的管辖;
  4. TM根据TC收集的各个分支事务的执行结果,向TC发起全局事务提交或回滚决议;
  5. TC调度XID下管辖的所有分支事务完成提交或回滚操作。
Seata的工作流程
Seata提供了4种事务模式(快速有效地对分布式事务进行控制):AT(使用最多,无业务入侵,使开发员更专注于业务逻辑开发)、TCC、SAGA、XA 。

AT模式的前提
    项目必须是使用JDBC访问(支持本地ACID事务特性的)关系型数据库(如:MySQL、Oracle)的JAVA应用。
    此外,还需要针对业务中涉及的各个数据库表,分别创建一个UNDO_LOG(回滚日志)表(不同数据库创建UNDO_LOG表时会略有不同)。例(MySQL下创建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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

AT模式的工作机制(两个阶段)
  假设某数据库中存在一张名为webset的表:
    1. id bigint(20) 主键
    2. name varchar(255)     
    3. url varchar(255)
  在某次分支事务中,需要在webset表中执行以下操作
    update webset set url = 'www.baidu.com' where name = '百度';
一阶段:
  1. 获取SQL的基本信息:Seata拦截并解析业务SQL,得到SQL的操作类型(UPDATE)、表名(webset)、判断条件(where name = '百度')等相关信息。
  2. 查询前镜像:根据得到的业务 SQL 信息,生成“前镜像查询语句”。 
    select id,name,url from webset where  name='百度';
    执行“前镜像查询语句”,得到即将执行操作的数据,并将其保存为“前镜像数据(beforeImage)”。
  3. 执行业务SQL(update webset set url = 'www.baidu.com' where name = '百度';)。
  4. 查询后镜像:根据“前镜像数据”的主键(id : 1),生成“后镜像查询语句”。 
    select id,name,url from webset where  id= 1;
    执行“后镜像查询语句”,得到执行业务操作后的数据,并将其保存为“后镜像数据(afterImage)”。
  5. 插入回滚日志:将前后镜像数据和业务SQL的信息组成一条回滚日志记录,插入到UNDO_LOG表中。
  6. 注册分支事务,生成行锁:在这次业务操作的本地事务提交前,RM会向TC注册分支事务,并针对主键id为1的记录生成行锁。
  以上所有操作均在同一个数据库事务内完成,可以保证一阶段的操作的原子性。
  7. 本地事务提交:将业务数据的更新和前面生成的UNDO_LOG一并提交。
  8. 上报执行结果:将本地事务提交的结果上报给TC。
二阶段:提交
  当所有的RM都将自己分支事务的提交结果上报给TC后,TM根据TC收集的各个分支事务的执行结果,来决定向TC发起全局事务的提交或回滚。
  若所有分支事务都执行成功,TM向TC发起全局事务的提交,并批量删除各个RM保存的UNDO_LOG记录和行锁;否则全局事务回滚。 
二阶段:回滚
  若全局事务中的任何一个分支事务失败,则TM向TC发起全局事务的回滚,并开启一个本地事务,执行如下操作。
  1. 查找UNDO_LOG记录:通过XID和分支事务 ID(Branch ID) 查找所有的UNDO_LOG记录。
  2. 数据校验:将UNDO_LOG中的后镜像数据(afterImage)与当前数据进行比较,如果有不同,则说明数据被当前全局事务之外的动作所修改,需要人工对这些数据进行处理。
  3. 生成回滚语句:根据UNDO_LOG中的前镜像(beforeImage)和业务 SQL 的相关信息生成回滚语句:update webset set url= 'www.baidu.com' where id = 1;
  4. 还原数据:执行回滚语句,并将前镜像数据、后镜像数据以及行锁删除。
  5. 提交事务:提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给TC。
Seata AT 模式一阶段
  1. 下载Seata服务器

从github下载Seata服务器(压缩包+SourceCode)

Seata服务器的目录说明:
  1. bin目录(存放:可执行命令)
  2. conf目录(存放:配置文件)
  3. lib目录(存放:依赖Jar包)
  4. logs目录(存放:日志)
  1. Seata配置:配置中心(存放配置文件,客户端可以根据需要获取指定的配置文件)
支持:
  file (本地文件,包含 conf、properties、yml等配置文件)、nacos、consul、apollo、etcd、zookeeper等多种配置中心。

Seata整合Nacos配置中心
  1. SeataServer(Seata服务器)配置
  将SeataServer的config目录下的registry.conf文件中的config.type配置方式改为Nacos,并对Nacos配置中心的相关信息进行配置:
    config {
      #配置方式
      type = "nacos"
      nacos {
        #nacos服务器地址
        serverAddr = "127.0.0.1:1111"
        #配置中心的命名空间
        namespace = ""
        #配置中心所在的分组
        group = "SEATA_GROUP"
        #Nacos配置中心的用户名
        username = "nacos"
        #Nacos配置中心的密码
        password = "nacos"
      }
    }
  2. SeataClient(即微服务架构中的服务)配置
    1. 在pom.xml文件添加依赖
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>最新版</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
        <version>1.2.0及以上版本</version>
    </dependency>
    如果是SpringCloud项目中,通常只需要添加 
    <!--引入 seata 依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    2. 在application.yml配置文件对Nacos配置中心进行配置
seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111 # Nacos 配置中心的地址
      group : "SEATA_GROUP"  #分组
      namespace: ""
      username: "nacos"   #Nacos 配置中心的用于名
      password: "nacos"  #Nacos 配置中心的密码
  3. 上传配置到Nacos配置中心
    1. 根据需要修改SeataServer源码/script/config-center目录中的config.txt。  
    2. 跳转到SeataServer源码/script/config-center/nacos目录(确保 Nacos 服务器已启动)
      Linux:直接终端执行如下命令:
      Windows:右键鼠标选择Git Bush Here,并在弹出的 Git 命令窗口中执行如下命令:
        sh nacos-config.sh -h 127.0.0.1 -p 1111  -g SEATA_GROUP -u nacos -w nacos命令。
        说明:
          -h:Nacos 的 host,默认取值为 localhost
          -p:端口号,默认取值为 8848
          -g:Nacos 配置的分组,默认取值为 SEATA_GROUP
          -u:Nacos 用户名
          -w:Nacos 密码
  4. 验证Nacos配置中心
    启动NacosServer,登陆Nacos控制台(http://localhost:1111/nacos/)查看配置列表。
  1. Seata配置:服务注册中心(存放:服务与服务地址的映射关系)
支持: 
  file (直连)、eureka、consul、nacos、etcd、zookeeper、sofa、redis等多种服务注册中心。

Seata整合Nacos注册中心
  1. SeataServer配置
    将SeataServer的config目录下的registry.conf文件的registry.type注册方式改为Nacos,并对Nacos注册中心的相关信息进行配置:
    registry {
      #注册方式
      type = "nacos"
      nacos {
        application = "seata-server"
        # 修改 nacos 注册中心的地址
        serverAddr = "127.0.0.1:1111"
        group = "SEATA_GROUP"
        namespace = ""
        cluster = "default"
        username = "nacos"
        password = "nacos"
      }
    }
  2. SeataClient配置
    1. 在pom.xml文件添加依赖(同上)
    2. 在application.yml配置文件中对Nacos注册中心进行配置
seata:
  registry:
    type: nacos
    nacos:
      application: seata-server  
      server-addr: 127.0.0.1:1111 # Nacos 注册中心的地址
      group : "SEATA_GROUP" #分组
      namespace: ""   
      username: "nacos"   #Nacos 注册中心的用户名
      password: "nacos"   # Nacos 注册中心的密码
  3. 验证Nacos注册中心
    启动NacosServer、SeataServer,登录Nacos控制台查看服务列表。
  1. Seata事务分组
Seata通过事务分组查找获取TC服务,流程如下:
    1. 在应用中配置事务分组。
    2. 应用通过配置中心去查找配置:service.vgroupMapping.{事务分组} 获取TC集群名。
    3. 获得集群名称后,应用通过一定的前后缀 + 集群名称去构造服务名。
    4. 得到服务名后,去注册中心去拉取服务列表,获得后端真实的 TC 服务列表。

1. SeataServer配置
  在SeataServer的config目录下的registry.conf文件的nacos中添加cluster = "com.sst.cx"设置TC集群名。
2. SeataClient配置
  在application.yml中配置
spring:
     alibaba:
      seata:
        tx-service-group: service-order-group  #事务分组名,默认值为:服务名-fescar-service-group
seata:
  registry:
    type: nacos   #从 Nacos 获取 TC 服务
    nacos:
      server-addr: 127.0.0.1:1111
  config:
    type: nacos   #使用 Nacos 作为配置中心
    nacos:
      server-addr: 127.0.0.1:1111
3. 上传配置到Nacos
  修改SeataServer源码/script/config-center目录中的config.txt(事务分组名service-order-group、TC集群名com.sst.cx):
    service.vgroupMapping.service-order-group=com.sst.cx
4.  获取事务分组
  1. 依次启动Nacos集群、SeataServer、SeataClient。
  2. SeataClient在启动时,会根据application.yml中的spring.cloud.alibaba.seata.tx-service-group获取事务分组名:service-order-group。
5. 获取TC集群名
  使用事务分组名“service-order-group”拼接成“service.vgroupMapping.service-order-group”,并从 Nacos配置中心获取该配置的取值(TC集群名):com.sst.cx。
6. 查找TC服务
  根据TC集群名、Nacos注册中心的地址(server-addr)以及命名空间(namespace),在Nacos注册中心找到真实的TC服务列表。
  1. 启动SeataServer(使用db模式)配置:服务注册中心、配置中心、事务分组
SeataServer的3种存储模式(store.mode):
  1. file(文件存储模式)默认
    单机模式,全局事务的会话信息在内存中读写,并持久化本地文件 root.data,性能较高。
  2. db(数据库存储模式)
    高可用模式,全局事务会话信息通过数据库共享,性能较低。     
    需要建数据库表。
  3. redis(缓存处处模式)
    Seata Server 1.3 及以上版本支持该模式,性能较高,但存在事务信息丢失风险。
    需要配置 redis 持久化配置。

1. 建表
在db模式下,需要针对全局事务的会话信息创建以下3张数据库表(在seata数据库下)。
  1. 全局事务表,对应的表为:global_table
  2. 分支事务表,对应的表为:branch_table
  3. 全局锁表,对应的表为:lock_table
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(256),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

2. 配置SeataServer
  将SeataServer的conf目录下的registry.conf文件的registry.type服务注册方式和config.type配置方式改为Nacos。
registry {
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:1111"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
  }
}
config {
  type = "nacos"
  nacos {
    serverAddr = "127.0.0.1:1111"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
}

3. 将Seata配置上传到Nacos
  修改SeataServer源码/script/config-center目录下的config.txt,并上传到Nacos。
#将 Seata Server 的存储模式修改为 db
store.mode=db
# 数据库驱动
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 数据库 url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&characterEncoding=UTF-8&useUnicode=true&serverTimezone=UTC 
# 数据库的用户名
store.db.user=root 
# 数据库的密码
store.db.password=12345678
# 自定义事务分组
service.vgroupMapping.service-order-group=default 
service.vgroupMapping.service-storage-group=default
service.vgroupMapping.service-account-group=default 

4. 启动Seata Server
  Windows:双击SeataServer的bin目录下的seata-server.bat启动脚本。
  Linux:sh seata-server.sh
  1. 示例(在业务中集成Seata)
电商系统中用户下单购买一件商品,涉及到3个微服务(分别使用各自的数据库)
  1. Order(订单服务):创建和修改订单。
  2. Storage(库存服务):对指定的商品扣除仓库库存。
  3. Account(账户服务) :从用户帐户中扣除商品金额。
服务调用步骤如下:
  1. 调用Order服务,创建一条订单数据,订单状态为“未完成”;
  2. 调用Storage服务,扣减商品库存;
  3. 调用Account服务,从用户账户中扣除商品金额;
  4. 调用Order服务,将订单状态修改为“已完成”。
服务调用
===》1. 创建订单(Order)服务
sql
create database seata_order character set utf8 collate utf8_general_ci;
use seata_order;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `product_id` bigint DEFAULT NULL COMMENT '产品id',
  `count` int DEFAULT NULL COMMENT '数量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金额',
  `status` int DEFAULT NULL COMMENT '订单状态:0:未完成;1:已完结',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
  `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

创建spring-cloud-alibaba-seata-order-8005子项目
1. 修改pom.xml文件
    <dependencies>
        <!--nacos 服务注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入 OpenFeign 的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>
        <!--引入 seata 依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.sst.cx</groupId>
            <artifactId>spring-cloud-alibaba-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!--添加 Spring Boot 的监控模块-->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--配置中心 nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!--mybatis自动生成代码插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <configurationFile>src/main/resources/mybatis-generator/generatorConfig.xml</configurationFile>
                    <verbose>true</verbose>
                    <!-- 是否覆盖,true表示会替换生成的JAVA文件,false则不覆盖 -->
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <!--mysql驱动包-->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.19</version>
                    </dependency>
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.4.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
2. 创建bootstrap.yml配置文件(类路径resources目录下)
spring:
  cloud:
    ## Nacos认证信息
    nacos:
      config:
        username: nacos
        password: nacos
        context-path: /nacos
        server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
        namespace:   # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3. 创建application.yml配置文件(类路径resources目录下)
spring:
  application:
    name: spring-cloud-alibaba-seata-order-8005 #服务名
  #数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动
    name: defaultDataSource
    url: jdbc:mysql://localhost:3306/seata_order?serverTimezone=UTC #数据库连接地址
    username: root  #数据库的用户名
    password: 12345678  #数据库密码
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:1111 #nacos 服务器地址
        namespace: public #nacos 命名空间
        username:
        password:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080  #Sentinel 控制台地址
        port: 8719
    alibaba:
      seata:
        #自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
        tx-service-group: service-order-group
server:
  port: 8005  #端口
seata:
  application-id: ${spring.application.name}
  #自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
  tx-service-group: service-order-group
  service:
    grouplist:
      #Seata 服务器地址
      seata-server: 127.0.0.1:8091
  # Seata 的注册方式为 nacos
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
  # Seata 的配置中心为 nacos
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
feign:
  sentinel:
    enabled: true #开启 OpenFeign 功能
management:
  endpoints:
    web:
      exposure:
        include: "*"
#### MyBatis 配置 ######
mybatis:
  # 指定 mapper.xml 的位置
  mapper-locations: classpath:mybatis/mapper/*.xml
  #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
  type-aliases-package: com.sst.cx.domain
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true
4. 创建Order.java
package com.sst.cx.domain;
import java.math.BigDecimal;
public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Long getUserId() {
        return userId;
    }
    public void setUserId(Long userId) {
        this.userId = userId;
    }
    public Long getProductId() {
        return productId;
    }
    public void setProductId(Long productId) {
        this.productId = productId;
    }
    public Integer getCount() {
        return count;
    }
    public void setCount(Integer count) {
        this.count = count;
    }
    public BigDecimal getMoney() {
        return money;
    }
    public void setMoney(BigDecimal money) {
        this.money = money;
    }
    public Integer getStatus() {
        return status;
    }
    public void setStatus(Integer status) {
        this.status = status;
    }
}
5. 创建OrderMapper.java
package com.sst.cx.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.sst.cx.domain.Order;
@Mapper
public interface OrderMapper {
    int deleteByPrimaryKey(Long id);
    int insert(Order record);
    int create(Order order);
    int insertSelective(Order record);
    // 修改订单状态,从零改为1
    void update(@Param("userId") Long userId, @Param("status") Integer status);
    Order selectByPrimaryKey(Long id);
    int updateByPrimaryKeySelective(Order record);
    int updateByPrimaryKey(Order record);
}
6. 创建OrderMapper.xml(resouces/mybatis/mapper 目录下)
<?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.sst.cx.mapper.OrderMapper">
    <resultMap id="BaseResultMap" type="Order">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="user_id" jdbcType="BIGINT" property="userId"/>
        <result column="product_id" jdbcType="BIGINT" property="productId"/>
        <result column="count" jdbcType="INTEGER" property="count"/>
        <result column="money" jdbcType="DECIMAL" property="money"/>
        <result column="status" jdbcType="INTEGER" property="status"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , user_id, product_id, count, money, status
    </sql>
    <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from t_order
        where id = #{id,jdbcType=BIGINT}
    </select>
    <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
        delete
        from t_order
        where id = #{id,jdbcType=BIGINT}
    </delete>
    <insert id="insert" parameterType="Order">
        insert into t_order (id, user_id, product_id,
                             count, money, status)
        values (#{id,jdbcType=BIGINT}, #{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},
                #{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER})
    </insert>
    <insert id="insertSelective" parameterType="Order">
        insert into t_order
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="id != null">
                id,
            </if>
            <if test="userId != null">
                user_id,
            </if>
            <if test="productId != null">
                product_id,
            </if>
            <if test="count != null">
                count,
            </if>
            <if test="money != null">
                money,
            </if>
            <if test="status != null">
                status,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="id != null">
                #{id,jdbcType=BIGINT},
            </if>
            <if test="userId != null">
                #{userId,jdbcType=BIGINT},
            </if>
            <if test="productId != null">
                #{productId,jdbcType=BIGINT},
            </if>
            <if test="count != null">
                #{count,jdbcType=INTEGER},
            </if>
            <if test="money != null">
                #{money,jdbcType=DECIMAL},
            </if>
            <if test="status != null">
                #{status,jdbcType=INTEGER},
            </if>
        </trim>
    </insert>
    <insert id="create" parameterType="Order">
        insert into t_order (user_id, product_id,
                             count, money, status)
        values (#{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},
                #{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER})
    </insert>
    <update id="updateByPrimaryKeySelective" parameterType="Order">
        update t_order
        <set>
            <if test="userId != null">
                user_id = #{userId,jdbcType=BIGINT},
            </if>
            <if test="productId != null">
                product_id = #{productId,jdbcType=BIGINT},
            </if>
            <if test="count != null">
                count = #{count,jdbcType=INTEGER},
            </if>
            <if test="money != null">
                money = #{money,jdbcType=DECIMAL},
            </if>
            <if test="status != null">
                status = #{status,jdbcType=INTEGER},
            </if>
        </set>
        where id = #{id,jdbcType=BIGINT}
    </update>
    <update id="updateByPrimaryKey" parameterType="Order">
        update t_order
        set user_id    = #{userId,jdbcType=BIGINT},
            product_id = #{productId,jdbcType=BIGINT},
            count      = #{count,jdbcType=INTEGER},
            money      = #{money,jdbcType=DECIMAL},
            status     = #{status,jdbcType=INTEGER}
        where id = #{id,jdbcType=BIGINT}
    </update>
    <update id="update">
        update t_order
        set status = 1
        where user_id = #{userId}
          and status = #{status};
    </update>
</mapper>
7. 创建OrderService.java
package com.sst.cx.service;
import com.sst.cx.domain.*;
public interface OrderService {
    // 创建订单数据
    CommonResult create(Order order);
}
8. 创建StorageService.java
package com.sst.cx.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.sst.cx.domain.CommonResult;
@FeignClient(value = "spring-cloud-alibaba-seata-storage-8006")
public interface StorageService {
    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
9.  创建AccountService.java
package com.sst.cx.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
import com.sst.cx.domain.CommonResult;
@FeignClient(value = "spring-cloud-alibaba-seata-account-8007")
public interface AccountService {
    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
10. 创建OrderServiceImpl.java
package com.sst.cx.service.impl;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import com.sst.cx.mapper.OrderMapper;
import com.sst.cx.domain.*;
import com.sst.cx.service.*;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;
    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:下订单->扣库存->减余额->改订单状态
     */
    @Override
    @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
    public CommonResult create(Order order) {
        log.info("----->开始新建订单");
        // 1 新建订单
        order.setUserId(new Long(1));
        order.setStatus(0);
        orderMapper.create(order);
        // 2 扣减库存
        log.info("----->订单服务开始调用库存服务,开始扣减库存");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("----->订单微服务开始调用库存,扣减库存结束");
        // 3 扣减账户
        log.info("----->订单服务开始调用账户服务,开始从账户扣减商品金额");
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("----->订单微服务开始调用账户,账户扣减商品金额结束");
        // 4 修改订单状态,从零到1,1代表已经完成
        log.info("----->修改订单状态开始");
        orderMapper.update(order.getUserId(), 0);
        log.info("----->修改订单状态结束");
        log.info("----->下订单结束了------->");
        return new CommonResult(200, "订单创建成功");
    }
}
11. 创建OrderController.java
package com.sst.cx.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import com.sst.cx.service.OrderService;
import com.sst.cx.domain.*;
@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;
    @GetMapping("/order/create/{productId}/{count}/{money}")
    public CommonResult create(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count
            , @PathVariable("money") BigDecimal money) {
        Order order = new Order();
        order.setProductId(Integer.valueOf(productId).longValue());
        order.setCount(count);
        order.setMoney(money);
        return orderService.create(order);
    }
}
12. 给主启动类添加@EnableDiscoveryClient、@EnableFeignClients注解
package com.sst.cx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "com.sst.cx")
public class SpringCloudAlibabaSeataOrder8005Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudAlibabaSeataOrder8005Application.class, args);
    }
}
===》2. 搭建库存(Storage)服务
sql
create database seata_storage character set utf8 collate utf8_general_ci;
use seata_storage;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` bigint DEFAULT NULL COMMENT '产品id',
  `total` int DEFAULT NULL COMMENT '总库存',
  `used` int DEFAULT NULL COMMENT '已用库存',
  `residue` int DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `t_storage` VALUES ('1', '1', '100', '0', '100');
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
  `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';

创建spring-cloud-alibaba-seata-storage-8006子项目
1. 修改pom.xml文件
    <dependencies>
        <!--nacos 服务注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入 OpenFeign 的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>
        <!--seata 依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.sst.cx</groupId>
            <artifactId>spring-cloud-alibaba-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!--添加 Spring Boot 的监控模块-->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--配置中心 nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!--mybatis自动生成代码插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <configurationFile>src/main/resources/mybatis-generator/generatorConfig.xml</configurationFile>
                    <verbose>true</verbose>
                    <!-- 是否覆盖,true表示会替换生成的JAVA文件,false则不覆盖 -->
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <!--mysql驱动包-->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.19</version>
                    </dependency>
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.4.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
2. 创建bootstrap.yml配置文件(类路径resources目录下)
spring:
  cloud:
    ## Nacos认证信息
    nacos:
      config:
        username: nacos
        password: nacos
        context-path: /nacos
        server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
        namespace:   # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3. 创建application.yml配置文件(类路径resources目录下)
spring:
  application:
    name: spring-cloud-alibaba-seata-storage-8006
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    name: defaultDataSource
    url: jdbc:mysql://localhost:3306/seata_storage?serverTimezone=UTC
    username: root
    password: 12345678
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:1111
        namespace: public
        username:
        password:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
        port: 8719
    alibaba:
      seata:
        tx-service-group: service-storage-group
server:
  port: 8006
seata:
  application-id: ${spring.application.name}
  tx-service-group: service-storage-group
  service:
    grouplist:
      seata-server: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
feign:
  sentinel:
    enabled: true
management:
  endpoints:
    web:
      exposure:
        include: "*"
##### MyBatis 配置 #####
mybatis:
  # 指定 mapper.xml 的位置
  mapper-locations: classpath:mybatis/mapper/*.xml
  #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
  type-aliases-package: com.sst.cx.domain
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true
4. 创建Storage.java
package com.sst.cx.domain;
public class Storage {
    private Long id;
    private Long productId;
    private Integer total;
    private Integer used;
    private Integer residue;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Long getProductId() {
        return productId;
    }
    public void setProductId(Long productId) {
        this.productId = productId;
    }
    public Integer getTotal() {
        return total;
    }
    public void setTotal(Integer total) {
        this.total = total;
    }
    public Integer getUsed() {
        return used;
    }
    public void setUsed(Integer used) {
        this.used = used;
    }
    public Integer getResidue() {
        return residue;
    }
    public void setResidue(Integer residue) {
        this.residue = residue;
    }
}
5. 创建StorageMapper.java
package com.sst.cx.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.sst.cx.domain.Storage;
@Mapper
public interface StorageMapper {
    Storage selectByProductId(Long productId);
    int decrease(Storage record);
}
6. 创建StorageMapper.xml(resouces/mybatis/mapper 目录下)
<?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.sst.cx.mapper.StorageMapper">
    <resultMap id="BaseResultMap" type="Storage">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="product_id" jdbcType="BIGINT" property="productId"/>
        <result column="total" jdbcType="INTEGER" property="total"/>
        <result column="used" jdbcType="INTEGER" property="used"/>
        <result column="residue" jdbcType="INTEGER" property="residue"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , product_id, total, used, residue
    </sql>
    <update id="decrease" parameterType="Storage">
        update t_storage
        <set>
            <if test="total != null">
                total = #{total,jdbcType=INTEGER},
            </if>
            <if test="used != null">
                used = #{used,jdbcType=INTEGER},
            </if>
            <if test="residue != null">
                residue = #{residue,jdbcType=INTEGER},
            </if>
        </set>
        where product_id = #{productId,jdbcType=BIGINT}
    </update>
    <select id="selectByProductId" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from t_storage
        where product_id = #{productId,jdbcType=BIGINT}
    </select>
</mapper>
7. 创建StorageService.java
package com.sst.cx.service;
public interface StorageService {
    int decrease(Long productId, Integer count);
}
8. 创建StorageServiceImpl.java
package com.sst.cx.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import com.sst.cx.domain.Storage;
import com.sst.cx.mapper.StorageMapper;
import com.sst.cx.service.StorageService;
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
    @Resource
    StorageMapper storageMapper;
    @Override
    public int decrease(Long productId, Integer count) {
        log.info("------->storage-service中扣减库存开始");
        log.info("------->storage-service 开始查询商品是否存在");
        Storage storage = storageMapper.selectByProductId(productId);
        if (storage != null && storage.getResidue().intValue() >= count.intValue()) {
            Storage storage2 = new Storage();
            storage2.setProductId(productId);
            storage.setUsed(storage.getUsed() + count);
            storage.setResidue(storage.getTotal().intValue() - storage.getUsed());
            int decrease = storageMapper.decrease(storage);
            log.info("------->storage-service 扣减库存成功");
            return decrease;
        } else {
            log.info("------->storage-service 库存不足,开始回滚!");
            throw new RuntimeException("库存不足,扣减库存失败!");
        }
    }
}
9. 创建StorageController.java
package com.sst.cx.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import com.sst.cx.domain.CommonResult;
import com.sst.cx.service.StorageService;
@RestController
@Slf4j
public class StorageController {
    @Resource
    private StorageService storageService;
    @Value("${server.port}")
    private String serverPort;
    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {
        int decrease = storageService.decrease(productId, count);
        CommonResult result;
        if (decrease > 0) {
            result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, decrease);
        } else {
            result = new CommonResult(505, "from mysql,serverPort:  " + serverPort, "库存扣减失败");
        }
        return result;
    }
}
10. 给主启动类添加@EnableDiscoveryClient、@EnableFeignClients注解
package com.sst.cx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "com.sst.cx")
public class SpringCloudAlibabaSeataStorage8006Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudAlibabaSeataStorage8006Application.class, args);
    }
}
===》3. 搭建账户(Account)服务
sql
create database seata_account character set utf8 collate utf8_general_ci;
use seata_account;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint 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 '剩余可用额度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `t_account` VALUES ('1', '1', '1000', '0', '1000');
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
  `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建spring-cloud-alibaba-seata-account-8007 子项目
1. 修改pom.xml文件
    <dependencies>
        <!--nacos 服务注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入 OpenFeign 的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.sst.cx</groupId>
            <artifactId>spring-cloud-alibaba-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!--添加 Spring Boot 的监控模块-->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!--mybatis自动生成代码插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <configurationFile>src/main/resources/mybatis-generator/generatorConfig.xml</configurationFile>
                    <verbose>true</verbose>
                    <!-- 是否覆盖,true表示会替换生成的JAVA文件,false则不覆盖 -->
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <!--mysql驱动包-->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.19</version>
                    </dependency>
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.4.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
2. 创建bootstrap.yml配置文件(类路径resources目录下)
spring:
  cloud:
    ## Nacos认证信息
    nacos:
      config:
        username: nacos
        password: nacos
        context-path: /nacos
        server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
        namespace:   # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3. 创建application.yml配置文件(类路径resources目录下)
spring:
  application:
    name: spring-cloud-alibaba-seata-account-8007
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    name: defaultDataSource
    url: jdbc:mysql://localhost:3306/seata_account?serverTimezone=UTC
    username: root
    password: 12345678
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:1111
        namespace: public
        username:
        password:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
        port: 8719
    alibaba:
      seata:
        tx-service-group: service-account-group
server:
  port: 8007
seata:
  application-id: ${spring.application.name}
  tx-service-group: service-account-group
  service:
    grouplist:
      seata-server: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:1111
feign:
  sentinel:
    enabled: true
management:
  endpoints:
    web:
      exposure:
        include: "*"
##### MyBatis 配置 ###
mybatis:
  # 指定 mapper.xml 的位置
  mapper-locations: classpath:mybatis/mapper/*.xml
  #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
  type-aliases-package: com.sst.cx.domain
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true
4. 创建Account.java
package com.sst.cx.domain;
import java.math.BigDecimal;
public class Account {
    private Long id;
    private Long userId;
    private BigDecimal total;
    private BigDecimal used;
    private BigDecimal residue;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Long getUserId() {
        return userId;
    }
    public void setUserId(Long userId) {
        this.userId = userId;
    }
    public BigDecimal getTotal() {
        return total;
    }
    public void setTotal(BigDecimal total) {
        this.total = total;
    }
    public BigDecimal getUsed() {
        return used;
    }
    public void setUsed(BigDecimal used) {
        this.used = used;
    }
    public BigDecimal getResidue() {
        return residue;
    }
    public void setResidue(BigDecimal residue) {
        this.residue = residue;
    }
}
5. 创建AccountMapper.java
package com.sst.cx.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal;
import com.sst.cx.domain.Account;
@Mapper
public interface AccountMapper {
    Account selectByUserId(Long userId);
    int decrease(Long userId, BigDecimal money);
}
6. 创建AccountMapper.xml(resouces/mybatis/mapper 目录下)
<?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.sst.cx.mapper.AccountMapper">
    <resultMap id="BaseResultMap" type="Account">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="user_id" jdbcType="BIGINT" property="userId"/>
        <result column="total" jdbcType="DECIMAL" property="total"/>
        <result column="used" jdbcType="DECIMAL" property="used"/>
        <result column="residue" jdbcType="DECIMAL" property="residue"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , user_id, total, used, residue
    </sql>
    <select id="selectByUserId" resultType="Account">
        select
        <include refid="Base_Column_List"/>
        from t_account
        where user_id = #{userId,jdbcType=BIGINT}
    </select>
    <update id="decrease">
        UPDATE t_account
        SET residue = residue - #{money},
            used    = used + #{money}
        WHERE user_id = #{userId};
    </update>
</mapper>
7. 创建AccountService.java
package com.sst.cx.service;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
public interface AccountService {
    // 扣减账户余额
    int decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
8. 创建AccountServiceImpl.java
package com.sst.cx.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import com.sst.cx.domain.Account;
import com.sst.cx.mapper.AccountMapper;
import com.sst.cx.service.AccountService;
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
    @Resource
    AccountMapper accountMapper;
    @Override
    public int decrease(Long userId, BigDecimal money) {

        log.info("------->account-service 开始查询账户余额");
        Account account = accountMapper.selectByUserId(userId);
        log.info("------->account-service 账户余额查询完成," + account);
        if (account != null && account.getResidue().intValue() >= money.intValue()) {
            log.info("------->account-service 开始从账户余额中扣钱!");
            int decrease = accountMapper.decrease(userId, money);
            log.info("------->account-service 从账户余额中扣钱完成");
            return decrease;
        } else {
            log.info("账户余额不足,开始回滚!");
            throw new RuntimeException("账户余额不足!");
        }
    }
}
9. 创建AccountController.java
package com.sst.cx.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import javax.annotation.Resource;
import com.sst.cx.domain.CommonResult;
import com.sst.cx.service.AccountService;
@RestController
@Slf4j
public class AccountController {
    @Resource
    private AccountService accountService;
    @Value("${server.port}")
    private String serverPort;
    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
        int decrease = accountService.decrease(userId, money);
        CommonResult result;
        if (decrease > 0) {
            result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, decrease);
        } else {
            result = new CommonResult(505, "from mysql,serverPort:  " + serverPort, "账户扣减失败");
        }
        return result;
    }
}
10. 给主启动类添加@EnableDiscoveryClient、@EnableFeignClients注解
package com.sst.cx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "com.sst.cx")
public class SpringCloudAlibabaSeataAccount8007Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringCloudAlibabaSeataAccount8007Application.class, args);
    }
}
11. 依次启动NacosServer集群、SeataServer、spring-cloud-alibaba-seata-order-8005、spring-cloud-alibaba-seata-storage-8006、spring-cloud-alibaba-seata-account-8007,当控制台出现register success日志时,说明该服务已经成功连接上SeataServer(TC)。 
在浏览器中访问http://localhost:8005/order/create/1/2/20,结果如下:
  {"code":200,"message":"订单创建成功","data":null}
  执行以下SQL语句 
    1. 查询seata_order数据库中的t_order表:SELECT * FROM seata_order.t_order;  
    2. 查询seata_storage数据库中的t_storage表:SELECT * FROM seata_storage.t_storage;
    3. 查询seata_account数据库中的t_account表:SELECT * FROM seata_account.t_account;
    可以看到,已经创建一条订单数据,且订单状态(status)已修改为“已完成”;商品库存已经扣减 2 件,仓库中商品储量从 100 件减少到了 98 件;账户余额已经扣减,金额从 1000 元减少到了 980 元。
在浏览器中访问http://localhost:8005/order/create/1/10/1000,页面会显示异常信息,提示账户余额不足。再次查询t_order表、t_storage表、t_account表,从控制台可以看到异常后发生了回滚。


项目结构
Order订单服务回滚
Storage库存服务回滚
Account账户服务回滚
给类/方法添加@GlobalTransactional注解来实现分布式事务的开启、管理和控制(当调用该方法时TM会先向TC注册全局事务,由TC生成一个全局唯一的XID并返回给TM)。
  @GlobalTransactional(name = "com-sst-cx-create-order", rollbackFor = Exception.class)

存疑:不添加@GlobalTransactional注解也开启了全局事务。

相关文章

网友评论

    本文标题:SpringCloud之AlibabaSeata(分布式事务组件

    本文链接:https://www.haomeiwen.com/subject/xvnxxdtx.html