自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

分布式事務神器!Seata如何保證跨庫操作數(shù)據(jù)零差錯?原理+實戰(zhàn)全解析!

開發(fā) 前端
眾所周知,在微服務系統(tǒng)中,不同的服務通常對應的數(shù)據(jù)庫也不一樣,因此當涉及到多個服務的數(shù)據(jù)操作時,必然會存在跨庫提交數(shù)據(jù)的現(xiàn)象。

一、背景介紹

在之前的文章中,我們簡單介紹了一下 Spring Cloud Alibaba 的技術體系中的 Nacos、Dubbo 和 Sentinel 組件應用,通過這幾款組件基本可以構建一個簡易版的微服務框架系統(tǒng)。

我們知道,在微服務系統(tǒng)中一些模塊通常會以一個獨立的服務來開發(fā)和部署,比如用戶服務、訂單服務、庫存服務、賬單服務等等。隨著服務拆分的越來越細,微服務的數(shù)量也會隨之增長,系統(tǒng)的復雜度也會變得很高。例如,當用戶選擇某個商品下一筆單的時候,通常會先調用庫存服務的庫存扣減邏輯,如果庫存充足,接著再調用訂單服務的創(chuàng)建訂單邏輯。看似一個簡單的操作,其實至少會經過兩個微服務的數(shù)據(jù)寫入動作。從業(yè)務角度來看,這一系列的服務操作,要么一起成功,要么一起失敗,才能保障系統(tǒng)數(shù)據(jù)的完整性。

眾所周知,在微服務系統(tǒng)中,不同的服務通常對應的數(shù)據(jù)庫也不一樣,因此當涉及到多個服務的數(shù)據(jù)操作時,必然會存在跨庫提交數(shù)據(jù)的現(xiàn)象。

當請求一切都正常的時候,還好說;但是當某個服務節(jié)點出現(xiàn)異常時,就不好說了。比如調用庫存服務的庫存扣減邏輯成功了,但再調用訂單服務的創(chuàng)建訂單邏輯失敗了,實際上這個訂單沒有創(chuàng)建成功,但是庫存已經成功扣減了,從業(yè)務角度來看,庫存數(shù)據(jù)顯然并不正確。當創(chuàng)建訂單時如果出現(xiàn)失敗,訂單服務需要反向調用庫存增加邏輯將扣減的庫存進行回滾,以便保證系統(tǒng)數(shù)據(jù)的正確性。

以上還只是介紹了兩個服務的數(shù)據(jù)操作,而實際上在業(yè)務開發(fā)過程中,有的請求操作可能涉及到好幾個微服務的數(shù)據(jù)寫入動作,如果最后一個服務節(jié)點出現(xiàn)異常,意味著前面執(zhí)行過的服務都得進行類似的數(shù)據(jù)回滾操作,通過人工寫異常代碼進行數(shù)據(jù)回滾顯然很雞肋。

在微服務環(huán)境下,有沒有一種工具能幫助我們解決在跨庫提交時數(shù)據(jù)不一致的問題呢?

答案肯定是有的,Spring Cloud Alibaba 技術生態(tài)中的 Seata 組件就可以幫忙我們解決這類問題。

二、Seata 簡介

什么是 Seata?

Seata 是一款開源的分布式事務解決方案,由阿里巴巴集團開發(fā)并開源,旨在解決微服務架構下的分布式事務問題。它提供了高性能和易用性,同時支持多種事務模式,能幫助開發(fā)者在分布式系統(tǒng)中實現(xiàn)數(shù)據(jù)一致性。

下面我們一起來簡單的了解一下它的架構設計。

2.1、Seata 架構圖

在 Seata 的架構中,一共有三個角色:

圖片圖片

各個角色承擔著不同的用途:

  • TC (Transaction Coordinator) :也被稱為事務協(xié)調者,負責維護全局和分支事務的狀態(tài),驅動全局事務提交或回滾。
  • TM (Transaction Manager) :也被稱為事務管理器,負責定義全局事務的范圍,開始全局事務、提交或回滾全局事務。
  • RM ( Resource Manager ) :也被稱資源管理器,負責管理分支事務處理的資源( Resource ),與 TC 交談以注冊分支事務和報告分支事務的狀態(tài),并驅動分支事務提交或回滾。

其中,TC 為單獨部署的Server 服務端,TM 和 RM 為嵌入到應用中的 Client 客戶端。

在 Seata 中,一個分布式事務的生命周期可以用如下圖來概括:

圖片圖片

具體的執(zhí)行過程,可以用如下幾個步驟來概括:

  • 第一步:當某個服務開啟分布式事務操作時,TM 會向 TC 發(fā)起請求開啟一個全局事務,TC 會生成一個 XID 作為該全局事務的編號并返回給 TM。此時 XID 會在微服務的調用鏈路中傳播,保證將多個微服務的子事務關聯(lián)在一起。
  • 第二步:接著 RM 會向 TC 發(fā)起請求將本地事務注冊為全局事務的一個分支事務,同時通過全局事務的 XID 將其關聯(lián)。
  • 第三步:當服務執(zhí)行完成以后,TM 會向 TC 發(fā)起請求告訴 XID 對應的全局事務是進行提交還是回滾操作。
  • 第四步:最后,TC 會向 RM 們發(fā)起請求將 XID 對應的自己的本地事務進行提交還是回滾操作。
  • 第五步:如果其中有任何一個分支事務操作出現(xiàn)了異常,TC 會將其記錄下來,以便于人工介入。

2.2、事務模式

為了打造一站式的分布事務解決方案,Seata 為用戶提供了  AT、TCC、SAGA 和 XA 四種事務模式。

目前使用的流行度情況是:AT > TCC > Saga。因此在學習 Seata 的時候,我們可以重點關注一下 AT 模式,搞懂背后的實現(xiàn)原理即可。

下面我們簡單的介紹一下 AT 模式實現(xiàn)原理,其它的模式實現(xiàn)大家可以自行參閱官方Seata 文檔。

2.2.1、AT 模式實現(xiàn)原理

AT 模式是一種無侵入的分布式事務解決方案。在 AT 模式下,每個數(shù)據(jù)庫會被當做是一個 Resource,業(yè)務通過 JDBC 標準接口訪問數(shù)據(jù)庫資源時,Seata 框架會對所有請求進行攔截,并將業(yè)務數(shù)據(jù)和回滾日志記錄到本地數(shù)據(jù)庫,以便對當前事務的執(zhí)行情況做出相應的處理。

與其它的事務模式相比,AT 模式實現(xiàn)原理相對來說要簡單很多,而且不易出錯,并且使用上也非常的簡潔,只需要在對應的方法上添加@GlobalTransactional全局事務注解就可以開啟分布式事務操作,示例如下:

// 開啟一個全局事務,方法內的跨庫數(shù)據(jù)操作要么全部成功,要么全部失敗
@GlobalTransactional
public void purchase() {
    // 調用服務A
    serviceA.doSomething();
    // 調用服務B
    serviceB.doSomething();
}

在 AT 模式中,整個事務執(zhí)行過程,可以用兩個階段來概括。

  • 一階段:業(yè)務 SQL 執(zhí)行操作;
  • 二階段:Seata 框架根據(jù)一階段執(zhí)行情況自動進行事務的提交和回滾操作;

詳細的執(zhí)行流程如下!

2.2.1.1、一階段流程

在一階段,Seata 會攔截“業(yè)務 SQL”,解析 SQL 語義,找到“業(yè)務 SQL”要更新的業(yè)務數(shù)據(jù)。在業(yè)務數(shù)據(jù)被更新前,將其保存成“before image”,然后執(zhí)行“業(yè)務 SQL”,在業(yè)務數(shù)據(jù)更新之后,再將其保存成“after image”,并將業(yè)務數(shù)據(jù)和回滾日志記錄到本地日志表中。

以上操作全部在一個數(shù)據(jù)庫事務內完成,因此一階段操作的原子性可以得到保證。

具體執(zhí)行流程可以用如下圖來概括。

圖片圖片

2.2.1.2、二階段 - 提交流程

在二階段,如果是提交的話,因為“業(yè)務 SQL”在一階段已經提交至數(shù)據(jù)庫, 所以 Seata 框架只需將一階段保存的快照數(shù)據(jù)刪掉,完成數(shù)據(jù)清理即可。

圖片圖片

2.2.1.3、二階段 - 回滾流程

在二階段,如果是回滾的話,Seata 就需要回滾一階段已經執(zhí)行的“業(yè)務 SQL”,還原業(yè)務數(shù)據(jù),回滾方式便是用“before image”還原業(yè)務數(shù)據(jù)。但在還原前要首先要校驗臟寫,對比“數(shù)據(jù)庫當前業(yè)務數(shù)據(jù)”和 “after image”,如果兩份數(shù)據(jù)完全一致就說明沒有臟寫,可以還原業(yè)務數(shù)據(jù),如果不一致就說明有臟寫,出現(xiàn)臟寫就需要轉人工處理。

具體執(zhí)行流程可以用如下圖來概括。

圖片圖片

AT 模式的一階段、二階段均由 Seata 框架自動生成,用戶只需編寫“業(yè)務 SQL”,便能輕松接入分布式事務,因此大家普遍認為 AT 模式是一種對業(yè)務無任何侵入的分布式事務解決方案,就像開啟本地事務操作一樣簡單。

介紹了這么多,如何在項目中使用 Seata 呢?下面我們一起來看看具體的實踐方式。

三、Seata 服務端部署

在使用 Seata 之前,我們需要先部署 Seata TC Server 服務,通過它來維護全局事務和分支事務的狀態(tài)等信息。

具體的部署方式如下。

3.1、單機部署

訪問https://github.com/seata/seata/releases,下載想要的 Seata 版本。

這里,我們選擇v1.5.2版本來安裝部署。

# 下載
$ wget https://github.com/seata/seata/releases/download/v1.5.2/seata-server-1.5.2.tar.gz

# 解壓
$ tar -zxvf seata-server-1.5.2.tar.gz

解壓之后,可以看到類似如下目錄結構:

圖片圖片

  • bin目錄存放的是 Seata 服務相關的啟動腳本;
  • conf目錄存放的是 Seata 服務相關的配置項;
  • lib目錄存放的是 Seata 服務相關的依賴庫;
  • log目錄存放的是 Seata 服務相關的啟動日志;
  • script目錄存放的是 Seata 服務相關的配置案例腳本;

Seata TC Server 的啟動方式非常簡單,只需要在bin目錄下執(zhí)行對應的腳本,就可以啟動服務。

  • mac/linux 系統(tǒng),執(zhí)行sh seata-server.sh,即可啟動服務;
  • windows 系統(tǒng),雙擊seata-server.bat,即可啟動服務;

啟動  Seata TC Server 后,默認的控制臺訪問端口是7091,在瀏覽器中訪問http://127.0.0.1:7091,如果看到如下界面,說明啟動成功了。

圖片圖片

默認的用戶名和密碼為seata,登陸之后會看到如下界面。

圖片圖片

如果無法訪問,請確認服務是否啟動成功,可以在log目錄下查看相關的啟動日志,排查具體的啟動情況。

3.2、集群部署

默認配置下,Seata TC Server 使用的是 file 模式存儲數(shù)據(jù),全局事務會話信息在內存中讀寫,并持久化到本地文件中,數(shù)據(jù)讀寫性能較高,常用于學習或測試環(huán)境使用,不適合生產環(huán)境中部署。

對于生產環(huán)境,通常我們會采用 db 數(shù)據(jù)庫來實現(xiàn)全局事務會話信息的共享,同時以集群的方式來部署,以便實現(xiàn)服務的高可用效果。

圖片圖片

實現(xiàn)也很簡單,以 Mysql 數(shù)據(jù)庫為例,具體實現(xiàn)步驟如下。

3.2.1、數(shù)據(jù)持久化存儲

首先,打開script目錄,找到mysql.sql文件,它是一個數(shù)據(jù)庫表初始化腳本,等會會用到它。

圖片圖片

然后,在Mysql數(shù)據(jù)庫中創(chuàng)建一個seata-server數(shù)據(jù)庫,并在該庫下執(zhí)行mysql.sql腳本,最終結果如下圖:

圖片圖片

接著,在 Seata 安裝包中打開conf/application.example.yml文件,找到store.db相關配置屬性。

圖片圖片

將其復制出來,然后拷貝到conf/application.yml文件中。

圖片圖片

這里需要重點注意一下。

  • 如果你的目標數(shù)據(jù)庫是 Mysql 5.x,對應的驅動配置類為com.mysql.jdbc.Driver;
  • 如果如果你的目標數(shù)據(jù)庫是 Mysql 8.x,對應的驅動配置類應為com.mysql.cj.jdbc.Driver;

如果配置錯誤,可能會導致啟動報錯,連接不上目標數(shù)據(jù)源。

最后,重啟 Seata TC Server 服務即可。

集群部署的方式也比較簡單,將安裝包部署在不同的機器上,共同連接一個目標數(shù)據(jù)源就可以了。

3.2.2、設置使用 Nacos 注冊中心

Seata TC Server 對主流的注冊中心也提供了集成,Seata 客戶端可以通過注冊中心獲取 Seata TC Server 所在的服務實例。

引入注冊中心之后,Seata 的交互流程可以用如下圖來概括。

圖片圖片

考慮到國內使用 Nacos 作為注冊中心比較廣泛,在這里我們簡單的介紹一下它的配置方式。

與上文類似,在 Seata 安裝包中打開conf/application.example.yml文件,找到store.registry相關配置屬性。

圖片圖片

將其復制出來,然后拷貝到conf/application.yml文件中。

圖片圖片

最后,重啟 Seata TC Server 服務即可。

訪問 nacos 的服務控制臺,如果看到 Seata 服務,說明服務注冊成功了。

圖片圖片

四、Seata 客戶端應用

Seata 服務端部署完成后,下面我們一起來看看應用服務如何接入 Seata。

目前 Seata 對主流的服務遠程調用框架都進行適配和支持,比如常用的 Dubbo、gRPC、Apache HttpClient、Spring Cloud OpenFeign、Spring RestTemplate 等,因此只需要在 SpringBoot 項目中引入seata-spring-boot-starter依賴包,Seata 客戶端會自動集成到項目中。

因為 Seata 是通過 DataSource 數(shù)據(jù)源進行代理實現(xiàn),所以天然對主流的 ORM 框架提供了非常好的支持,比如 JPA、MyBatis 等,當項目引入上文提到的 Seata 依賴包,Seata 會對服務進行自動裝配處理,無需在引入額外的包。

下面我們以用戶下單為例,先調用庫存服務進行扣減庫存,如果成功再調用訂單服務進行創(chuàng)建訂單,介紹一下如何使用 seata 來實現(xiàn)分布式事務提交和回滾操作。

服務之間的交互流程可以用如下圖來簡要概括。

圖片圖片

項目中用到的核心組件及對應的版本如下:

  • Spring Boot:2.2.5.RELEASE
  • Spring Cloud Alibaba:2.2.1.RELEASE
  • Mybatis:3.5.0
  • Mysql:8.0
  • Seata:1.1.0
  • Http Client:4.5.11

具體的實踐過程如下!

4.1、創(chuàng)建庫存服務

4.1.1、初始化數(shù)據(jù)庫

首先,創(chuàng)建一個seata-stock數(shù)據(jù)庫,并初始化相關業(yè)務表,示例如下:

CREATE TABLEIFNOTEXISTS`tb_stock` (
`id`int(11) NOTNULL AUTO_INCREMENT,
`product_code`varchar(255) DEFAULTNULL,
`count`int(11) DEFAULT0,
  PRIMARY KEY (`id`),
UNIQUEKEY (`commodity_code`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8;


INSERTINTO`tb_stock` (`id`, `product_code`, `count`)
VALUES (1, 'wahaha', 10);


CREATETABLEIFNOTEXISTS`undo_log`
(
    `branch_id`     BIGINT       NOTNULLCOMMENT'branch transaction id',
    `xid`           VARCHAR(128) NOTNULLCOMMENT'global transaction id',
    `context`       VARCHAR(128) NOTNULLCOMMENT'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOTNULLCOMMENT'rollback info',
    `log_status`    INT(11)      NOTNULLCOMMENT'0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOTNULLCOMMENT'create datetime',
    `log_modified`  DATETIME(6)  NOTNULLCOMMENT'modify datetime',
    UNIQUEKEY`ux_undo_log` (`xid`, `branch_id`)
    KEY`ix_log_created` (`log_created`)
) ENGINE = InnoDB AUTO_INCREMENT = 1DEFAULTCHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

其中,每個庫中必須包含undo_log表,它是 Seata AT 模式必須創(chuàng)建的表,主要用于分支事務的回滾。

4.1.2、創(chuàng)建服務應用

然后,建一個 Spring Boot 工程,命名為seata-client-order,并在pom.xml中引入相關的依賴內容,示例如下:

<properties>
    <spring-boot.version>2.2.5.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
    <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>

<dependencies>
    <!-- SpringBoot web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--mysql 驅動-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>
    <!--引入 seata 分布式事務組件 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <!-- 實現(xiàn) Seata 對 HttpClient 的集成支持  -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-http</artifactId>
        <version>1.1.0</version>
    </dependency>
    <!-- Apache HttpClient 依賴 -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.11</version>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <!-- 引入 springBoot 版本號 -->
        <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 版本號 -->
        <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 alibaba 適配的版本號 -->
        <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>
    </dependencies>
</dependencyManagement>

其中引入的seata-spring-boot-starter依賴,會自動裝載 Seata 客戶端相關配置。

4.1.3、編寫庫存扣減接口

接著,創(chuàng)建庫存扣減接口,以便用于發(fā)起調用,示例如下:

@RestController
@RequestMapping("/stock")
publicclass StockController {

    @Autowired
    private StockService stockService;

    @PostMapping("/deduct")
    public Boolean deduct(@RequestBody Stock stock) {
        try {
            stockService.deduct(stock.getProductCode(), stock.getCount());
            // 正??鄢龓齑妫祷?true
            returntrue;
        } catch (Exception e) {
            // 失敗扣除庫存,返回 false
            returnfalse;
        }
    }
}

對應的service層代碼如下:

@Component
publicclass StockService {

    @Autowired
    private StockMapper stockMapper;

    @Transactional
    public void deduct(String productCode, int count){
        Stock stock = new Stock();
        stock.setProductCode(productCode);
        stock.setCount(count);
        // 扣減庫存
        stockMapper.update(stock);
    }
}

對應的dao層  SQL 代碼如下:

<update id="update" parameterType="com.example.cloud.seata.client.entity.Stock">
    update tb_stock
    set `count` = `count` - #{count}
    where product_code = #{productCode}
</update>

4.1.4、創(chuàng)建服務啟動類

最后,創(chuàng)建服務啟動類。

@MapperScan("com.example.cloud.seata.client")
@SpringBootApplication
public class Application {

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

同時,創(chuàng)建全局配置文件并添加 seata 服務端相關的配置屬性,示例如下。

spring.application.name=seata-client-stock
server.port=9002

# 添加數(shù)據(jù)源配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata-stock
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# 配置mybatis全局配置文件掃描
mybatis.config-locatinotallow=classpath:mybatis/mybatis-config.xml
# 配置mybatis的xml配置文件掃描目錄
mybatis.mapper-locatinotallow=classpath:mybatis/mapper/*.xml

# 添加Seata 配置項
# Seata 應用編號,默認為spring.application.name
seata.application-id=seata-client-stock
# Seata 事務組編號,用于 TC 集群名
seata.tx-service-group=my_test_tx_group
# Seata 服務配置項,配置對應的虛擬組和分組的映射,其中127.0.0.1:8091為 seata 服務端的監(jiān)聽端口
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091

以上工程完成之后,將服務啟動起來。當看到如下信息,說明服務已經成功啟動。

圖片圖片

4.2、創(chuàng)建訂單服務

4.2.1、初始化數(shù)據(jù)庫

與上文類似,首先,創(chuàng)建一個seata-order數(shù)據(jù)庫,并初始化相關業(yè)務表,示例如下:

CREATE TABLE`tb_order` (
`id`intNOTNULL AUTO_INCREMENT,
`user_id`varchar(255) DEFAULTNULL,
`product_code`varchar(255) DEFAULTNULL,
`count`intDEFAULT'0',
`money`intDEFAULT'0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8;


CREATETABLEIFNOTEXISTS`undo_log`
(
    `branch_id`     BIGINT       NOTNULLCOMMENT'branch transaction id',
    `xid`           VARCHAR(128) NOTNULLCOMMENT'global transaction id',
    `context`       VARCHAR(128) NOTNULLCOMMENT'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOTNULLCOMMENT'rollback info',
    `log_status`    INT(11)      NOTNULLCOMMENT'0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOTNULLCOMMENT'create datetime',
    `log_modified`  DATETIME(6)  NOTNULLCOMMENT'modify datetime',
    UNIQUEKEY`ux_undo_log` (`xid`, `branch_id`)
    KEY`ix_log_created` (`log_created`)
) ENGINE = InnoDB AUTO_INCREMENT = 1DEFAULTCHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

4.2.2、創(chuàng)建服務應用

然后,建一個 Spring Boot 工程,命名為seata-client-order,依賴包與上文完全一致,這里就不再重復粘貼了。

4.2.3、編寫訂單創(chuàng)建接口

接著,創(chuàng)建一個下單接口,示例如下:

@RestController
@RequestMapping("/order")
publicclass OrderController {

    privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(OrderController.class);

    @Autowired
    private OrderService orderService;


    @GetMapping("/create")
    public String create(@RequestParam("userId") String userId,
                          @RequestParam("productCode") String productCode,
                          @RequestParam("orderCount") Integer orderCount) {
        try {
            orderService.create(userId, productCode, orderCount);
            return"訂單創(chuàng)建成功!";
        } catch (Exception e) {
            LOGGER.error("錯誤信息", e);
            return"訂單創(chuàng)建失??!";
        }
    }
}

對應的service層代碼如下:

@Component
publicclass OrderService {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * GlobalTransactional 表示當前服務需要開啟分布式事務操作,當通過 http 發(fā)起遠程調用的時候,seata 會將當前的全局事務會話 ID 傳遞到目標服務中。
     */
    @GlobalTransactional
    public void create(String userId, String productCode, int orderCount) throws Exception {
        // 扣減庫存
        reduceStock(productCode, orderCount);

        Order order = new Order();
        order.setUserId(userId);
        order.setProductCode(productCode);
        order.setCount(orderCount);
        order.setMoney(orderCount * 100);
        // 創(chuàng)建訂單
        orderMapper.insert(order);
    }


    /**
     * 通過 seata 包裝的 HttpClient 工具發(fā)起服務遠程調用
     */
    private void reduceStock(String productCode, Integer orderCount) throws IOException {
        // 參數(shù)拼接
        JSONObject params = new JSONObject()
                .fluentPut("productCode", productCode)
                .fluentPut("count", orderCount);
        // 執(zhí)行調用
        HttpResponse response = DefaultHttpExecutor.getInstance().executePost("http://127.0.0.1:9002", "/stock/deduct", params, HttpResponse.class);
        // 解析結果
        Boolean success = Boolean.valueOf(EntityUtils.toString(response.getEntity()));
        if (!success) {
            thrownew RuntimeException("扣減庫存失敗");
        }
    }
}

對應的dao層  SQL 代碼如下:

<insert id="insert" parameterType="com.example.cloud.seata.client.entity.Order">
    insert into tb_order(id, user_id, product_code, `count`, money)
    values(#{id}, #{userId}, #{productCode}, #{count}, #{money})
</insert>

可以發(fā)現(xiàn),在OrderService類的create()方法中多了一個@GlobalTransactional,它表示當前服務需要開啟分布式事務操作,當通過 http 發(fā)起遠程調用的時候,seata 會將當前的全局事務會話 ID 傳遞到目標服務中。

其次,在發(fā)起服務遠程調用時,需要用io.seata包中的Http工具來發(fā)起,因為它會將當前的全局事務會話 ID 信息作為頭部參數(shù),傳遞到目標服務中;如果使用 seata 不支持的組件,可能需要自行進行適配。

4.2.4、創(chuàng)建服務啟動類

最后,與上文類似,創(chuàng)建服務啟動類。

@MapperScan("com.example.cloud.seata.client")
@SpringBootApplication
public class Application {

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

同時,創(chuàng)建全局配置文件并添加 seata 服務端相關的配置屬性,示例如下。

spring.application.name=seata-client-order
server.port=9001

# 添加數(shù)據(jù)源配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata-order
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# 配置mybatis全局配置文件掃描
mybatis.config-locatinotallow=classpath:mybatis/mybatis-config.xml
# 配置mybatis的xml配置文件掃描目錄
mybatis.mapper-locatinotallow=classpath:mybatis/mapper/*.xml

# 添加Seata 配置項
# Seata 應用編號,默認為spring.application.name
seata.application-id=seata-client-order
# Seata 事務組編號,用于 TC 集群名
seata.tx-service-group=my_test_tx_group
# Seata 服務配置項,配置對應的虛擬組和分組的映射,其中127.0.0.1:8091為 seata 服務端的監(jiān)聽端口
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091

4.3、服務測試

最后,將seata-client-order和seata-client-stock服務啟動起來,我們一起來驗證一下如下兩種情況,是否都能如期完成。

  • 分布式事務正常提交
  • 分布式事務異常回滾

4.3.1、分布式事務正常提交

首先,我們一起看看數(shù)據(jù)庫中原始數(shù)據(jù)情況。

  • seata-stock庫中的庫存數(shù)據(jù)

圖片圖片

  • seata-order庫中的訂單數(shù)據(jù)

圖片圖片

接著,在瀏覽器中訪問http://127.0.0.1:9001/order/create?userId=張三&productCode=wahaha&orderCount=1,它會執(zhí)行如下兩個動作:

  • 第一個:調用庫存服務,將產品產品編碼為wahaha的庫存減 1;
  • 第二個:如果庫存扣減成功,插入一條產品編碼為wahaha數(shù)量為 1 的訂單信息;

發(fā)起接口請求后,我們先來看看 Seata TC Server 服務控制臺,可以看到類似如下的全局事務注冊信息。

圖片圖片

圖片圖片

正如上文所說,當某個方法開啟全局事務時,方法內所有的本地事務操作會先將自己的本地事務和全局事務ID進行關聯(lián),然后注冊到 Seata TC Server,以便完成全局事務的后續(xù)處理。

再次回看數(shù)據(jù)庫,看看目標數(shù)據(jù)表中的數(shù)據(jù)情況。

  • seata-stock庫中的庫存數(shù)據(jù)

圖片圖片

  • seata-order庫中的訂單數(shù)據(jù)

圖片圖片

從數(shù)據(jù)結果來看,與預期一致。

當分布式事務提交成功后,對應的數(shù)據(jù)庫下的undo_log表日志數(shù)據(jù)也會被一并刪除。

如果想觀察undo_log日志的變動情況,可以將創(chuàng)建訂單完成之后,停頓幾秒,比如如下方式。

圖片圖片

然后再次發(fā)起下單請求,可以 seata 存儲的相關日志信息。

圖片圖片

與此同時,我們還可以通過查看服務的日志信息,來觀察分支事務的操作情況。

圖片圖片

圖片圖片

其中Branch commit result信息代表分支事務的二階段操作。

4.3.2、分布式事務異?;貪L

測試完正常流程之后,下面我們再來驗證一下異常流程。

修改OrderService類中create()方法代碼,在創(chuàng)建訂單完成之后,試圖拋出異常,測試一下扣減的庫存數(shù)據(jù)是否能正?;貪L。

圖片圖片

首先,我們還是對數(shù)據(jù)庫中原始數(shù)據(jù)進行截個圖。

  • seata-stock庫中的庫存數(shù)據(jù)

圖片圖片

  • seata-order庫中的訂單數(shù)據(jù)

圖片圖片

然后,再次在瀏覽器中訪問http://127.0.0.1:9001/order/create?userId=張三&productCode=wahaha&orderCount=1。

預期的結果是:兩個庫的數(shù)據(jù)應該都不會發(fā)生變化!

再次回看數(shù)據(jù)庫,觀察目標數(shù)據(jù)表中的數(shù)據(jù)情況。

  • seata-stock庫中的庫存數(shù)據(jù)

圖片圖片

  • seata-order庫中的訂單數(shù)據(jù)

圖片圖片

為了便于觀察數(shù)據(jù)變化,我們在上文拋異常的位置停頓了 5 秒。

過 5 秒后,再次回看數(shù)據(jù)庫表中的數(shù)據(jù)情況,結果如下。

  • seata-stock庫中的庫存數(shù)據(jù)

圖片圖片

  • seata-order庫中的訂單數(shù)據(jù)

圖片圖片

數(shù)據(jù)結果與預期一致。

訪問 Seata TC Server 服務控制臺,還可以看到全局事務的回滾狀態(tài)。

圖片圖片

五、小結

最后總結一下,Seata 是阿里開源的一款開源的分布式事務解決方案,致力于提供高性能和簡單易用的分布式事務服務。在微服務項目應用比較廣泛,尤其是 AT 事務模式,深受歡迎,因此掌握 Seata 的實現(xiàn)原理及使用方式,能有效的幫助我們解決微服務中的分布式事務問題。

由于篇幅較長,如果有描述不對的地方,歡迎大家留言指正!

六、參考

1、https://seata.apache.org/zh-cn/docs/overview/what-is-seata/

2、https://www.iocoder.cn/Seata/install/

3、https://www.iocoder.cn/Spring-Boot/Seata/

4、https://www.cnblogs.com/sanshengshui/p/14169121.html

責任編輯:武曉燕 來源: 潘志的技術筆記
相關推薦

2022-06-21 08:27:22

Seata分布式事務

2022-07-10 20:24:48

Seata分布式事務

2022-01-12 10:02:02

TCC模式 Seata

2021-04-23 08:15:51

Seata XA AT

2022-06-27 08:21:05

Seata分布式事務微服務

2022-03-24 07:51:27

seata分布式事務Java

2024-01-26 13:17:00

rollbackMQ訂單系統(tǒng)

2021-08-06 08:33:27

Springboot分布式Seata

2020-11-27 07:01:44

分布式事務

2024-10-09 14:14:07

2025-04-30 10:44:02

2023-11-06 13:15:32

分布式事務Seata

2019-08-19 10:24:33

分布式事務數(shù)據(jù)庫

2025-04-01 01:04:00

Redis集群緩存

2024-08-19 09:05:00

Seata分布式事務

2020-04-28 12:18:08

Seata模式分布式

2025-01-26 00:00:40

Seata分布式事務

2020-11-16 08:56:02

Python

2023-01-06 09:19:12

Seata分布式事務

2025-04-28 00:44:04

點贊
收藏

51CTO技術棧公眾號