SpringCloud Alibaba Seata处理分布式事务

第一章 分布式事务问题


  随着业务的不断发展,单体架构已经无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的首选,但所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是“分布式事务”问题。

  在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身数据库事务来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。

用户购买商品的业务逻辑。整个业务需要调用三个微服务:

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

  为了保证数据的正确性和一致性,我们必须保证所有这些操作要么全部成功,要么全部失败,否则就可能出现类似于商品库存已扣减,但用户账户资金尚未扣减的情况。各服务自身的事务特性显然是无法实现这一目标的,此时,我们可以通过分布式事务框架来解决这个问题。

  Seata 就是这样一个分布式事务处理框架,它是由阿里巴巴和蚂蚁金服共同开源的分布式事务解决方案,能够在微服务架构下提供高性能且简单易用的分布式事务服务。

第二章 Seata简介


2.1 Seata是什么?

  Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

2.2 Seata 整体工作流程

  一个典型的分布式事务过程:主要是通过 XID 和 3 个核心组件实现的。

  • Transaction ID XID: 是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
  • 三组件
    1. Transaction Coordinator (TC):事务协调器,它是事务的协调者(这里指的是 Seata 服务器),主要负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
    2. Transaction Manager(TM):事务管理器,它是事务的发起者,负责定义全局事务的范围,并根据 TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
    3. Resource Manager (RM):资源管理器,它是资源的管理者(这里可以将其理解为各服务使用的数据库)。它负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。

  以上三个组件相互协作,TC 以 Seata 服务器(Server)形式独立部署,TM 和 RM 则是以 Seata Client 的形式集成在微服务中运行,其整体工作流程如下图。

在这里插入图片描述

处理过程:

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

2.3 Seata AT 模式

  Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。

  在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。

2.3.1 AT 模式的前提

  任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:

  • 必须使用支持本地 ACID 事务特性的关系型数据库,例如 MySQL、Oracle 等;
  • 应用程序必须是使用 JDBC 对数据库进行访问的 JAVA 应用。

  此外,我们还需要针对业务中涉及的各个数据库表,分别创建一个 UNDO_LOG(回滚日志)表。

2.3.2 AT 模式的工作机制

  Seata 的 AT 模式工作时大致可以分为以两个阶段,下面我们就结合一个实例来对 AT 模式的工作机制进行介绍。

  假设某数据库中存在一张名为 webset 的表,表结构如下。

在这里插入图片描述
在某次分支事务中,我们需要在 webset 表中执行以下操作。

update webset set url = 'c.biancheng.net' where name = 'C语言中文网';

一阶段

Seata AT 模式一阶段的工作流程如下图所示。

在这里插入图片描述
Seata AT 模式一阶段工作流程如下:

  1. 获取 SQL 的基本信息:Seata 拦截并解析业务 SQL,得到 SQL 的操作类型(UPDATE)、表名(webset)、判断条件(where name = ‘C语言中文网’)等相关信息。
  2. 查询前镜像:根据得到的业务 SQL 信息,生成“前镜像查询语句”。
    select id,name,url from webset where name=‘C语言中文网’;
    执行“前镜像查询语句”,得到即将执行操作的数据,并将其保存为“前镜像数据(beforeImage)”。
    在这里插入图片描述
  3. 执行业务 SQL(update webset set url = ‘c.biancheng.net’ where name = ‘C语言中文网’;),将这条记录的 url 修改为 c.biancheng.net。\
  4. 查询后镜像:根据“前镜像数据”的主键(id : 1),生成“后镜像查询语句”。
    select id,name,url from webset where id= 1;;
    执行“后镜像查询语句”,得到执行业务操作后的数据,并将其保存为“后镜像数据(afterImage)”。
    在这里插入图片描述
  5. 插入回滚日志:将前后镜像数据和业务 SQL 的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中,示例回滚日志如下。
{
  "@class": "io.seata.rm.datasource.undo.BranchUndoLog",
  "xid": "172.26.54.1:8091:5962967415319516023",
  "branchId": 5962967415319516027,
  "sqlUndoLogs": [
    "java.util.ArrayList",
    [
      {
        "@class": "io.seata.rm.datasource.undo.SQLUndoLog",
        "sqlType": "UPDATE",
        "tableName": "webset",
        "beforeImage": {
          "@class": "io.seata.rm.datasource.sql.struct.TableRecords",
          "tableName": "webset",
          "rows": [
            "java.util.ArrayList",
            [
              {
                "@class": "io.seata.rm.datasource.sql.struct.Row",
                "fields": [
                  "java.util.ArrayList",
                  [
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "id",
                      "keyType": "PRIMARY_KEY",
                      "type": -5,
                      "value": [
                        "java.lang.Long",
                        1
                      ]
                    },
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "url",
                      "keyType": "NULL",
                      "type": 12,
                      "value": "biancheng.net"
                    }
                  ]
                ]
              }
            ]
          ]
        },
        "afterImage": {
          "@class": "io.seata.rm.datasource.sql.struct.TableRecords",
          "tableName": "webset",
          "rows": [
            "java.util.ArrayList",
            [
              {
                "@class": "io.seata.rm.datasource.sql.struct.Row",
                "fields": [
                  "java.util.ArrayList",
                  [
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "id",
                      "keyType": "PRIMARY_KEY",
                      "type": -5,
                      "value": [
                        "java.lang.Long",
                        1
                      ]
                    },
                    {
                      "@class": "io.seata.rm.datasource.sql.struct.Field",
                      "name": "url",
                      "keyType": "NULL",
                      "type": 12,
                      "value": "c.biancheng.net"
                    }
                  ]
                ]
              }
            ]
          ]
        }
      }
    ]
  ]
}
  1. 注册分支事务,生成行锁:在这次业务操作的本地事务提交前,RM 会向 TC 注册分支事务,并针对主键 id 为 1 的记录生成行锁。

以上所有操作均在同一个数据库事务内完成,可以保证一阶段的操作的原子性。

  1. 本地事务提交:将业务数据的更新和前面生成的 UNDO_LOG 一并提交。
  2. 上报执行结果:将本地事务提交的结果上报给 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= ‘biancheng.net’ where id = 1;
  4. 还原数据:执行回滚语句,并将前镜像数据、后镜像数据以及行锁删除。
  5. 提交事务:提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

2.4 下载

https://github.com/seata/seata/releases

我下载的是1.4.2版本。同时也要下载源码包seata-1.4.2.zip(DB模式下要执行SQL脚本以及Nacos作为配置中心,Seata相关的配置需要注册到Nacos
在这里插入图片描述

第三章 Seata-Server安装


Server端存储模式(store.mode)现有file、db、redis三种(后续将引入raft,mongodb),file模式无需改动,直接启动即可,下面专门讲下db启动步骤。

file模式为单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高;
db模式为高可用模式,全局事务会话信息通过db共享,相应性能差些;
redis模式Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置.

3.1 解压seata-server-1.4.2.zip文件并修改配置

目录是:

seata-server-1.4.2\seata\conf

需要修改的配置文件

registry.conf
file.conf

registry.conf指定注册模式,如 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa,默认:type = “file”,指向file.conf文件

file.conf:有模式:file、db、redis,用于存储事务的日记。

registry.conf当前指定使用nacos作为注册中心和配置中心,需要修改registry项和config项,同时修改nacos的连接信息。
建议增加namespace的配置,将Seata相应的配置放在一起,不然后面配置中心的东西很多

在这里插入图片描述

registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    group = "SEATA_GROUP"
    namespace = "61b373e2-b229-46e4-925d-5220f8653886"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "61b373e2-b229-46e4-925d-5220f8653886"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  apollo {
    appId = "seata-server"
    ## apolloConfigService will cover apolloMeta
    apolloMeta = "http://192.168.1.204:8801"
    apolloConfigService = "http://192.168.1.204:8080"
    namespace = "application"
    apolloAccesskeySecret = ""
    cluster = "seata"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
    nodePath = "/seata/seata.properties"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}


file.conf


## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "dbcp"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://localhost:3306/seata?serverTimezone=GMT%2B8&characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    ## redis mode: single、sentinel
    mode = "single"
    ## single mode property
    single {
      host = "127.0.0.1"
      port = "6379"
    }
    ## sentinel mode property
    sentinel {
      masterName = ""
      ## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
      sentinelHosts = ""
    }
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }
}

3.2 在seata库里新建表

Server端:server端数据库脚本 (包含 lock_table、branch_table 与 global_table) 及各个容器配置,脚本需要在源码包找(seata-1.4.2.zip)

执行脚本(仅使用DB模式才需要执行脚本)

脚本所在的目录(seata-1.4.2为源码seata-1.4.2.zip)找见mysql.sql

seata-1.4.2\script\server\db
-- -------------------------------- 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(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(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

3.3 启动Seata

启动Seata,记得先启动Nacos

Windows启动:
进入目录:seata-server-1.4.2\bin,双击seata-server.bat直接启动

Linux命令启动:
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

在这里插入图片描述

在这里插入图片描述

3.4 导入Seata相应的配置项到Nacos的配置中心(非注册中心)

config-center 官网说明地址是:
https://github.com/seata/seata/tree/develop/script/config-center

3.4.1 config.txt

该文件在源码包中:seata-1.4.2.zip,解压后的目录是:

seata-1.4.2\script\config-center\config.txt

3.4.2 nacos-config.sh

当前使用的是Nacos作为配置中心(config-center),所以进入到nacos目录:

seata-1.4.2\script\config-center\nacos 

有两个脚本文件:
nacos-config.py
nacos-config.sh
第一个是phython脚本,第二个是Linux脚本
那Windows脚本怎么执行呢?安装Git-2.26.2-64-bit.exe,使用Git Bash执行。

3.4.3 修改config.txt文件

my_test_tx_group是默认有的,没有修改。增加分布式事务分组nacos_producer_tx_group、nacos_consumer_tx_group。注意:名称长度不能超过VARCHAR(32)。事务分组允许只配置一个,也可以配置成多个,用于切换,此处额外增加2个,具体配置如下:

service.vgroupMapping.my_test_tx_group=default
service.vgroupMapping.nacos-service-tx-group=default


  1. vgroup_mapping.my-tx-group = "seata-server"为事务组名称,这里的值需要和TC中配置的service.vgroup-mapping.my-tx-group一致;
  2. 事务组的命名不要用下划线’_‘,可以用’-'因为在seata的高版本中使用underline下划线 将导致service not to be found。

什么是事务分组?见:
https://seata.io/zh-cn/docs/user/txgroup/transaction-group.html

修改数据存储模式为db模式,修改数据连接和账号密码

store.mode=db
store.db.datasource=dbcp
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root

3.4.4 执行sh文件

建议增加namespace的配置(-t参数,命名空间的id是在Nacos创建生成的),将Seata相应的配置放在一起

通过Git Bash将相应的配置导致到Nacos配置中心,命令如下

sh D:/java/seata/seata-server-1.4.2/config-center/nacos/nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GRUOP -t 61b373e2-b229-46e4-925d-5220f8653886 -u nacos -w nacos

在这里插入图片描述
config.txt文件脚本执行后,成功91,失败4个,但失败的不影响Seata运行。

查看Nacos配置列表
在这里插入图片描述

不导入config.txt文件,项目加入Seata依赖和配置启动后,会出现下面的错误:can not get cluster name in registry config,详细如下:

SPRING-CLOUD-NACOS-SERVICE服务提供者:

2021-05-10 09:27:18.719 ERROR 13544 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.SPRING-CLOUD-NACOS-SERVICE_tx_group', please make sure registry config correct
2021-05-10 09:27:18.753 ERROR 13544 --- [eoutChecker_2_1] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.SPRING-CLOUD-NACOS-SERVICE_tx_group', please make sure registry config correct

SPRING-CLOUD-NACOS-CONSUMER服务消费者:

2021-05-10 09:27:33.786 ERROR [SPRING-CLOUD-NACOS-CONSUMER,,,] 14916 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.SPRING-CLOUD-NACOS-CONSUMER_tx_group', please make sure registry config correct
2021-05-10 09:27:33.820 ERROR [SPRING-CLOUD-NACOS-CONSUMER,,,] 14916 --- [eoutChecker_2_1] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.SPRING-CLOUD-NACOS-CONSUMER_tx_group', please make sure registry config correct

第四章 订单/库存/账户业务数据库准备


4.1 分布式事务业务说明

  这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。

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

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

4.2 创建业务数据库

seata_order:存储订单的数据库;

seata_storage:存储库存的数据库;

seata_account:存储账户信息的数据库。

create database seata_order;
create database seata_storage;
create database seata_account;

4.3 分别创建对应的业务表

seata_order库下建t_order表

DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (
  `int` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) 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:已完结',
  PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;

seata_storage库下建t_storage 表

DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage`  (
  `int` bigint(11) NOT NULL AUTO_INCREMENT,
  `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 '剩余库存',
  PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);

seata_account库下建t_account 表

CREATE TABLE `t_account`  (
  `id` bigint(11) NOT NULL 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 NULL COMMENT '剩余可用额度',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账户表' ROW_FORMAT = Dynamic;
 
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);

4.3 分别创建对应的回滚日志表

3个库下都需要建各自的回滚日志表

脚本在源码包,目录是:

seata-1.4.2\script\client\at\db
 -- 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       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

在这里插入图片描述

第五章 订单、库存、账户微服务准备


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

5.1 新建订单模块

5.1.1 新建 Module seata-order-service2001

在这里插入图片描述

5.1.2 新建 pom文件

Seata依赖说明(建议单选)
依赖seata-all
依赖seata-spring-boot-starter,支持yml、properties配置(.conf可删除),内部已依赖seata-all
依赖spring-cloud-alibaba-seata,内部集成了seata,并实现了xid传递

 <dependencies>

        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--spring-boot-starter-actuator 可以用于检测系统的健康情况、当前的Beans、系统的缓存等-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

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

       <!--使用自己下载的  seata server 版本-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.4.2</version>
        </dependency>

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

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

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!--mysql-druid-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>

        <!--        Lombok能以简单的注解形式来简化java代码,提高开发人员的开发效率。例如开发中经常需要写的javabean,都需要花时间去添加相应的getter/setter,也许还要去写构造器、equals等方法,而且需要维护,当属性多时会出现大量的getter/setter方法,这些显得很冗长也没有太多技术含量,一旦修改属性,就容易出现忘记修改对应方法的失误。-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

5.1.3 yml文件

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        # 自定义事务组名称需要与seata-server中的对应
        tx-service-group: nacos-service-tx-group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: root




#关闭feign的hystrix支持
feign:
  hystrix:
    enabled: false


logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml



5.1.4 file.config、registry.conf 配置

拷贝seata-server/conf目录下的file.conf、registry.conf复制到resourses目录下面

在这里插入图片描述

5.1.5 domain

在这里插入图片描述

Order

package com.atguigu.springcloud.domain;

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

import java.math.BigDecimal;


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private Long id;

    private Long userId;

    private Long productId;

    private Integer count;

    private BigDecimal money;

    private Integer status; //订单状态:0:创建中;1:已完结
}

CommonResult

package com.atguigu.springcloud.domain;

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


@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {

    private Integer code;
    private String message;
    private T data;

    public CommonResult(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

5.1.6 Dao接口实现

在这里插入图片描述
OrderDao

package com.atguigu.springcloud.dao;

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

/**
 * @Author cjz
 * @Date 2022/3/29 21:54
 */
@Mapper
public interface OrderDao {

    //1 新建订单
    void create(Order order);

    //2 修改订单状态,从零改为1
    void update(@Param("userId") Long userId, @Param("status") Integer status);
}

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.springcloud.dao.OrderDao">

    <resultMap id="order" type="com.atguigu.springcloud.domain.Order">
        <result property="id" column="id" jdbcType="BIGINT"/>
        <result property="userId" column="user_id" jdbcType="BIGINT"/>
        <result property="productId" column="product_id" jdbcType="BIGINT"/>
        <result property="count" column="count" jdbcType="INTEGER"/>
        <result property="money" column="money" jdbcType="BIGINT"/>
        <result property="status" column="status" jdbcType="INTEGER"/>
    </resultMap>


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

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

5.1.7 Service接口及其实现

在这里插入图片描述

AccountService

package com.atguigu.springcloud.service;

import com.atguigu.springcloud.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;


@FeignClient(value = "seata-account-service")
public interface AccountService {

    @PostMapping("/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long productId, @RequestParam("money") BigDecimal money);
}

StorageService

package com.atguigu.springcloud.service;

import com.atguigu.springcloud.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;


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

    @PostMapping("/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

OrderService

package com.atguigu.springcloud.service;

import com.atguigu.springcloud.domain.Order;


public interface OrderService {
    void create(Order order);
}

OrderServiceImpl

package com.atguigu.springcloud.service.impl;

import com.atguigu.springcloud.dao.OrderDao;
import com.atguigu.springcloud.domain.Order;
import com.atguigu.springcloud.service.AccountService;
import com.atguigu.springcloud.service.OrderService;
import com.atguigu.springcloud.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;



@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderDao orderDao;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;


    /**
     * 创建订单->调用库存服务扣减库存->调用账户扣减账户->修改订单状态
     * @param order
     */
    @Override
    //@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
    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");

        //修改订单状态,从零到1 1 代表已经完成
        log.info("------------>修改订单状态,*****start");
        orderDao.update(order.getUserId(),0);
        log.info("------------>修改订单状态,*****end");

        log.info("------------>下订单结束,哈哈哈哈哈哈哈");
    }
}

整体业务事调用库存服务 扣减库存->调用账户扣减账户->修改订单状态 我们暂时将@GlobalTransactional注释显示分布式事务

5.1.8 Controller

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.domain.CommonResult;
import com.atguigu.springcloud.domain.Order;
import com.atguigu.springcloud.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;


@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    @GetMapping("/order/create")
    public CommonResult create(Order order){
        orderService.create(order);
        return new CommonResult<>(200,"订单创建成功");
    }
}

5.1.9 Config配置

DataSourceProxyConfig:

在这里插入图片描述

package com.atguigu.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * 使用Seata对数据源进行代理
 */
@Configuration
public class DataSourceSeataProxyConfig {

    @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();
    }
}

5.1.10 主启动类

package com.atguigu.springcloud;

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

/**
 * exclude = DataSourceAutoConfiguration.class 取消数据源的自动创建,
 * 读取自定义的DataSourceProxyConfig.class类,使用Seata对数据源进行代理
 */
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan(value = "com.atguigu.springcloud.dao")
public class SeataOrderMainApp2001 {

    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}

5.2 新建库存Storage-Module

具体详情就不写了,可以取gitee仓库地址查看

gitee仓库地址

在这里插入图片描述

5.2 新建账户Account-Module

具体详情就不写不出来了,可以取gitee仓库地址查看

gitee仓库地址

在这里插入图片描述

第六章 测试


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

6.1 数据库初始数据

在这里插入图片描述

6.2 启动项目

启动三个微服务

在这里插入图片描述

在这里插入图片描述

6.3 模拟正常下单

http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

在这里插入图片描述

数据库情况

在这里插入图片描述
可以看到,正常下单是没有问题的,库存减10,账户月减少100,订单状态变为1

6.4 模拟超时异常,不加@GlobalTransactional注解

AccountServiceImpl

在这里插入图片描述
由于20秒已经超过了OpenFeign的超时时间,所以会执行失败。

再访问:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
在这里插入图片描述
数据库情况

在这里插入图片描述

没下单成功竟然减掉了库存和用户的账户余额。

6.5 模拟超时异常,添加@GlobalTransactional注解

AccountServiceImpl

OrderServiceImpl添加@GlobalTransactional注解,这个注解的作用是只要发生异常就回滚

在这里插入图片描述
再次访问:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

可以看到,下单后数据库数据并没有任何改变,数据都没有添加进来,分布式事务回滚测试是成功的。

6.6 @GlobalTransactional 注解

  在分布式微服务架构中,我们可以使用 Seata 提供的 @GlobalTransactional 注解实现分布式事务的开启、管理和控制。

  当调用 @GlobalTransaction 注解的方法时,TM 会先向 TC 注册全局事务,TC 生成一个全局唯一的 XID,返回给 TM。

  @GlobalTransactional 注解既可以在类上使用,也可以在类方法上使用,该注解的使用位置决定了全局事务的范围,具体关系如下:

  • 在类中某个方法使用时,全局事务的范围就是该方法以及它所涉及的所有服务。
  • 在类上使用时,全局事务的范围就是这个类中的所有方法以及这些方法涉及的服务。

6.7 再看TC/TM/RM三个组件

在这里插入图片描述

处理过程:

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
  2. XID 在微服务调用链路的上下文中传播;
  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
  4. TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值