13.SpringCloudSeata處理分散式事務

长名06發表於2024-12-05

分散式事務(引入)

面試題

你簡歷上寫用微服務boot/cloud做過專案,你不可能只有一個資料庫吧?請你談談多個資料庫之間,你如何處理分散式事務?

舉例:在訂單支付成功後,交易中心會呼叫訂單中心的服務把訂單狀態更新,並呼叫物流中心的服務通知商品發貨,同時還要呼叫積分中心的服務為使用者增加相應的積分。如何保障分散式事務一致性,成為了確保訂單業務穩定執行的核心訴求之一。

阿里巴巴的Seata-AT模式如何做到對業務的無侵入?
對於分散式事務,你知道的解決方案有那些?請你談談?
  • 2PC(兩階段提交)
  • 3PC(三階段提交)
  • TCC方案[TCC(Try-Confirm-Cancel)被稱為補償事務),類似2PC的柔性分散式解決方案,2PC改良版]
  • LocalMessage本地訊息表
  • 獨立訊息微服務 + RabbitMQ/Kafka元件,實現可靠訊息最終一致性方案
  • 最大努力通知方案...

分散式事務問題,如何產生?先看業務

上述面試問題都指向一個重要問題?

一次業務操作需要跨多個資料來源或需要跨多個系統(多個程序)進行遠端呼叫,就會產生分散式事務問題,但是關係型資料庫提供的能力是基於單機事務的,一旦遇到分散式事務場景,就需要透過更多其他技術手段來解決問題。

微服務出現之前

單機單庫沒有分散式事務的問題。

微服務出現之後

單體應用被拆分成微服務應用,原來的三個模組,被拆分成三個對立的應用,分別使用三個獨立的資料來源,業務操作需要呼叫三個服務來完成,此時每個服務自己內部的資料一致性由本地事務來保證,但是全域性的資料一致性問題無法保證。

結論

迫切希望提供一種分散式事務框架,解決微服務架構下的分散式事務問題。

1.Seata簡介

1.1 是什麼

Simple Extensible Autonomous Transaction Architecture(簡單可擴充套件自治事務框架) -- Seata

1.1.1 官網解釋

Apache Seata(incubating) 是一款開源的分散式事務解決方案,致力於在微服務架構下提供高效能和簡單易用的分散式事務服務。

阿里,已經將其捐獻給了Apache基金會。

1.1.2 發展歷程

阿里巴巴作為國內最早一批進行應用分散式(微服務化)改造的企業,很早就遇到微服務架構下的分散式事務問題。

2019年1月份螞蟻金服和阿里巴巴共同開源的分散式事務解決方案:

2014 年,阿里中介軟體團隊釋出 TXC(Taobao Transaction Constructor),為集團內應用提供分散式事務服務。

2016 年,TXC 在經過產品化改造後,以 GTS(Global Transaction Service) 的身份登陸阿里雲,成為當時業界唯一一款雲上分散式事務產品。在阿雲裡的公有云、專有云解決方案中,開始服務於眾多外部客戶。

2019 年起,基於 TXC 和 GTS 的技術積累,阿里中介軟體團隊發起了開源專案 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社群一起建設這個分散式事務解決方案。

2019 年 fescar(全稱fast easy commit and rollback) 被重新命名為了seata(simple extensiable autonomous transaction architecture)。TXC、GTS、Fescar 以及 seata 一脈相承,為解決微服務架構下的分散式事務問題交出了一份與眾不同的答卷。

1.2 功能

Seata是一款開源的分散式事務解決方案,致力於在微服務架構下提供高效能和簡單易用的分散式事務服務。

1.3 下載地址

官網

GitHub

1.4 怎麼用

Spring中的本地事務@Transactional,全域性事務@GlobalTransactional

Seata的分散式事務解決方案

2.Seata工作流程簡介(原理)

2.1 分散式事務的管理,就是全域性事務id的傳遞和變更,要讓開發者無感知

2.2 Seata對分散式事務的協調和控制就是1 + 3

2.2.1 1個XID

XID是全域性事務的唯一標識,它可以在服務的呼叫鏈路中傳遞,繫結到服務的事務上下文中。

2.2.2 官網版3個概念

2.2.3 周陽老師對Seata術語的解釋

TC(Tansaction Coordinator):事務協調器

就是Seata元件,負責維護全域性事務和分支事務的狀態,驅動全域性事務提交或回滾。

TM(Transaction Manager):事務管理器

標註全域性@GlobalTransactional啟動入口的微服務模組(介面),它是全域性事務的發起者,負責定義全域性事務的範圍,並根據TC維護的全域性事務和分支事務狀態,做出開始事務、提交事務、回滾事務的決議。

RM(Resource Manager):資源管理器

就是關係型資料庫本身,可以時多個RM,負責管理分支事務上的資源,向TC註冊分支事務,彙報分支事務狀態,驅動分支事務的提交或回滾。

注意,Seata的不同模式,使用的儲存,執行中產生的的全域性事務資料的介質不同,有的是檔案,有的是關係型資料庫的表,而且使用關係型資料庫作為儲存介質時,所支援的關係型資料庫也不同,具體見官網資料來源支援,這也是使用Seata作為分散式解決方案的限制之一。

2.3 分散式事務的執行流程-小總結

三個元件相互協作,TC以Seata 伺服器(Server)形式獨立部署,TM和RM則是以Seata Client的形式整合在微服務中執行,

A typical lifecycle of Seata managed distributed transaction:

  1. TM asks TC to begin a new global transaction. TC generates an XID representing the global transaction.
  2. XID is propagated through microservices' invoke chain.
  3. RM registers local transaction as a branch of the corresponding global transaction of XID to TC.
  4. TM asks TC for committing or rollbacking the corresponding global transaction of XID.
  5. TC drives all branch transactions under the corresponding global transaction of XID to finish branch committing or rollbacking.

1.TM 向 TC 申請開啟一個全域性事務,全域性事務建立成功並生成一個全域性唯一的 XID;

2.XID 在微服務呼叫鏈路的上下文中傳播;

3.RM 向 TC 註冊分支事務,將其納入 XID 對應全域性事務的管轄;

4.TM 向 TC 發起針對 XID 的全域性提交或回滾決議;

5.TC 排程XID 下管轄的全部分支事務完成提交或回滾請求。

2.4 Seata的事務模式

AT模式


周陽老師,本次課程,只介紹了AT模式。理由是,日常工作,企業調研和本次課時安排限制。

3.Seata-Server2.0.0安裝

3.1 下載

見本文1.3 下載地址

3.2 Seata引數官網參考

https://seata.apache.org/zh-cn/docs/user/configurations

3.3 Seata新手部署指南

Seata分TC,TM和RM三個角色,TC(Server)端為單獨服務端部署,TM和RM(Client端)由業務系統整合。

官網新手部署指南

3.4 MySQL8.0資料庫建庫 + 建表

3.4.1 建立TC使用的庫
create database seata;

use seata;
3.4.2 在上一步seata庫裡建表

建表sql地址

--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License.  You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

-- -------------------------------- 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_status_gmt_modified` (`status` , `gmt_modified`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- 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 = utf8mb4;

-- 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),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);


CREATE TABLE IF NOT EXISTS `vgroup_table`
(
    `vGroup`    VARCHAR(255),
    `namespace` VARCHAR(255),
    `cluster`   VARCHAR(255),
  UNIQUE KEY `idx_vgroup_namespace_cluster` (`vGroup`,`namespace`,`cluster`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

3.5 更改配置

修改seata-server-2.0.0\conf\application.yml配置檔案,記得先備份(防止需要回退到原有的配置)

#  Copyright 1999-2019 Seata.io Group.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${user.home}/logs/seata}
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash
 
console:
  user:
    username: seata
    password: seata

seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace:
      group: SEATA_GROUP #後續自己在nacos裡面新建,不想新建SEATA_GROUP,就寫DEFAULT_GROUP
      username: nacos
      password: nacos
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP #後續自己在nacos裡面新建,不想新建SEATA_GROUP,就寫DEFAULT_GROUP
      namespace:
      cluster: default
      username: nacos
      password: nacos    

  store:
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
      user: root
      password: 123456
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 1000
      max-wait: 5000


  #  server:
  #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**

3.6 啟動Nacos2.2.3埠號8848

startup.cmd -m standalone

命令執行成功後訪問http://localhost:8848/nacos

3.7 啟動seata-server-2.0.0

進入seata軟體目錄\bin,啟動略

進入seata前端介面,http://localhost:7091

進入Nacos,服務列表,會有seata-server服務。

4.Seata案例實戰-資料庫和表準備

訂單 + 庫存 + 賬戶3個業務資料庫MySQL準備。

以下演示都需要先啟動Nacos後啟動Seata,保證兩個都OK。

4.1 分散式事務本案例業務說明

這裡我們建立三個服務,一個訂單服務,一個庫存服務,一個賬戶服務。**

當使用者下單時,會在訂單服務中建立一個訂單,然後透過遠端呼叫庫存服務來扣減下單商品的庫存,

再透過遠端呼叫賬戶服務來扣減使用者賬戶裡面的餘額,

最後在訂單服務中修改訂單狀態為已完成。該操作跨越三個資料庫,有兩次遠端呼叫,很明顯會有分散式事務問題。

4.2 建立3個業務資料庫DATABASE

seata_order:儲存訂單的資料庫;

seata_storage:儲存庫存的資料庫;

seata_account:儲存賬戶資訊的資料庫;

CREATE DATABASE seata_order;

CREATE DATABASE seata_storage;

CREATE DATABASE seata_account;

4.3 按照上述3庫分別建立對應的undo_log回滾日誌表

因為AT模式,是需要將分散式事務中,修改前後的資料,做一個快照,儲存到資料庫中,如果全域性事務失敗,分支(本地)事務需要回滾,方便修改本地事務已經提交的資料。所以,需要在單個微服務對應的資料庫中,建立回滾日誌表。成功的話,只要把記錄的這條資料刪除即可。全域性事務成功或失敗,沒必要保留此事務中的臨時資料。

4.3.1 undo_log表

https://github.com/apache/incubator-seata/blob/2.x/script/client/at/db/mysql.sql

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 = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

注意,在當前筆記中,是以AT模式的mysql資料來源,所以選擇的建表語句都是mysql版本的。不同資料來源見GitHub或Seata官網。

4.4 按照上述庫分別建立對應業務表和undo_log表

#訂單庫
CREATE DATABASE seata_order;
USE seata_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=1 DEFAULT CHARSET=utf8;


-- 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 = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

#賬號庫
CREATE DATABASE seata_account;
USE seata_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 t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');


-- 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 = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

#庫存庫
CREATE DATABASE seata_storage;
USE seata_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=1 DEFAULT CHARSET=utf8;
#插入一條假資料
INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');

-- 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 = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

4.5 最終效果

在MySQL8.0中有seata庫,seata_order庫,seata_storage庫,seata_account庫,其中有對應的表,表資訊略,見上sql語句。

5.Seata案例實戰-微服務編碼落地實現

案例需求:下訂單 -> 減庫存 -> 扣餘額 -> 改(訂單)狀態

5.1 使用外掛生成基礎類

使用最開始的generator模組,和之前生成t_pay表的基礎類相同。

5.1.1 config.properties
#t_pay表包名
package.name=com.atguigu.cloud

# mysql8.0
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456

# seata_order
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456

# seata_storage
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456

# seata_account
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
5.1.2 generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <properties resource="config.properties"/>

    <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
            <property name="caseSensitive" value="true"/>
        </plugin>

        <jdbcConnection driverClass="${jdbc.driverClass}"
                        connectionURL="${jdbc.url}"
                        userId="${jdbc.user}"
                        password="${jdbc.password}">
        </jdbcConnection>

        <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>

        <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>

        <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>

        <table tableName="t_pay" domainObjectName="Pay">
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>

        <!--  seata_order -->
        <!--<table tableName="t_order" domainObjectName="Order">
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>-->

        <!--seata_storage-->
        <!--<table tableName="t_storage" domainObjectName="Storage">
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>-->

        <!--seata_account-->
        <!--<table tableName="t_account" domainObjectName="Account">
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>-->

    </context>
</generatorConfiguration>
5.1.3 生成

注意,jdbc連線時,修改為自己建立的庫和賬號密碼,生成略。

5.2 修改公共cloud-api-commons新增庫存和賬號兩個Feign服務介面

package com.atguigu.cloud.apis;

import com.atguigu.cloud.resp.ResultData;
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 StorageFeignApi {
    /**
     * 扣減庫存
     */
    @PostMapping(value = "/storage/decrease")
    ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
package com.atguigu.cloud.apis;

import com.atguigu.cloud.resp.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-account-service")
public interface AccountFeignApi {
    //扣減賬戶餘額
    @PostMapping("/account/decrease")
    ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

5.3 新建訂單Order微服務

5.3.1 建Module

seata-order-service2001

5.3.2 改POM
 <dependencies>
        <!-- nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--alibaba-seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!--cloud-api-commons-->
        <dependency>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringBoot整合druid連線池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <!-- Swagger3 呼叫方式 http://你的主機IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
        <!--mybatis和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--Mysql資料庫驅動8 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
        </dependency>
        <!--通用Mapper4-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!-- fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
5.3.3 寫YML
server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服務註冊中心地址
  # ==========applicationName + druid-mysql8 driver===================
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: 123456
# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  configuration:
    map-underscore-to-camel-case: true

# ========================seata===================
seata:
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
      application: seata-server
  tx-service-group: default_tx_group # 事務組,由它獲得TC服務的叢集名稱
  service:
    vgroup-mapping: # 點選原始碼分析
      default_tx_group: default # 事務組與TC服務叢集的對映關係
  data-source-proxy-mode: AT

logging:
  level:
    io:
      seata: info

Nacos,為什麼設計Namespace + Group + DataId三者關係?

Nacos資料模型Key由三元組唯一確定,Namespace預設是空串,公共名稱空間(public),分組預設是DEFAULT_GROUP。

上圖落地的對應原始碼(筆記最下面還有):io.seata.spring.boot.autoconfigure.properties.client.ServiceProperties

@Override
public void afterPropertiesSet() throws Exception {
    if (0 == vgroupMapping.size()) {
        vgroupMapping.put(DEFAULT_TX_GROUP, DEFAULT_TC_CLUSTER);
        //compatible with old value, will remove next version
        vgroupMapping.put(DEFAULT_TX_GROUP_OLD, DEFAULT_TC_CLUSTER);
    }
    if (0 == grouplist.size()) {
        grouplist.put(DEFAULT_TC_CLUSTER, DEFAULT_GROUPLIST);
    }
}

上圖落地的對應原始碼:io.seata.common.DefaultValues

String DEFAULT_TX_GROUP = "default_tx_group"; //預設事務分組
@Deprecated
String DEFAULT_TX_GROUP_OLD = "my_test_tx_group";//之前版本的預設事務分組,已過時
String DEFAULT_TC_CLUSTER = "default";//預設叢集對映關係
String DEFAULT_GROUPLIST = "127.0.0.1:8091";

String DEFAULT_DATA_SOURCE_PROXY_MODE = "AT";//預設模式

詳細過度版的配置

#seata:
#  registry: # seata註冊配置
#    type: nacos # seata註冊型別
#    nacos:
#      application: seata-server #seata應用名稱
#      server-addr: 127.0.0.1:8848
#      namespace: ""
#      group: SEATA_GROUP
#      cluster: default
#  config:             # seata配置抓取
#    nacos:
#      server-addr: 127.0.0.1:8848
#      namespace: ""
#      group: SEATA_GROUP
#      username: nacos
#      password: nacos
#  tx-service-group: default_tx_group # 事務組,由它獲得TC服務的叢集名稱
#  service:
#    vgroup-mapping:
#      default_tx_group: default # 事務群組的對映配置關係
#  data-source-proxy-mode: AT
#  application-id: seata-server
5.3.4 主啟動
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服務註冊和發現
@EnableFeignClients
public class SeataOrderMainApp2001 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}
5.3.5 業務類

entities

package com.atguigu.cloud.entities;

import lombok.ToString;

import javax.persistence.*;
import java.io.Serializable;

/**
 * 表名:t_order
*/
@Table(name = "t_order")
@ToString
public class Order implements Serializable {
    @Id
    @GeneratedValue(generator = "JDBC")
    private Long id;

    /**
     * 使用者id
     */
    @Column(name = "user_id")
    private Long userId;

    /**
     * 產品id
     */
    @Column(name = "product_id")
    private Long productId;

    /**
     * 數量
     */
    private Integer count;

    /**
     * 金額
     */
    private Long money;

    /**
     * 訂單狀態:0:建立中;1:已完結
     */
    private Integer status;

    /**
     * @return id
     */
    public Long getId() {
        return id;
    }

    /**
     * @param id
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 獲取使用者id
     *
     * @return userId - 使用者id
     */
    public Long getUserId() {
        return userId;
    }

    /**
     * 設定使用者id
     *
     * @param userId 使用者id
     */
    public void setUserId(Long userId) {
        this.userId = userId;
    }

    /**
     * 獲取產品id
     *
     * @return productId - 產品id
     */
    public Long getProductId() {
        return productId;
    }

    /**
     * 設定產品id
     *
     * @param productId 產品id
     */
    public void setProductId(Long productId) {
        this.productId = productId;
    }

    /**
     * 獲取數量
     *
     * @return count - 數量
     */
    public Integer getCount() {
        return count;
    }

    /**
     * 設定數量
     *
     * @param count 數量
     */
    public void setCount(Integer count) {
        this.count = count;
    }

    /**
     * 獲取金額
     *
     * @return money - 金額
     */
    public Long getMoney() {
        return money;
    }

    /**
     * 設定金額
     *
     * @param money 金額
     */
    public void setMoney(Long money) {
        this.money = money;
    }

    /**
     * 獲取訂單狀態:0:建立中;1:已完結
     *
     * @return status - 訂單狀態:0:建立中;1:已完結
     */
    public Integer getStatus() {
        return status;
    }

    /**
     * 設定訂單狀態:0:建立中;1:已完結
     *
     * @param status 訂單狀態:0:建立中;1:已完結
     */
    public void setStatus(Integer status) {
        this.status = status;
    }
}

OrderMapper

OrderMapper介面

import com.atguigu.cloud.entities.Order;
import tk.mybatis.mapper.common.Mapper;

public interface OrderMapper extends Mapper<Order> {
}

resources資料夾下新建mapper資料夾後新增OrderMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.cloud.mapper.OrderMapper">
    <resultMap id="BaseResultMap" type="com.atguigu.cloud.entities.Order">
        <!--
          WARNING - @mbg.generated
        -->
        <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>

</mapper>

Service介面及實現

import com.atguigu.cloud.entities.Order;

public interface OrderService {
    /**
     * 建立訂單
     */
    void create(Order order);

}
import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.service.OrderService;

public class OrderServiceImpl implements OrderService {
       @Resource
    private OrderMapper orderMapper;
    @Resource//訂單微服務透過OpenFeign去呼叫庫存微服務
    private StorageFeignApi storageFeignApi;
    @Resource//訂單微服務透過OpenFeign去呼叫賬戶微服務
    private AccountFeignApi accountFeignApi;


    @Override
    @GlobalTransactional(name = "zzyy-create-order",rollbackFor = Exception.class) //AT
    public void create(Order order) {

        //XID檢查
        String xid = RootContext.getXID();//底層使用的ThreadLocal儲存xid資訊
        //1. 新建訂單
        log.info("==================>開始新建訂單"+"\t"+"xid_order:" +xid);
        //訂單狀態status:0:建立中;1:已完結
        order.setStatus(0);
        int result = orderMapper.insertSelective(order);

        //插入訂單成功後獲得插入mysql的實體物件
        Order orderFromDB = null;
        if(result > 0) {
            orderFromDB = orderMapper.selectOne(order);
            //orderFromDB = orderMapper.selectByPrimaryKey(order.getId());
            log.info("-------> 新建訂單成功,orderFromDB info: "+orderFromDB);
            System.out.println();
            //2. 扣減庫存
            log.info("-------> 訂單微服務開始呼叫Storage庫存,做扣減count");
            storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
            log.info("-------> 訂單微服務結束呼叫Storage庫存,做扣減完成");
            System.out.println();
            //3. 扣減賬號餘額
            log.info("-------> 訂單微服務開始呼叫Account賬號,做扣減money");
            accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
            log.info("-------> 訂單微服務結束呼叫Account賬號,做扣減完成");
            System.out.println();
            //4. 修改訂單狀態
            //訂單狀態status:0:建立中;1:已完結
            log.info("-------> 修改訂單狀態");
            orderFromDB.setStatus(1);

            Example whereCondition=new Example(Order.class);
            Example.Criteria criteria=whereCondition.createCriteria();
            criteria.andEqualTo("userId",orderFromDB.getUserId());
            criteria.andEqualTo("status",0);

            int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);

            log.info("-------> 修改訂單狀態完成"+"\t"+updateResult);
            log.info("-------> orderFromDB info: "+orderFromDB);
        }
        System.out.println();
        log.info("==================>結束新建訂單"+"\t"+"xid_order:" +xid);

    }
}

OrderController

import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.OrderService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    /**
     * 建立訂單
     */
    @GetMapping("/order/create")
    public ResultData create(Order order)
    {
        orderService.create(order);
        return ResultData.success(order);
    }
}

5.4 新建庫存Storage微服務

5.4.1 建Module

seata-storage-service2002

5.4.2 改POM
    <dependencies>
        <!-- nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--alibaba-seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!--cloud_commons_utils-->
        <dependency>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringBoot整合druid連線池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <!-- Swagger3 呼叫方式 http://你的主機IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
        <!--mybatis和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--Mysql資料庫驅動8 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
        </dependency>
        <!--通用Mapper4-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!-- fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
5.4.3 寫YML
server:
  port: 2002

spring:
  application:
    name: seata-storage-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服務註冊中心地址
  # ==========applicationName + druid-mysql8 driver===================
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: 123456
# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  configuration:
    map-underscore-to-camel-case: true
# ========================seata===================
seata:
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
      application: seata-server
  tx-service-group: default_tx_group # 事務組,由它獲得TC服務的叢集名稱
  service:
    vgroup-mapping:
      default_tx_group: default # 事務組與TC服務叢集的對映關係
  data-source-proxy-mode: AT

logging:
  level:
    io:
      seata: info
5.4.4 主啟動
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服務註冊和發現
@EnableFeignClients
public class SeataStorageMainApp2002 {
    public static void main(String[] args) {
        SpringApplication.run(SeataStorageMainApp2002.class, args);
    }
}
5.4.5 業務類

entities

package com.atguigu.cloud.entities;

import javax.persistence.*;

/**
 * 表名:t_storage
*/
@Table(name = "t_storage")
public class Storage {
    @Id
    @GeneratedValue(generator = "JDBC")
    private Long id;

    /**
     * 產品id
     */
    @Column(name = "product_id")
    private Long productId;

    /**
     * 總庫存
     */
    private Integer total;

    /**
     * 已用庫存
     */
    private Integer used;

    /**
     * 剩餘庫存
     */
    private Integer residue;

    /**
     * @return id
     */
    public Long getId() {
        return id;
    }

    /**
     * @param id
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 獲取產品id
     *
     * @return productId - 產品id
     */
    public Long getProductId() {
        return productId;
    }

    /**
     * 設定產品id
     *
     * @param productId 產品id
     */
    public void setProductId(Long productId) {
        this.productId = productId;
    }

    /**
     * 獲取總庫存
     *
     * @return total - 總庫存
     */
    public Integer getTotal() {
        return total;
    }

    /**
     * 設定總庫存
     *
     * @param total 總庫存
     */
    public void setTotal(Integer total) {
        this.total = total;
    }

    /**
     * 獲取已用庫存
     *
     * @return used - 已用庫存
     */
    public Integer getUsed() {
        return used;
    }

    /**
     * 設定已用庫存
     *
     * @param used 已用庫存
     */
    public void setUsed(Integer used) {
        this.used = used;
    }

    /**
     * 獲取剩餘庫存
     *
     * @return residue - 剩餘庫存
     */
    public Integer getResidue() {
        return residue;
    }

    /**
     * 設定剩餘庫存
     *
     * @param residue 剩餘庫存
     */
    public void setResidue(Integer residue) {
        this.residue = residue;
    }
}

Mapper

Mapper介面

import com.atguigu.cloud.entities.Storage;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;

public interface StorageMapper extends Mapper<Storage> {
    /**
     * 扣減庫存
     */
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);

}

resources資料夾下新建mapper資料夾後新增Mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.atguigu.cloud.mapper.StorageMapper">
  <resultMap id="BaseResultMap" type="com.atguigu.cloud.entities.Storage">
    <!--
      WARNING - @mbg.generated
    -->
    <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>

  <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.atguigu.cloud.mapper.StorageMapper;
import com.atguigu.cloud.service.StorageService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class StorageServiceImpl implements StorageService {

    @Resource
    private StorageMapper storageMapper;

    /**
     * 扣減庫存
     */
    @Override
    public void decrease(Long productId, Integer count) {
        log.info("------->storage-service中扣減庫存開始");
        storageMapper.decrease(productId, count);
        log.info("------->storage-service中扣減庫存結束");
    }
}

Controller

import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.OrderService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    /**
     * 建立訂單
     */
    @GetMapping("/order/create")
    public ResultData create(Order order)
    {
        orderService.create(order);
        return ResultData.success(order);
    }
}

5.5 新建賬戶Account微服務

5.5.1 建Module

seata-account-service2003

5.5.2 改POM
    <dependencies>
        <!-- nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--alibaba-seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!--cloud_commons_utils-->
        <dependency>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringBoot整合druid連線池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <!-- Swagger3 呼叫方式 http://你的主機IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
        <!--mybatis和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--Mysql資料庫驅動8 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
        </dependency>
        <!--通用Mapper4-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!-- fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
5.5.3 寫YML
server:
  port: 2003

spring:
  application:
    name: seata-account-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服務註冊中心地址
  # ==========applicationName + druid-mysql8 driver===================
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: xing
# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  configuration:
    map-underscore-to-camel-case: true



# ========================seata===================
seata:
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
      application: seata-server
  tx-service-group: default_tx_group # 事務組,由它獲得TC服務的叢集名稱
  service:
    vgroup-mapping:
      default_tx_group: default # 事務組與TC服務叢集的對映關係
  data-source-proxy-mode: AT

logging:
  level:
    io:
      seata: info
5.5.4 主啟動
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import tk.mybatis.spring.annotation.MapperScan;

@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
public class SeataAccountMainApp2003 {
   public static void main(String[] args) {
       SpringApplication.run(SeataAccountMainApp2003.class, args);
   }
}
5.5.5 業務類

entities

package com.atguigu.cloud.entities;

import lombok.ToString;

import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * 表名:t_account
*/
@Table(name = "t_account")
@ToString
public class Account {
    @Id
    @GeneratedValue(generator = "JDBC")
    private String id;

    private String username;

    private Double balance;

    /**
     * @return id
     */
    public String getId() {
        return id;
    }

    /**
     * @param id
     */
    public void setId(String id) {
        this.id = id;
    }

    /**
     * @return username
     */
    public String getUsername() {
        return username;
    }

    /**
     * @param username
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * @return balance
     */
    public Double getBalance() {
        return balance;
    }

    /**
     * @param balance
     */
    public void setBalance(Double balance) {
        this.balance = balance;
    }
}

Mapper

Mapper介面

package com.atguigu.cloud.mapper;

import com.atguigu.cloud.entities.Account;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;

public interface AccountMapper extends Mapper<Account> {
    /**
     * @param userId
     * @param money 本次消費金額
     */
    void decrease(@Param("userId") Long userId, @Param("money") Long money);
}

resources資料夾下新建mapper資料夾後新增Mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.cloud.mapper.AccountMapper">
  <resultMap id="BaseResultMap" type="com.atguigu.cloud.entities.Account">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="balance" jdbcType="DOUBLE" property="balance" />
  </resultMap>

  <!--
          money   本次消費金額

          t_account資料庫表
          total總額度 = 累計已消費金額(used) + 剩餘可用額度(residue)
      -->
  <update id="decrease">
    UPDATE
      t_account
    SET
      residue = residue - #{money},used = used + #{money}
    WHERE user_id = #{userId};
  </update>
</mapper>

Service介面及實現

package com.atguigu.cloud.service;

public interface AccountService {
    /**
     * 扣減賬戶餘額
     * @param userId 使用者id
     * @param money 本次消費金額
     */
    void decrease(Long userId, Long money);
}
package com.atguigu.cloud.service.impl;

import com.atguigu.cloud.mapper.AccountMapper;
import com.atguigu.cloud.service.AccountService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class AccountServiceImpl implements AccountService
{
    @Resource
    AccountMapper accountMapper;

    /**
     * 扣減賬戶餘額
     */
    @Override
    public void decrease(Long userId, Long money) {
        log.info("------->account-service中扣減賬戶餘額開始");

        accountMapper.decrease(userId,money);

        //myTimeOut();
        //int age = 10/0;
        log.info("------->account-service中扣減賬戶餘額結束");
    }

    /**
     * 模擬超時異常,全域性事務回滾
     */
    private static void myTimeOut()
    {
        try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

Controller

import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountController {

    @Resource
    AccountService accountService;

    /**
     * 扣減賬戶餘額
     */
    @RequestMapping("/account/decrease")
    public ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money){
        accountService.decrease(userId,money);
        return ResultData.success("扣減賬戶餘額成功!");
    }
}

6.Seata案例實戰-測試

這裡建議看,周陽老師原影片測試,影片比較直觀

6.1 啟動服務

啟動Nacos,之後啟動Seata,之後啟動2001,2002,2003服務。

6.2 資料庫初始情況

見本文,中插入初始表的sql語句。略

6.3 正常下單

下訂單 -> 減庫存 -> 扣餘額 -> 修改訂單狀態。

先測試,沒有在訂單模組,新增@GlobalTransactional註解

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

瀏覽器發起請求。

出現故障

解決方案,降低boot和cloud版本。

正常下單,第2次,正常執行。

新增一個訂單,和2個扣減。

6.4 超時異常出錯,未加@GlobalTransactional

修改

    @Override
    public void decrease(Long userId, Long money) {
        log.info("------->account-service中扣減賬戶餘額開始");

        accountMapper.decrease(userId,money);

        myTimeOut();
        //int age = 10/0;
        log.info("------->account-service中扣減賬戶餘額結束");
    }

重新下單,重新發起請求。

發現,庫存和賬戶金額扣減後,訂單狀態並沒有設定為已經完成,沒有從0改1。

6.5 超時異常解決,新增@GlobalTransactional

重新下單,保留了超時方法情況下。

檢視Seata的後臺,會發現,全域性事務id和全域性鎖資訊

超時結束後,3個庫的資料並無任何改變,被回滾了,包括記錄修改前後資訊的undo_log中的記錄也會被刪除。

7.Seata原理小總結

AT模式如何做到對業務的無侵入

這裡,強烈推薦,看Seata官網對於AT模式原理的介紹。

整體機制,是兩階段提交協議。

  • 一階段:業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。
  • 二階段:
    • 提交非同步化,非常快速的完成。
    • 回滾透過一階段的回滾日誌進行反向補償(注意,所謂的回滾類似與MySQL中的undolog日誌,是在原有的資料基礎上,再執行記錄的與原先修改相反的sql語句,完成最終資料的回滾)。

只是為了記錄自己的學習歷程,且本人水平有限,不對之處,請指正。

相關文章