天天看點

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

1.事務的介紹

1.1.什麼是本地事務?

當你需要一次執行多條SQL語句時,可以使用事務。通俗一點說,如果這幾條SQL語句全部執行成功,則才對資料庫進行一次更新,如果有一條SQL語句執行失敗,則這幾條SQL語句全部復原,這個時候需要用到事務。

劉德華《無間道》:去不了終點,回到原點

回顧一下資料庫事務的四大特性ACID:

原子性(Atomicity)
       要麼都執行,要麼都不執行

一緻性(Consistency)
       事務前後的資料都是正确的

隔離性(Isolation)
      事物之間互相隔離,互不幹擾(并發執行的事務彼此無法看到對方的中間狀态)

持久性(Durability)
       事務一旦送出不可再復原 
           

1.2.本地事務控制

在計算機系統中,更多的是通過關系型資料庫來控制事務,這是利用資料庫本身的事務特性來實作的,是以叫資料庫事務,由于應用主要靠關系資料庫來控制事務,而資料庫通常和應用在同一個伺服器,是以基于關系型資料庫的事務又被稱為本地事務。

1.2.1.資料庫本身控制事物:

begin transaction;
      //1.本地資料庫操作:儲存訂單
      //2.本地資料庫操作:扣減庫存
rollback;
或
commit transaction;
           

1.2.2.jdbc中使用事物:

1.擷取對資料庫的連接配接

2.設定事務不自動送出(預設情況是自動送出的)

3.把想要一次性送出的幾個sql語句用事務進行送出

try{
    Statement stmt = null; 
    stmt =conn.createStatement(); 
    stmt.executeUpdate(sql1); 
    int a=6/0;
    stmt.executeUpdate(Sql2); 
    . 
    . 
    . 
    conn.commit();   //使用commit送出事務 
}
           

4.捕獲異常,進行資料的復原(復原一般寫在catch塊中)

catch(Exception e) { 
   ... 
   conn.rollback(); 
}
           

1.2.3.aop控制事務:

@Transactional
begin tran
public void insertOrder(TbOrder tbOrder) {
    //1、儲存訂單
    tbOrderMapper.insertSelective(tbOrder);

    //2、扣減庫存
    itemServiceFeign.updateItem(tbOrder.getItemId(), tbOrder.getNum());
}
commit/rollback
           

1.3.分布式事務

随着網際網路的快速發展,軟體系統由原來的單體應用轉變為分布式應用,下圖描述了單體應用向微服務的演變:

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

分布式系統會把一個應用系統拆分為可獨立部署的多個服務,是以需要服務與服務之間遠端協作才能完成事務操作,這種分布式系統環境下由不同的服務之間通過網絡遠端協作完成事務稱之為分布式事務,例如使用者注冊送積分事務、建立訂單減庫存事務,銀行轉賬事務等都是分布式事務。

我們知道本地事務依賴資料庫本身提供的事務特性來實作,但是在分布式環境下,會變成下邊這樣:

@Transactional
@Override
public void insertOrder(TbOrder tbOrder) {
    //1、儲存訂單
    tbOrderMapper.insertSelective(tbOrder);

    //2、扣減庫存
    itemServiceFeign.updateItem(tbOrder.getItemId(), tbOrder.getNum());

    //模拟扣款失敗,此時扣減庫存會復原嗎?
    int a = 6/0;
}
           

還可以設想,當遠端調用扣減庫存成功了,由于網絡問題遠端調用并沒有傳回,此時本地事務送出失敗就復原了儲存訂單的操作,此時也不滿足一緻性。

是以在分布式架構的基礎上,傳統資料庫事務就無法使用了,訂單和庫存不在一個資料庫中甚至不在一個應用系統裡,實作業務需要通過遠端調用,由于網絡問題就會導緻分布式事務問題。

1.4.分布式事物産生的場景

1、典型的場景就是微服務架構 微服務之間通過遠端調用完成事務操作。 比如:訂單微服務和商品微服務,下單的同時訂單微服務請求商品微服務減庫存。簡言之:跨JVM程序産生分布式事務。

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

2、單體系統通路多個資料庫執行個體 當單體系統需要通路多個資料庫(執行個體)時就會産生分布式事務。 比如:使用者資訊和訂單資訊分别在兩個MySQL執行個體存儲,使用者管理系統删除使用者資訊,需要分别删除使用者資訊及使用者的訂單資訊,由于資料分布在不同的資料執行個體,需要通過不同的資料庫連結去操作資料,此時産生分布式事務。 簡言之:跨資料庫執行個體産生分布式事務。

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

3、多服務通路同一個資料庫執行個體 比如:訂單微服務和商品微服務即使通路同一個資料庫也會産生分布式事務,兩個微服務持有了不同的資料庫連結進行資料庫操作,此時産生分布式事務。簡言之:跨JVM程序産生分布式事務。

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

1.5.總結分布式事務産生的場景:

  • 1.一個服務,多個資料庫
  • 2.多個服務,一個資料庫
  • 3.多個服務,多個資料庫

2.分布式事務解決方案之seata

2.1.Seata介紹

2.1.1.Seata是什麼

​ 2019年1月,阿裡巴巴中間件團隊發起了開源項目 Fescar(Fast & EaSy Commit And Rollback),其願景是讓分布式事務的使用像本地事務的使用一樣,簡單和高效,并逐漸解決開發者們遇到的分布式事務方面的所有難題。後來更名為 Seata,意為:Simple Extensible Autonomous Transaction Architecture,是一套分布式事務解決方案。

官網:

http://seata.io/zh-cn/

2.2.2.Seata的分布式事務解決方案

Seata提供了四種不同的分布式事務解決方案:

  • **XA模式:**強一緻性分階段事務模式,犧牲了一定的可用性,無業務侵入.
  • **TCC模式:**最終一緻的分階段事務模式,有業務侵入
  • **AT(auto transaction)模式:**最終一緻的分階段事務模式,無業務侵入,也是Seata的預設模式. (一般都使用此模式)
  • **SAGA模式:**長事務模式,有業務侵入

2.2.2.Seata的核心元件

Seata事物管理中有三個重要的核心元件:

  • **TC(Transaction Coordinator)-事務協調者:**維護分支事務的狀态,協調全局事務送出或復原。
  • **TM(Transaction Manager)-事務管理器:**定義全局事務的範圍,并開始全局事務、送出或復原全局事務。
  • **RM(Resource Manager)-資料總管:**向TC注冊分支事務和報告分支事務的狀态,并驅動分支事務送出或復原。

TM是一個分布式事務的發起者和終結者,TC負責維護分布式事務的運作狀态,而RM則負責本地事務的運作并上報,如下圖所示:

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

2.2.AT模式的工作流程

下面我們通過一個分支事務的執行過程來了解 Seata 的工作流程。

例如有一個業務表 product(id,name),分支事務的業務邏輯:

update product set name = 'GTS' where name = 'TXC';
           

2.2.1.一階段

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

2.2.1.1.解析 SQL

得到 SQL 的類型(UPDATE),表(product),條件(where name = ‘TXC’)等相關的資訊。

2.2.1.2.查詢前鏡像

根據解析得到的條件資訊,生成查詢語句,定位資料。

select id, name from product where name = 'TXC';
           

得到前鏡像:

id name
1 TXC

2.2.1.3.執行業務 SQL

執行自己的業務邏輯:

update product set name = 'GTS' where name = 'TXC';
           

name

改為了

GTS

2.2.1.4.查詢後鏡像

根據前鏡像的結果,通過 主鍵 定位資料。

select id, name from product where id = 1;
           

得到後鏡像:

id name
1 GTS

2.2.1.5.插入復原日志

把前後鏡像資料以及業務 SQL 相關的資訊組成一條復原日志記錄,插入到

UNDO_LOG

表中。

2.2.1.6.注冊分支事務

RM向TC注冊分支事務将其其納入XID對應全局事務的管轄,并申請 product 表中主鍵值等于 1 的記錄的全局鎖 。

2.2.1.7.本地事務送出

業務資料的更新和前面步驟中生成的 UNDO LOG 一并送出。

2.2.1.8.上報事務狀态

将本地事務送出的結果上報給 TC。

2.2.2.二階段 - 復原

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

(1)收到 TC 的分支復原請求,開啟一個本地事務,執行如下操作。

(2)通過 XID 和 Branch ID 查找到相應的 UNDO LOG 記錄。

(3)資料校驗

拿 UNDO LOG 中的後鏡與目前資料進行比較,根據校驗結果決定是否做復原。

(4)根據 UNDO LOG 中的前鏡像和業務 SQL 的相關資訊生成并執行復原的語句:

update product set name = 'TXC' where id = 1;
           

(5)送出本地事務

并把本地事務的執行結果(即分支事務復原的結果)上報給 TC。

2.2.3.二階段 - 送出

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

第二階段全局事務送出,TC會通知各各分支參與者送出分支事務,在第一階段就已經送出了分支事務,這裡各各參與者隻需要删除undo_log即可,并且可以異步執行,第二階段很快可以完成。

2.3.Seata安裝和啟動

2.3.1.下載下傳

下載下傳位址:https://github.com/seata/seata/releases

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

2.3.2.安裝

  • 上傳并解壓安裝包:
[[email protected] ~]# cd /usr/upload
[[email protected] upload]# tar -zxvf seata-server-1.4.2.tar.gz -C /usr/local	
           

2.3.3.修改配置檔案

  • 修改

    seata/seata-server-1.4.2/conf/

    目錄下的

    registry.conf

    檔案:
  • 隻保留
    registry {
    #tc服務的注冊中心類型,其他服務要找到此Seata,這裡選擇nacos,也可以是eureka、zookeeper等
    type = "nacos"
    
    nacos {
      # seata tc服務注冊到nacos的服務名稱,可以自定義
      application = "seata-server"
      # nacos的位址
      serverAddr = "192.168.239.131:8848"
      # seata服務所在分組
      group = "DEFAULT_GROUP"
      # seata服務所在的名稱空間,這裡不填就是使用預設的"public"
      namespace = ""
      # TC叢集名,預設是"default"
      cluster = "default"
      # 這個是nacos的使用者名
      username = ""
      # 這個是nacos的密碼
      password = ""
    }
      }
      config {
    # tc服務的配置中心類型,可能搭叢集,統一管理配置檔案:file、nacos 、apollo、zk、consul、etcd3
    type = "nacos"
    
    nacos {
      serverAddr = "192.168.239.131:8848"
      namespace = ""
      group = "DEFAULT_GROUP"
      username = ""
      password = ""
      dataId = "seataServer.properties"
    }
      }  
               

2.3.4.在Nacos中添加配置資訊

  • 配置資訊位址:https://gitee.com/seata-io/seata/blob/develop/script/config-center/config.txt
    #資料存儲方式,db代表資料庫
      store.mode=db
      store.db.datasource=druid
      store.db.dbType=mysql
      store.db.driverClassName=com.mysql.jdbc.Driver
      store.db.url=jdbc:mysql://192.168.239.129:3306/seata?useUnicode=true&rewriteBatchedStatements=true
      store.db.user=root
      store.db.password=1111
      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
      # 事務、日志等配置
      server.recovery.committingRetryPeriod=1000
      server.recovery.asynCommittingRetryPeriod=1000
      server.recovery.rollbackingRetryPeriod=1000
      server.recovery.timeoutRetryPeriod=1000
      server.maxCommitRetryTimeout=-1
      server.maxRollbackRetryTimeout=-1
      server.rollbackRetryTimeoutUnlockEnable=false
      server.undo.logSaveDays=7
      server.undo.logDeletePeriod=86400000
      
      # 用戶端與服務端傳輸方式
      transport.serialization=seata
      transport.compressor=none
      # 關閉metrics功能,提高性能
      metrics.enabled=false
      metrics.registryType=compact
      metrics.exporterList=prometheus
      metrics.exporterPrometheusPort=9898
               
分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata
分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata
分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

2.3.5.建立TC服務資料表

先建立一個名為seata的資料庫,然後建表。

建表語句位址:https://gitee.com/seata-io/seata/blob/develop/script/server/db/mysql.sql

  • 全局事務表:global_table
  • 分支事務表:branch_table
  • 鎖表:lock_table

    – -------------- 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(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;

    分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

2.3.6.啟動Seata

配置好registry.conf檔案,在nacos中添加了配置檔案,創好資料庫和表就可以嘗試啟動Seata。

  • 啟動:./seata-server.sh 或 ./seata-server.sh -h 192.168.239.137 -p 8091
    [[email protected] local]# cd seata/bin
      [[email protected] bin]# ./seata-server.sh
      #或
      [[email protected] bin]# ./seata-server.sh -h 192.168.239.137 -p 8091
      ... ...
      io.seata.config.FileConfiguration        : The configuration file used is /usr/local/seata/seata-server-1.4.2/conf/registry.conf
       com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
      i.s.core.rpc.netty.NettyServerBootstrap  : Server started, listen port: 8091
               
  • 測試seata是否注冊成功,去nacos服務中心檢視
    分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata
  • 點選詳情檢視服務的具體資訊:
分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata
  • 關閉Seata:
    Ctrl+c  #可關閉Seata服務,關閉時間稍長。
               

3.使用Seata實作事務控制

3.1.建立微服務資料表

3.1.1.建立item服務資料表

  • 建庫
    分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

undo_log建表語句位址:https://gitee.com/seata-io/seata/blob/develop/script/client/at/db/mysql.sql

-- 商品表 ----------------------------
DROP TABLE IF EXISTS `tb_item`;
CREATE TABLE `tb_item` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `num` int(11) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- Records of tb_item ----------------------------
INSERT INTO `tb_item` VALUES ('1', '手機', '100');
INSERT INTO `tb_item` VALUES ('3', '電腦', '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 = utf8 COMMENT ='AT transaction mode undo table';
           

3.1.2.建立order服務資料表

  • 建庫
  • 建表語句
    -- 訂單表 Table structure for tb_order ----------------------------
      DROP TABLE IF EXISTS `tb_order`;
      CREATE TABLE `tb_order` (
        `id` int(11) NOT NULL AUTO_INCREMENT,
        `user_id` int(11) DEFAULT NULL,
        `item_id` int(11) DEFAULT NULL,
        `num` int(11) DEFAULT NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB 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 = utf8 COMMENT ='AT transaction mode undo table';
               
分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

3.2.建立工程

3.2.1.建立seata_demo

3.2.1.1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bjpowernode</groupId>
    <artifactId>seata_demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <!-- 自定義屬性标簽 -->
    <properties>
        <!-- 項目源碼及編譯輸出的編碼 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <!-- 項目編譯JDK版本 -->
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring-boot-version>2.3.2.RELEASE</spring-boot-version>
        <spring-cloud-version>Hoxton.SR9</spring-cloud-version>
        <spring-cloud-alibaba-version>2.2.6.RELEASE</spring-cloud-alibaba-version>
        <mybatis-version>3.5.1</mybatis-version>
        <mysql-connector-java-version>5.1.38</mysql-connector-java-version>
        <druid-version>1.0.9</druid-version>
        <spring-mybatis-version>2.0.1</spring-mybatis-version>
    </properties>
    <!--管理依賴版本号-->
    <dependencyManagement>
        <dependencies>
            <!--Spring Boot-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud Netflix-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud 阿裡巴巴-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- MyBatis -->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>${mybatis-version}</version>
            </dependency>
            <!-- MySql Driver -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java-version}</version>
            </dependency>
            <!--Alibaba DataBase Connection Pool-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid-version}</version>
            </dependency>
            <!--MyBatis And Spring Integration Starter-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${spring-mybatis-version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <resources>
            <!-- mapper.xml檔案在java目錄下 -->
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <!--解決maven項目resources目錄顯示為普通目錄問題-->
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
    </build>
</project>
           

3.2.2.建立common_pojo

3.2.2.1.pojo

逆向工程

3.2.3.建立seata_item_service

3.2.3.1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>seata_demo</artifactId>
        <groupId>com.bjpowernode</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata_item_service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </dependency>
        <!-- MySql Driver -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--Alibaba DataBase Connection Pool-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <!--MyBatis And Spring Integration Starter-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.bjpowernode</groupId>
            <artifactId>common_pojo</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
           

3.2.3.2.application.yml

server:
  port: 8091
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.239.129:3306/item?characterEncoding=UTF-8
    username: root
    password: 1111
    type: com.alibaba.druid.pool.DruidDataSource
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.239.131:8848
  application:
    name: seata-item-service
           

3.2.3.3.mapper

逆向工程

3.2.3.4.service

package com.bjpowernode.service;

import com.bjpowernode.mapper.TbItemMapper;
import com.bjpowernode.pojo.TbItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ItemServiceImpl implements ItemService {

    @Autowired
    private TbItemMapper tbItemMapper;

    @Override
    public void updateItem(Integer itemId, Integer num) {
        TbItem tbItem = tbItemMapper.selectByPrimaryKey(itemId);
        tbItem.setNum(tbItem.getNum()-num);
        tbItemMapper.updateByPrimaryKeySelective(tbItem);
    }
}
           

3.2.3.5.controller

package com.bjpowernode.controller;

import com.bjpowernode.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/item")
public class ItemController {

    @Autowired
    private ItemService itemService;

    @RequestMapping("/updateItem")
    public void updateItem(Integer itemId, Integer num){
        itemService.updateItem(itemId,num);
    }
}
           

3.2.3.6.app

package com.bjpowernode;

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

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjpowernode.mapper")
public class SeataItemServiceApp {
    public static void main(String[] args) {
        SpringApplication.run(SeataItemServiceApp.class, args);
    }
}
           

3.2.4.建立seata_item_feign

3.2.4.1.feign

package com.bjpowernode.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient("seata-item-service")
@RequestMapping("/item")
public interface ItemServiceFeign {

    @RequestMapping("/updateItem")
    public void updateItem(@RequestParam("itemId") Integer itemId, 
                           @RequestParam("num") Integer num);
}
           

3.2.4.1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>seata_demo</artifactId>
        <groupId>com.bjpowernode</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata_item_feign</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.bjpowernode</groupId>
            <artifactId>common_pojo</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
           

3.2.5.建立seata_order_service

3.2.3.1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>seata_demo</artifactId>
        <groupId>com.bjpowernode</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata_order_service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </dependency>
        <!-- MySql Driver -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--Alibaba DataBase Connection Pool-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <!--MyBatis And Spring Integration Starter-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.bjpowernode</groupId>
            <artifactId>seata_item_feign</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
           

3.2.3.2.application.yml

server:
  port: 8090
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.239.129:3306/order?characterEncoding=UTF-8
    username: root
    password: 1111
    type: com.alibaba.druid.pool.DruidDataSource
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.239.131:8848
  application:
    name: seata-order-service
           

3.2.3.3.mapper

逆向工程

3.2.3.4.service

package com.bjpowernode.service;

import com.bjpowernode.feign.ItemServiceFeign;
import com.bjpowernode.mapper.TbOrderMapper;
import com.bjpowernode.pojo.TbOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private TbOrderMapper tbOrderMapper;
    @Autowired
    private ItemServiceFeign itemServiceFeign;

    @Transactional
    @Override
    public void insertOrder(TbOrder tbOrder) {
        //1、儲存訂單
        tbOrderMapper.insertSelective(tbOrder);

        //2、扣減庫存
        itemServiceFeign.updateItem(tbOrder.getItemId(), tbOrder.getNum());

        //模拟扣款失敗,此時扣減庫存會復原嗎?
        int a = 6/0;
    }
}
           

3.2.3.5.controller

package com.bjpowernode.controller;

import com.bjpowernode.pojo.TbOrder;
import com.bjpowernode.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @RequestMapping("/insertOrder")
    public Map insertOrder(TbOrder tbOrder){
        HashMap<Integer, Object> result = new HashMap<>();
        try {
            orderService.insertOrder(tbOrder);
            result.put(200,"下單成功");
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            result.put(500,"下單失敗");
            return result;
        }
    }
}
           

3.2.3.6.app

package com.bjpowernode;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.bjpowernode.mapper")
public class SeataOrderServiceApp {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderServiceApp.class, args);
    }
}
           

3.2.6.測試

啟動後可檢視nacos的服務中心:

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata
  • 浏覽器通路:http://127.0.0.1:8090/order/insertOrder?userId=2&itemId=3&num=4
  • 分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata
  • 扣款失敗時,訂單事務復原,商品事務不復原,庫存丢失
    分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata
    分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

3.3.使用Seata分布式事務架構,來管理工程的步驟

3.3.1(加依賴)在pom.xml中加入Seata啟動器

在每個要使用的服務工程中都加上該啟動器

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

3.3.2.(加配置)在application.yml檔案中添加配置

在每個相關分布式事務服務的配置檔案添加下面配置

seata: #分布式事務架構Seata的配置
  registry:
    type: nacos #查找TC服務
    nacos:
      server-addr: 192.168.239.131:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-server #TC服務名
  tx-service-group: seata-demo #事務組名稱,根據tx-service-group名稱獲得TC服務cluster名稱
  service:
    vgroup-mapping: #事務組與TC cluster的映射關系
      seata-demo: default
           

3.3.3.(加注解)需要的方法上加@GlobalTransactional

在需要分布式事務的方法上加下面注解,用Seata管理

@GlobalTransactional
           
@Service
@Transactional
public class OrderServiceImpl implements OrderService {

    @Autowired
    private TbOrderMapper tbOrderMapper;
    @Autowired
    private ItemServiceFeign itemServiceFeign;

//添加注解@GlobalTransactional,開啟分布式事務
    @GlobalTransactional
    @Override
    public void insertOrder(TbOrder tbOrder) {
        //1、儲存訂單
        tbOrderMapper.insertSelective(tbOrder);

        //2、扣減庫存
        itemServiceFeign.updateItem(tbOrder.getItemId(), tbOrder.getNum());

        //模拟扣款失敗,此時扣減庫存會復原嗎?
        int a = 6/0;
    }
}
           

3.3.4.啟動項目服務會報的一個錯誤

連接配接不到Seata伺服器。

分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata
  • 因為這裡去連Seata的ip位址出現錯誤,是前面提到的錯誤,這個是Seata伺服器啟動沒有帶ip出現的錯誤。
    分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

3.3.5 解決方法:重新用帶ip的方式啟動Seata伺服器。

指令:

./seata-server.sh -h 192.168.239.137 -p 8091
           
分布式事務架構Seata1.事務的介紹2.分布式事務解決方案之seata

3.3.6.再次測試,有分布式事務管理就不會丢失庫存

  • 浏覽器通路:http://127.0.0.1:8090/order/insertOrder?userId=2&itemId=3&num=4

繼續閱讀