环境说明(不看你会后悔的)

seata-server:1.2.0
seata客户端也要是1.2.0

如果你使用的是 seata-spring-boot-starter , 你需要引入一个 seata-all 包 但是官网说 seata-spring-boot-starter 中已经包含 seata-all , 如果不引入就会报错,找不到某个类

分布式事务问题

分布式前

单机单库没这个问题

从1:1 -> 1:N -> N: N

单体应用对单机数据库,单体应用对应多个数据库,微服务下每个微服务对应一个数据库

分布式之后

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用不同的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:

  • 仓储服务:对给定的商品扣除仓储数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

1610803444525.png

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题

Seata简介

是什么

Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务

官网地址

能干嘛

分布式事务处理过程的-ID+三组件模型

Transaction ID XID:全局唯一的事务ID

3组件概念

  • TC (Transaction Coordinator) - 事务协调者
    维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器
    定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器
    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

处理过程

① TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;

② XID在微服务调用链路的上下文传播

③ RM向TC注册分支事务,将其纳入XID对应全局事务的管辖

④ TM向TC发起针对XID的全局提交或回滚决议;

⑤ TC调度XID下管辖的全部分支事务完成提交或回滚事务

1610803444555.png

去哪下

发布说明:https://github.com/seata/seata/releases

怎么玩

Spring 本地@Transactional

全局@GlobalTransactional,SEATA的分布式交易解决方案

1610803444597.png

资源目录介绍(官网)

官网

点击查看

  1. client
    • 存放client端sql脚本,参数配置
  2. config-center
    • 各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件
  3. server
    • server端数据库脚本及各个容器配置

Seata-Server安装(1.2.0)

  1. 官网地址 , http://seata.io/zh-cn/

修改file.conf配置文件

seata-server-1.2.0.tar.gz 解压到指定目录并修改conf目录下的file.conf配置文件

  1. cd /usr/local/seata
  2. tar -xvf seata-server-1.2.0.tar.gz

先备份原始 file.conf文件

  1. cp file.conf file.conf.bk

主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息

store模块

1610803444635.png

  1. ## transaction log store, only used in seata-server
  2. store {
  3. ## store mode: file、db
  4. mode = "db"
  5. ## file store property
  6. file {
  7. ## store location dir
  8. dir = "sessionStore"
  9. # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
  10. maxBranchSessionSize = 16384
  11. # globe session size , if exceeded throws exceptions
  12. maxGlobalSessionSize = 512
  13. # file buffer size , if exceeded allocate new buffer
  14. fileWriteBufferCacheSize = 16384
  15. # when recover batch read size
  16. sessionReloadReadSize = 100
  17. # async, sync
  18. flushDiskMode = async
  19. }
  20. ## database store property
  21. db {
  22. ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
  23. datasource = "druid"
  24. ## mysql/oracle/postgresql/h2/oceanbase etc.
  25. dbType = "mysql"
  26. driverClassName = "com.mysql.cj.jdbc.Driver"
  27. url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8"
  28. user = "gysuiyueran"
  29. password = "SRNDxrhc=1314"
  30. minConn = 5
  31. maxConn = 30
  32. globalTable = "global_table"
  33. branchTable = "branch_table"
  34. lockTable = "lock_table"
  35. queryLimit = 100
  36. maxWait = 5000
  37. }
  38. }

mysql8.0数据库新建库 seata并导入表

执行MySQL数据库操作前,需要我们手动创建一个名称为 seata 的数据库,然后在该数据库下建表

server,存放server侧所需SQL和部署脚本

进入资源目录 seata/script/server/db/mysql.sql ,执行SQL语句。建表语句如下,你也可以点击链接获取

  • db: server 侧的保存模式为 db 时所需表的建表语句
  • docker-compose: server 侧通过 docker-compose 部署的脚本
  • helm: server 侧通过 Helm 部署的脚本
  • kubernetes: server 侧通过 Kubernetes 部署的脚本

建表sql语句

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- 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(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession 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(http://img.itvip666.com/group1/M00/00/08/wKgB_F-eKaqABkM9AAAuaafhloE641.png),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(http://img.itvip666.com/group1/M00/00/08/wKgB_F-eKWyAFGfLAAA2NTP_iC8008.png),
    `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;

1610803444659.png

修改registry.conf配置文件

修改\seata\conf目录下的registry.conf配置文件

这里配置注册中心为nacos,以及修改nacos连接地址信息

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

1610803444684.png

1610803444712.png

测试

1、先启动Nacos端口号8848

2、再启动seata-server,官网

命令启动: sh seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test

-h: 注册到注册中心的ip -p: Server rpc 监听端口 -m: 全局事务会话信息存储模式,file、db、redis,优先读取启动参数 (Seata-Server 1.3及以上版本支持redis) -n: Server node,多个Server时,需区分各自节点,用于生成不同区间的transactionId,以免冲突 -e: 多环境配置参考 http://seata.io/en-us/docs/ops/multi-configuration-isolation.html

在这里我直接使用 命令启动: sh seata-server.sh -h 127.0.0.1 -p 8091 -m db

1610803444740.png

启动成功

1610803444763.png

3、seata服务被注册到nacos中了

1610803444789.png

config-center 配置中心配置

配置中心的配置,本文使用 nacos 作为配置中心。

获取要配置的参数信息

进入 资源目录 seata/script/config-center/config.txt ,展示的是 Seata 1.2.0 版本所有配置中心的内容,全部配置点击链接查看。本文使用db方式,故选择db相关配置,需要用到的配置如下:

service.vgroupMapping.my_test_tx_group=default  
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
store.mode=db  /*此处修改为db*/
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver  /*自定义修改*/
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true /*自定义修改*/
store.db.user=root  /*自定义修改*/
store.db.password=root  /*自定义修改*/
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

将该配置保存为config.txt

注意要把所有注释全部删除

将参数配置到Nacos配置中心

进入 资源目录 seata/script/config-center/nacos/nacos-config.sh ,该配置会将 seata 相关配置批量添加到 nacos 服务器。

该脚本可以随便放在某个位置,只要脚本 nacos-config.sh 能够读取到 config.txt 文件即可。本文放在如下为止

图示

1610803444822.png

  你自己打开 nacos-config.sh 脚本 看看它查找 config.txt 的逻辑就可以了,只要能够读取到 config.txt 文件即可。nacos-config.sh 脚本支持传入 四个参数:

  • -h nacos 所在服务器的IP地址,默认为 localhost
  • -p nacos 端口号,默认为 8848
  • -g nacos 配置所属 group 名称,默认为 SEATA_GROUP
  • -t 将 nacos 配置保存到指定的命名空间,默认为 “”,代表 public 命名空间(注意:-t 参数值接收的是 命名空间ID,不是 命名空间名称

使用 git 命令框 执行 sh nacos-config.sh ,就可以将配置批量保存到 nacos 服务器。如下图所示:

$ sh nacos-config.sh -h server.com -p 8848 -g SEATA_GPOUP
或者
sh nacos-config.sh -h server.com  -p 8848 -t seata-id

1610803445144.gif

到此为止,Config Center 配置中心参数,配置完成。

(seata客户端)订单/库存/账户业务数据库准备

1、以下演示都需要先启动Nacos后启动Seata,保证两个都OK
Seata没启动报错no available server to connect

分布式事务业务说明

这里我门会创建三个服务,一个订单服务,一个库存服务,一个账户服务当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。

该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

下订单—>扣库存—>减账户(余额)

创建业务数据库

  • seata_order:存储订单的数据库
    • t_order表
  • seata_storage:存储库存的数据库
    • t_storage表
  • seata_account:存储账户信息的数据库
    • t_account表

建库SQL

CREATE DATABASE seata_order;

CREATE DATABASE seata_storage;

CREATE DATABASE seata_account;

按照上述3库分别建对应业务表

seata_order库下建t_order表

CREATE TABLE t_order(
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
    `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
    `count` INT(11) DEFAULT NULL COMMENT '数量',
    `money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
    `status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中; 1:已完结'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

SELECT * FROM t_order;

seata_storage库下建t_storage表

CREATE TABLE t_storage(
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
    `total` INT(11) DEFAULT NULL COMMENT '总库存',
    `used` INT(11) DEFAULT NULL COMMENT '已用库存',
    `residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`)
VALUES('1','1','100','0','100');


SELECT * FROM t_storage;

seata_account库下建t_account表

CREATE TABLE t_account(
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
    `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
    `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
    `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
    `residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000');


SELECT * FROM t_account;

按照上述3库分别建对应的回滚日志表

订单-库存-账户3个库下都需要建各自的回滚日志表

进入资源目录 seata/script/client/at/db/mysql.sql ,展示的就是 undo_log 表的建表语句,该表需要在涉及到事务处理的每个库中都添加以下。undo_log 表建表语句如下:

回滚日志sql

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

最终效果

1610803445325.png

业务需求

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

(seata客户端) 订单Order-Module配置搭建

模块名:seata-order-service2001

POM

<dependencies>
    <!--SpringCloudAlibaba的seata分布式事务管理-->
    <!-- 服务端是什么版本就使用什么版本,注意必须是1.2.0版本,1.2.0版本会报错-->
    <!--SpringCloudAlibaba的seata分布式事务管理-->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.2.0</version>
    </dependency>
    <!-- 不引入下面这个依赖,项目启动会报错,找不到某个类 -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <version>1.2.0</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <version>2.2.1.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-all</artifactId>
            </exclusion>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
            </exclusion>
        </exclusions>
    </dependency>


    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--nacos-config-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>

    <!-- 引入自己定义的api通用包,可以使用通用返回结果类,以及支付Payment实体类 -->
    <dependency>
        <groupId>com.sgy.cloud2020</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>false</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

</dependencies>

application.yaml

server:
  port: 2001

spring:
    application:
        #对应Nacos Config中的Data ID,不是指服务名,实际服务应用名称以配置中心文件为准
        name: seata-order-service
    datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://server.com:3306/seata_storage
      username: root
      password: xxxxx
      # 使用我们自己的druid数据源
      type: com.alibaba.druid.pool.DruidDataSource
      druid:
        initialSize: 10 #初始化连接个数
        minIdle: 5    #最小连接个数
        maxActive: 500 #最大连接个数
        maxWait: 60000 #最大等待时间
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

#激活sentinel对Feign的支持
feign:
  sentinel:
    enabled: false
  client:
    config:
      default:
        # 设置feign客户端超时时间(OpenFeign默认支持Ribbon)
        #简历连接所用的时间,适用于网络状况正常的情况下,两端连接所需要的时间
        ConnectTimeout: 5000
        #指建立连接后从服务端读取到可用资源所用的时间
        ReadTimeout: 10000

logging:
  level:
    io:
      seata: info


management:
  endpoints:
    web:
      exposure:
        include: "*"

mybatis:
    mapperLocations: classpath:mapper/*.xml

bootstrap.yaml

spring:
  cloud:
    nacos:
      discovery:
        #Nacos注册中心地址
        server-addr: server.com:8848 #,127.0.0.1:8849,127.0.0.1:8850
        enabled: true
      config:
        #Nacos配置中心地址
        server-addr: server.com:8848 #,127.0.0.1:8849,127.0.0.1:8850
        #分组选择
        group: DEV_GROUP
        #类型(默认加载.properties),默认指定查找nacos-config.yml
        file-extension: yaml
        # 命名空间id
        namespace: dev-id
  #读取环境配置,指定环境后,还会加载nacos-config-dev.yml文件
  profiles:
    #    active: info
    #   active: test # 测试环境配置文件
    active: dev # 开发环境配置文件

application.yml 针对 seata 进行配置

不需要file.conf 和 registry.conf 这两个文件 (之前老版本的是需要的)

原来我们在客户端是使用 registry.conf 作为 seata 的配置文件,现在需要将配置移到 application.yml 或配置中心中,具体配置大家可以参考官网文件 https://github.com/seata/seata/blob/develop/script/client/spring/application.yml

进入 资源目录 seata/script/client/spring/ ,展示的就是 seata 整合 Spring 的全部配置内容,提供了 .properties、.yml 两种格式的配置。详细的配置项还挺多,此处就不粘贴了,你可以点击 资源目录 查看。此处挑选了本案例需要的部分内容进行配置,配置如下所示:

该配置在每个服务模块都需要配置一份,你也可以通过 nacos 配置中心的方式配置使用

seata:
  enabled: true
  application-id: ${spring.application.name}
  # Seata 事务组, 值为seata-server的file.conf中vgroupMapping后面跟的值,此处为fsp_tx_group
  tx-service-group: my_test_tx_group
  enable-auto-data-source-proxy: false
  service:
    vgroup-mapping:
      my_test_tx_group: default  # 此处key需要与tx-service-group的value一致,否则会报 no available service 'null' found, please make sure registry config correct 异常
    grouplist:
      default: server.com:8091
    enable-degrade: false
    # 是否开启本地事务
    disable-global-transaction: false
  config:
    type: nacos
    nacos:
      namespace: seata-id
      serverAddr: server.com:8848
      group: SEATA_GROUP
      userName: "nacos"
      password: "nacos"
  # 服务配置项
  registry:
    type: nacos
    nacos:
      application: seata-server  # 此处名称需和 seata server 服务端 application一致,否则会报 no available service 'null' found, please make sure registry config correct 异常
      server-addr: server.com:8848
      namespace: seata-id
      userName: "nacos"
      password: "nacos"

dao接口以及其实现

OrderDao

package com.sgy.springcloud.dao;

import com.sgy.springcloud.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper // 推荐在每个dao层中加上该注解
public interface OrderDao {
    /**
     * 创建订单
     */
    void create(Order order);

    /**
     * 更新订单,从0改为1
     * @param status 订单状态
     * @param userId 用户id
     */
    void update(@Param("userId") Long userId , @Param("status") Integer status);
}

mapper

resources文件夹下新建mapper文件夹后添加,OrderMapper

<?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.sgy.springcloud.dao.OrderDao">
    <resultMap id="BaseResultMap" type="com.sgy.springcloud.domain.Order">
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="user_id" property="userId" jdbcType="BIGINT" />
        <result column="product_id" property="productId" jdbcType="BIGINT" />
        <result column="count" property="count" jdbcType="INTEGER" />
        <result column="money" property="money" jdbcType="DECIMAL" />
        <result column="status" property="status" jdbcType="INTEGER" />
    </resultMap>

    <insert id="create" >
        insert into t_order (id,user_id,product_id,count,money,status)
        values (null,#{userId},#{productId},#{count},#{money},0)
    </insert>

    <update id="update">
        update t_order set status = 1 where user_id=#{userId} and status=#{status}
    </update>
</mapper>

Service接口及实现

OrderService

  1. 接口
public interface OrderService {

    /**
     * 创建订单
     * @param order
     */
    void create(Order order);
}
  1. 实现
@Service
public class OrderServiceImpl implements OrderService {

    private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Resource
    OrderDao orderDao;

    @Resource
    AccountService accountService;

    @Resource
    StorageService storageService;

    @Override
    public void create(Order order) {
        log.info("开始创建订单");
        orderDao.create(order);

        // 扣减库存
        log.info("============ 订单微服务 调用库存微服务,减库存 start ==============");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("============ 订单微服务 调用库存微服务,减库存 end  ==============");

        // 扣减账户余额
        log.info("============ 订单微服务 调用账户微服务,减余额 start ==============");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("============ 订单微服务 调用账户微服务,减余额 end ==============");

        // 修改订单状态
        // 扣减账户余额
        log.info("============ 修改订单状态 start ==============");
        orderDao.update(order.getUserId(),0);
        log.info("============ 修改订单状态 end ==============");

        log.info("下单结束了");
    }
}

AccountService

/**
 * 账户
 */
@FeignClient(value = "seata-account-service")
public interface AccountService {
    @PostMapping("/account/decrease")
    R decrease(@RequestParam(value = "userId") Long userId , @RequestParam(value = "money") BigDecimal money);
}

StorageService

// 库存
@FeignClient(value = "seata-storage-service")
public interface StorageService {

    @GetMapping(value = "/storage/decrease")
    R decrease(@RequestParam(value = "productId") Long productId , @RequestParam(value = "count") Integer count);
}

Controller

@RestController
public class OrderController {
    @Resource
    OrderService orderService;

    @GetMapping("/order/create")
    public R create(Order order) {
        orderService.create(order);
        return new R().ok("创建订单成功");
    }
}

config配置

MyBatisConfig

@Configuration
@MapperScan("com.sgy.springcloud.dao")
public class MyBatisConfig {
}

数据源配置

数据源代理(不支持自动和手动配置并存,不支持XA数据源自动代理)

  1. 0.9.0版本开始seata支持自动代理数据源

1.1.0: seata-all取消属性配置,改由注解@EnableAutoDataSourceProxy开启,并可选择jdk proxy或者cglib proxy 1.0.0: client.support.spring.datasource.autoproxy=true 0.9.0: support.spring.datasource.autoproxy=true

  1. 手动配置可参考下方的例子

Seata 是通过代理数据源实现事务分支,所以需要配置 io.seata.rm.datasource.DataSourceProxy 的 Bean,且是 @Primary默认的数据源,否则事务不会回滚,无法实现分布式事务,数据源配置类DataSourceProxyConfig.java如下:

代码清单:Alibaba/seata-nacos-jpa/order-server/src/main/java/com/springcloud/orderserver/config/DataSourceProxyConfig.java

/**
 * 使用seata代理数据源,实现分布式事务
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;


    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }


    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

主启动

import com.sgy.springcloud.config.DataSourceProxyConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;


@EnableFeignClients
@EnableDiscoveryClient
// 这个是要特别注意的地方,seata对数据源做了代理和接管,在每个参与分布式事务的服务中,都要做如下配置:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消数据源自动创建的配置
public class SeataOrderMainApp2001 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class,args);
    }
}

(seata客户端) 库存Storage-Module配置搭建

微服务名称

seata-storage-service2002

pom

<dependencies>
    <!--SpringCloudAlibaba的seata分布式事务管理-->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.2.0</version>
    </dependency>
    <!-- 不引入下面这个依赖,项目启动会报错,找不到某个类 -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <version>1.2.0</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <version>2.2.1.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-all</artifactId>
            </exclusion>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
            </exclusion>
        </exclusions>
    </dependency>


    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <!--nacos-config-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>

    <!-- 引入自己定义的api通用包,可以使用通用返回结果类,以及支付Payment实体类 -->
    <dependency>
        <groupId>com.sgy.cloud2020</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>false</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!--阿里Druid连接池集成SpringBoot起步依赖-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>


    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

</dependencies>

application.yaml

你可以将配置放入到配置中心

server:
  port: 2001

spring:
  application:
    #对应Nacos Config中的Data ID,不是指服务名,实际服务应用名称以配置中心文件为准
    name: seata-storage-service
  datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://server.com:3306/seata_storage
      username: root
      password: xxxxx
      # 使用我们自己的druid数据源
      type: com.alibaba.druid.pool.DruidDataSource
      druid:
        initialSize: 10 #初始化连接个数
        minIdle: 5    #最小连接个数
        maxActive: 500 #最大连接个数
        maxWait: 60000 #最大等待时间
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500



#激活sentinel对Feign的支持
feign:
  sentinel:
    enabled: false
  client:
    config:
      default:
        # 设置feign客户端超时时间(OpenFeign默认支持Ribbon)
        #简历连接所用的时间,适用于网络状况正常的情况下,两端连接所需要的时间
        ConnectTimeout: 5000
        #指建立连接后从服务端读取到可用资源所用的时间
        ReadTimeout: 10000

seata:
  enabled: true
  application-id: ${spring.application.name}
  # Seata 事务组, 值为seata-server的file.conf中vgroupMapping后面跟的值,此处为fsp_tx_group
  tx-service-group: my_test_tx_group
  enable-auto-data-source-proxy: false
  service:
    vgroup-mapping:
      my_test_tx_group: default  # 此处key需要与tx-service-group的value一致,否则会报 no available service 'null' found, please make sure registry config correct 异常
    grouplist:
      default: server.com:8091
    enable-degrade: false
    # 是否开启本地事务
    disable-global-transaction: false
  config:
    type: nacos
    nacos:
      namespace: seata-id
      serverAddr: server.com:8848
      group: SEATA_GROUP
      userName: "nacos"
      password: "nacos"
  # 服务配置项
  registry:
    type: nacos
    nacos:
      application: seata-server  # 此处名称需和 seata server 服务端 application一致,否则会报 no available service 'null' found, please make sure registry config correct 异常
      server-addr: server.com:8848
      namespace: seata-id
      userName: "nacos"
      password: "nacos"

logging:
  level:
    io:
      seata: info


management:
  endpoints:
    web:
      exposure:
        include: "*"

mybatis:
    mapperLocations: classpath:mapper/*.xml

bootstrap.yaml

spring:
  cloud:
    nacos:
      discovery:
        #Nacos注册中心地址
        server-addr: server.com:8848 #,127.0.0.1:8849,127.0.0.1:8850
        enabled: true
      config:
        #Nacos配置中心地址
        server-addr: server.com:8848 #,127.0.0.1:8849,127.0.0.1:8850
        #分组选择
        group: DEV_GROUP
        #类型(默认加载.properties),默认指定查找nacos-config.yml
        file-extension: yaml
        # 命名空间id
        namespace: dev-id
  #读取环境配置,指定环境后,还会加载nacos-config-dev.yml文件
  profiles:
    #    active: info
    #   active: test # 测试环境配置文件
    active: dev # 开发环境配置文件

domain

@Data
public class Storage {

    private Long id;

    // 产品id
    private Long productId;

    //总库存
    private Integer total;

    //已用库存
    private Integer used;

    //剩余库存
    private Integer residue;
}

dao

/**
 * 库存
 */
@Mapper
public interface StorageDao {
    void decrease(@Param("productId") Long productId , @Param("count") Integer count);
}

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.sgy.springcloud.dao.StorageDao">

    <resultMap id="BaseResultMao" type="com.sgy.springcloud.domain.Storage">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"  />
        <result column="total" property="total" jdbcType="INTEGER"  />
        <result column="used" property="used" jdbcType="INTEGER" />
        <result column="residue" property="residue" jdbcType="INTEGER"  />

    </resultMap>

    <update id="decrease">
        update t_storage
        set used = used + #{count} , residue = residue - #{count}
        where product_id = #{productId}
    </update>

</mapper>

service

public interface StorageService {
    void decrease(Long productId,Integer count);
}

实现

import com.sgy.springcloud.dao.StorageDao;
import com.sgy.springcloud.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class StorageServiceImpl implements StorageService {
    private static final Logger log = LoggerFactory.getLogger(StorageServiceImpl.class);

    @Resource
    StorageDao storageDao;

    @Override
    public void decrease(Long productId, Integer count) {
        log.info("开始扣减库存");
        storageDao.decrease(productId,count);
        log.info("完成扣减库存");
    }
}

config

配置mybatis

@Configuration
@MapperScan("com.sgy.springcloud.dao")
public class MyBatisConfig {
}

使用seata管理数据源

@Configuration
public class DataSourceProxyConfig {
    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

controller

@RestController
public class StorageController {
    @Resource
    StorageService storageService;

    public R decrease(@RequestParam("productId") Long productId , @RequestParam("count") Integer count)  {
        storageService.decrease(productId,count);
        return new R().ok("扣减库存成功");
    }
}

主启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)  // 取消数据源自动创建的配置
public class StorageService2002 {
    public static void main(String[] args) {
        SpringApplication.run(StorageService2002.class,args);
    }
}

(seata客户端) 账户Account-Module配置搭建

微服务名称

seata-account-service2003

pom

<dependencies>
    <!--SpringCloudAlibaba的seata分布式事务管理-->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.2.0</version>
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <version>1.2.0</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <version>2.2.1.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-all</artifactId>
            </exclusion>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
            </exclusion>
        </exclusions>
    </dependency>


    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <!--nacos-config-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>

    <!-- 引入自己定义的api通用包,可以使用通用返回结果类,以及支付Payment实体类 -->
    <dependency>
        <groupId>com.sgy.cloud2020</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>false</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!--阿里Druid连接池集成SpringBoot起步依赖-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>



    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

</dependencies>

application.yaml

server:
  port: 2003

spring:
  application:
    #对应Nacos Config中的Data ID,不是指服务名,实际服务应用名称以配置中心文件为准
    name: seata-account-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://server.com:3306/seata_account
    username: root
    password: xxxxx
    # 使用我们自己的druid数据源
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initialSize: 10 #初始化连接个数
      minIdle: 5    #最小连接个数
      maxActive: 500 #最大连接个数
      maxWait: 60000 #最大等待时间
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall,log4j
      maxPoolPreparedStatementPerConnectionSize: 20
      useGlobalDataSourceStat: true
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500


# 激活sentinel对Feign的支持
feign:
  sentinel:
    enabled: false
  client:
    config:
      default:
        # 设置feign客户端超时时间(OpenFeign默认支持Ribbon)
        #简历连接所用的时间,适用于网络状况正常的情况下,两端连接所需要的时间
        ConnectTimeout: 5000
        #指建立连接后从服务端读取到可用资源所用的时间
        ReadTimeout: 10000

seata:
  enabled: true
  application-id: ${spring.application.name}
  # Seata 事务组, 值为seata-server的file.conf中vgroupMapping后面跟的值,此处为fsp_tx_group
  tx-service-group: my_test_tx_group
  enable-auto-data-source-proxy: false
  service:
    vgroup-mapping:
      my_test_tx_group: default  # 此处key需要与tx-service-group的value一致,否则会报 no available service 'null' found, please make sure registry config correct 异常
    grouplist:
      default: server.com:8091
    enable-degrade: false
    # 是否开启本地事务
    disable-global-transaction: false
  config:
    type: nacos
    nacos:
      namespace: seata-id
      serverAddr: server.com:8848
      group: SEATA_GROUP
      userName: "nacos"
      password: "nacos"
  # 服务配置项
  registry:
    type: nacos
    nacos:
      application: seata-server  # 此处名称需和 seata server 服务端 application一致,否则会报 no available service 'null' found, please make sure registry config correct 异常
      server-addr: server.com:8848
      namespace: seata-id
      userName: "nacos"
      password: "nacos"

logging:
  level:
    io:
      seata: info


management:
  endpoints:
    web:
      exposure:
        include: "*"

mybatis:
  mapperLocations: classpath:mapper/*.xml

bootstrap.yaml


spring:
  cloud:
    nacos:
      discovery:
        #Nacos注册中心地址
        server-addr: server.com:8848 #,127.0.0.1:8849,127.0.0.1:8850
        enabled: true
      config:
        #Nacos配置中心地址
        server-addr: server.com:8848 #,127.0.0.1:8849,127.0.0.1:8850
        #分组选择
        group: DEV_GROUP
        #类型(默认加载.properties),默认指定查找nacos-config.yml
        file-extension: yaml
        # 命名空间id
        namespace: dev-id
  #读取环境配置,指定环境后,还会加载nacos-config-dev.yml文件
  profiles:
    #    active: info
    #   active: test # 测试环境配置文件
    active: dev # 开发环境配置文件

domain

package com.sgy.springcloud.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 总额度
     */
    private BigDecimal total;

    /**
     * 已用额度
     */
    private BigDecimal used;

    /**
     * 剩余额度
     */
    private BigDecimal residue;

}

dao

@Mapper
public interface AccountDao {
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

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.sgy.springcloud.dao.AccountDao">
    <resultMap id="BaseResultMap" type="com.sgy.springcloud.domain.Account">
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="user_id" property="userId" jdbcType="BIGINT" />
        <result column="total" property="total" jdbcType="DECIMAL" />
        <result column="used" property="used" jdbcType="DECIMAL" />
        <result column="residue" property="residue" jdbcType="DECIMAL" />

    </resultMap>

    <update id="decrease" >
        update t_account
        set used = used + #{money} , residue = residue -  #{money}
        where user_id = #{userId}
    </update>
</mapper>

service

public interface AccountService {
    void decrease(Long userId , BigDecimal money);
}
import com.sgy.springcloud.dao.AccountDao;
import com.sgy.springcloud.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;

@Service
public class AccountServiceImpl implements AccountService {
    private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Resource
    AccountDao accountDao;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("扣减用户余额 --- start");
        accountDao.decrease(userId, money);
        log.info("扣减用户余额 --- end");
    }
}

config

  1. mybatis配置
@Configuration
@MapperScan("com.sgy.springcloud.dao")
public class MyBatisConfig {
}
  1. seata数据源配置
@Configuration
public class DataSourceProxyConfig {
    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

controller

@RestController
public class AccountController {
    @Resource
    AccountService accountService;

    @PostMapping("/account/decrease")
    public R decrease(@RequestParam("userId") Long userId , @RequestParam("money") BigDecimal money) {
        accountService.decrease(userId, money);
        return new R<>().ok("扣减用户余额成功");
    }
}

主启动类

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)  // 取消数据源自动创建的配置
public class AccountService2003 {
    public static void main(String[] args) {
        SpringApplication.run(AccountService2003.class,args);
    }
}

seata大显身手

数据库初始状态

  1. 订单表

1610803445460.png

  1. 库存表

1610803445485.png

  1. 账户表

1610803445515.png

超时异常,没加@GlobalTransactional

  1. 在账户微服务中,模拟超时
@Service
public class AccountServiceImpl implements AccountService {
    private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Resource
    AccountDao accountDao;

    @Override
    public void decrease(Long userId, BigDecimal money) throws InterruptedException {
        log.info("扣减用户余额 --- start");
        // 模拟超时
        TimeUnit.SECONDS.sleep(20);
        accountDao.decrease(userId, money);
        log.info("扣减用户余额 --- end");
    }
}
  1. 测试

1610803445561.png

  1. 数据库情况

1610803445588.png

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

超时异常,添加@GlobalTransactional

  1. 订单微服务修改
@Service
public class OrderServiceImpl implements OrderService {

    private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Resource
    OrderDao orderDao;

    @Resource
    AccountService accountService;

    @Resource
    StorageService storageService;

    @GlobalTransactional(name = "seata-create-order",rollbackFor = Exception.class)
    @Override
    public void create(Order order) {
        log.info("开始创建订单");
        orderDao.create(order);

        // 扣减库存
        log.info("============ 订单微服务 调用库存微服务,减库存 start ==============");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("============ 订单微服务 调用库存微服务,减库存 end  ==============");

        // 扣减账户余额
        log.info("============ 订单微服务 调用账户微服务,减余额 start ==============");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("============ 订单微服务 调用账户微服务,减余额 end ==============");

        // 修改订单状态
        // 扣减账户余额
        log.info("============ 修改订单状态 start ==============");
        orderDao.update(order.getUserId(),0);
        log.info("============ 修改订单状态 end ==============");

        log.info("下单结束了");
    }
}
  1. 访问

1610803445630.png

  1. 查看数据库情况
    • 下单后数据库数据并没有任何改变

1610803445659.png