基於seata實現TCC分佈式事務解決方案

1、什麼是TCC事務

TCC是Try、Confirm、Cancel三個詞語的縮寫

TCC要求每個分支事務(即多個不同的數據庫實例)實現三個操作:預處理Try、確認 Confirm、撤銷Cancel。

Try操作做業務檢查及資源預留,Confirm做業務確認操作,Cancel實現一個與Try相反的 操作即回滾操作。TM首先發起所有的分支事務的try操作,任何一個分支事務的try操作執行失敗,TM將會發起所 有分支事務的Cancel操作,若try操作全部成功,TM將會發起所有分支事務的Confirm操作,其中Confirm/Cancel 操作若執行失敗,TM會進行重試

下面用幾張圖簡單描述一下TCC模式下的執行流程

TCC分爲三個階段:

  1. Try 階段是做業務檢查(一致性)及資源預留(隔離),此階段僅是一個初步操作,它和後續的Confirm 一起才能 真正構成一個完整的業務邏輯。

  2. Confirm 階段是做確認提交,Try階段所有分支事務執行成功後開始執行 Confirm。通常情況下,採用TCC則 認爲 Confirm階段是不會出錯的。即:只要Try成功,Confirm一定成功。若Confirm階段真的出錯了,需引 入重試機制或人工處理。

  3. Cancel 階段是在業務執行錯誤需要回滾的狀態下執行分支事務的業務取消,預留資源釋放。通常情況下,採 用TCC則認爲Cancel階段也是一定成功的。若Cancel階段真的出錯了,需引入重試機制或人工處理。

  4. TM事務管理器
    TM事務管理器可以實現爲獨立的服務,也可以讓全局事務發起方充當TM的角色,TM獨立出來是爲了成爲公 用組件,是爲了考慮系統結構和軟件複用

    TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分佈式事務調用鏈條,用來記錄事務上下文, 追蹤和記錄狀態,由於Confirm 和cancel失敗需進行重試,因此需要實現爲冪等,冪等性是指同一個操作無論請求 多少次,其結果都相同。

在這裏插入圖片描述

在這裏插入圖片描述

上面的執行是在操作成功的情況下,如果在分支事務執行失敗的情況下,則會出現下面的情況:
在這裏插入圖片描述

即當某個事務分支執行失敗的時候,這時會觸發cancel
在這裏插入圖片描述

最後我們再用簡單的話總結一下TCC的執行過程:

  • TCC是Try - 嘗試、Confirm - 確認、Cancel - 取消
  • Try嘗試階段,對資源進行鎖定
  • Confirm確認階段,對資源進行確認,完成操作
  • Cancel取消階段,對資源進行還原,取消操作

其實在之前的篇章中,我們介紹過關於seata的概念,把seata的相關概念放到這裏進行類比理解就很容易理解TCC 的執行流程,但需要說明的是,TCC模式下達到的效果是確保分佈式事務的最終一致性

2、TCC 解決方案

目前市面上的TCC框架衆多比如下幾種

框架名稱 Gitbub上star數量
tcc-transaction 3850
Hmily 2407
ByteTCC 1947
EasyTransaction 1690

關於上面幾種框架的技術和知識,有興趣的同學可以參閱相關資料查找學習,本篇會提到Hmily ,其實seata也提供了tcc的事務解決方案,打算對這兩種方式的TCC解決方案的代碼整合和演示進行說明

Hmily是一個高性能分佈式事務TCC開源框架。基於Java語言來開發(JDK1.8),支持Dubbo,Spring Cloud等 RPC框架進行分佈式事務。它目前支持以下特性:

  • 支持嵌套事務(Nested transaction support).

  • 採用disruptor框架進行事務日誌的異步讀寫,與RPC框架的性能毫無差別。

  • 支持SpringBoot-starter 項目啓動,使用簡單。

  • RPC框架支持 : dubbo,motan,springcloud。

  • 本地事務存儲支持 : redis,mongodb,zookeeper,file,mysql。

  • 事務日誌序列化支持 :java,hessian,kryo,protostuff。

  • 採用Aspect AOP 切面思想與Spring無縫集成,天然支持集羣。

  • RPC事務恢復,超時異常恢復等。
    Hmily利用AOP對參與分佈式事務的本地方法與遠程方法進行攔截處理,通過多方攔截,事務參與者能透明的 調用到另一方的Try、Confirm、Cancel方法;傳遞事務上下文;並記錄事務日誌,酌情進行補償,重試等。

  • Hmily不需要事務協調服務,但需要提供一個數據庫(mysql/mongodb/zookeeper/redis/file)來進行日誌存 儲。Hmily實現的TCC服務與普通的服務一樣,只需要暴露一個接口,也就是它的Try業務。Confirm/Cancel業務 邏輯,只是因爲全局事務提交/回滾的需要才提供的,因此Confirm/Cancel業務只需要被Hmily TCC事務框架 發現即可,不需要被調用它的其他業務服務所感知

3、基於seata 實現springboot與seata的整合

3.1 環境準備

資源名稱 版本
seata-server 1.0
mysql 5.7.25
zookeeper 3.4.6
dubbo 2.7.0

3.2 業務描述

本文實現一個下訂單減庫存的場景,3個主工程模塊,order - 訂單模塊,storage - 庫存模塊,代表2個分支事務,business - 業務實現模塊,即開啓全局事務的地方
在這裏插入圖片描述

3、數據庫準備

按照業務描述,我們需要創建兩個數據庫,在2個庫下分別保存着訂單表和庫存表,數據庫執行sql如下:

CREATE TABLE `tcc_order` (
  `order_id` int(255) NOT NULL AUTO_INCREMENT COMMENT '訂單編號',
  `order_code` varchar(255) DEFAULT NULL COMMENT '訂單編碼',
  `goods_code` varchar(32) NOT NULL COMMENT '商品編碼',
  `quantity` int(255) NOT NULL COMMENT '購買數量',
  `frozen_amount` float(255,0) NOT NULL DEFAULT '0' COMMENT '凍結金額 ',
  `amount` float(255,0) NOT NULL COMMENT '物品總價',
  `status` int(255) NOT NULL COMMENT '0-已創建 1-完成 2-取消',
  PRIMARY KEY (`order_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=113 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
CREATE TABLE `tcc_storage` (
  `storage_id` int(11) NOT NULL AUTO_INCREMENT,
  `goods_code` varchar(255) NOT NULL,
  `quantity` int(255) NOT NULL,
  `frozen_quantity` varchar(255) NOT NULL,
  PRIMARY KEY (`storage_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

在這裏插入圖片描述

4、啓動seata-server和zookeeper

通過之前的關於seata的學習我們知道,seata-server組作爲TC用於協調全局事務,即本身爲一個服務,從github上面下載下來之後,windows下直接進入bin目錄雙擊bat文件即可
在這裏插入圖片描述
zookeeper也是執行相同的操作,進入bin目錄
在這裏插入圖片描述

以上爲項目整合之前的環境準備,下面開始具體的項目搭建,採用聚合工程的方式搭建3個模塊,由於使用到了dubbo,因此我們決定將接口層抽離出來作爲一個單獨的模塊以便被其他模塊引用

因此整個工程結構包括4個模塊分別是:

  • order-provider [分支事務,服務提供方]
  • storage-provider[分支事務,服務提供方]
  • common
  • bussiness-consumer [全局事務發起方,服務消費方]

其中order-provide 和 storage-provider具有相似之處,在下面的介紹中我們會選取其中的一種重點說明

order-provider

pom依賴

<dependencies>
        <!--公共接口-->
        <dependency>
            <groupId>com.itlaoqi.seata.tcc</groupId>
            <artifactId>common</artifactId>
            <version>1.0.0-RELEASE</version>
        </dependency>

        <!-- SpringBoot Web(SpringMVC)模塊 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 利用JPA(Hibernate)進行數據庫讀寫 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>


        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!-- Dubbo RPC框架 -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <!-- Seata-all 1.0  -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!-- curator 是Zookeeper客戶端工具,curator-recipes提供了重試機制-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.1.0</version>
        </dependency>

        <!-- SpringTest單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

配置文件

在這裏插入圖片描述
由於採用seata,因此需要將file.conf和registry.conf兩個文件拷貝到resources目錄下,application.yml如下,file.conf和registry.conf直接從seata-server目錄拷貝過來暫時不做修改

spring:
  application:
    name: order-provider
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://106.15.37.145:3306/tcc_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  jpa:
    hibernate:
      naming:
        #開啓駝峯命名轉換
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
    show-sql: true
    #數據庫方言
    database-platform: org.hibernate.dialect.MySQLDialect
server:
  port: 8001

我們需要明白的是,order服務和storage服務各自提供一個操作數據庫的方法即可,然後在business服務中通過dubbo的形式調用order服務和storage服務的接口,因此還需要配置duboo文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:dubbbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo
       http://code.alibabatech.com/schema/dubbo/dubbo.xsd" default-autowire="byName">
    
    <!--服務名稱 -->
    <dubbo:application name="order-provider"/>

    <!--使用 zookeeper 註冊中心暴露服務,要先開啓 zookeeper-->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!-- 使用隨機端口暴露服務 -->
    <dubbo:protocol name="dubbo" port="-1"/>

    <!-- 服務提供者設置,提供固定10個線程提供服務,連接等待最大時長10秒,負載均衡策略爲“輪詢”,可以查閱資料進行自定義 -->
    <dubbo:provider timeout="10000" threads="10" threadpool="fixed" loadbalance="roundrobin"/>

    <!-- 描述服務提供者 -->
    <dubbo:service interface="com.congge.orderprovider.action.OrderAction" ref="orderActionImpl"/>

    <!-- 初始化Seata TCC全局事務掃描器 -->
    <bean class="io.seata.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="tcc-sample"/>
        <!-- 與file.conf的vgroup_mapping保持一致 -->
        <constructor-arg value="my_test_tx_group"/>
    </bean>

</beans>

下面進行編碼,在order端,即提供下訂單的服務接口

訂單order實體類


@Entity
@Table(name="tcc_order")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer orderId;
    private String orderCode; 
    private String goodsCode; 
    private Integer quantity; 
    private Float amount; 
    private Float frozenAmount; 
    private Integer status;

    public Integer getOrderId() {
        return orderId;
    }

    public void setOrderId(Integer orderId) {
        this.orderId = orderId;
    }

    public String getOrderCode() {
        return orderCode;
    }

    public void setOrderCode(String orderCode) {
        this.orderCode = orderCode;
    }

    public String getGoodsCode() {
        return goodsCode;
    }

    public void setGoodsCode(String goodsCode) {
        this.goodsCode = goodsCode;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }

    public Float getAmount() {
        return amount;
    }

    public void setAmount(Float amount) {
        this.amount = amount;
    }

    public Float getFrozenAmount() {
        return frozenAmount;
    }

    public void setFrozenAmount(Float frozenAmount) {
        this.frozenAmount = frozenAmount;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }
}

本文和數據庫交互採用的是jpa的形式,需提供一個OrderRepository

public interface OrderRepository extends JpaRepository<Order,Integer> {
}

接口都放在common工程的模塊中
在這裏插入圖片描述
order端暴露的服務接口如下:

public interface OrderAction {
    
    //Seata TCC在RM端核心註解,用於聲明TCC對應方法
    @TwoPhaseBusinessAction(name="TccOrderAction",commitMethod = "commit" , rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext,
                           @BusinessActionContextParameter(paramName = "orderCode") String orderCode,
                           @BusinessActionContextParameter(paramName = "goodsCode") String goodsCode,
                           @BusinessActionContextParameter(paramName = "quantity") int quantity,
                           @BusinessActionContextParameter(paramName = "amount") float amount
    );

    public boolean commit(BusinessActionContext actionContext);

    public boolean rollback(BusinessActionContext actionContext);
}

注意點:

這裏我們需要對在TCC的模式下進行編碼的一個說明,即一個具體的操作必須要有3個接口的支撐,即prepare,commit和rollback,命名可以自定,這樣的話,框架層(seata)在執行的時候才知道並且管理分支事務的執行狀態,其中BusinessActionContext 可以理解爲spring中的applicationContext,一種可以攜帶上下文信息並且在整個環境中傳遞事務狀態和參數的容器,通過BusinessActionContext 我們可以在commit和rollback階段拿到prepare中傳遞過來的參數信息

服務實現service

@Service("orderActionImpl")
public class OrderActionImpl implements OrderAction{

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private OrderRepository orderRepository;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean prepare(BusinessActionContext actionContext, String orderCode, String goodsCode, int quantity, float amount) {
        Order order = new Order();
        setOrder(order,orderCode,goodsCode,quantity,amount);
        orderRepository.save(order);
        logger.info("orderActionImpl分支事務已就緒, xid:" + actionContext.getXid());
        return true;
    }

    public void setOrder(Order order,String orderCode, String goodsCode, int quantity, float amount){
        order.setOrderCode(orderCode);
        order.setGoodsCode(goodsCode);
        order.setQuantity(quantity);
        order.setAmount(0f);
        order.setFrozenAmount(amount);
        order.setStatus(0);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean commit(BusinessActionContext actionContext) {
        String orderCode = (String)actionContext.getActionContext("orderCode");
        Order condition = new Order();
        condition.setOrderCode(orderCode);
        Example<Order> sample = Example.of(condition);
        Order order = orderRepository.findOne(sample).get();
        //冪等性校驗
        if(order.getStatus() == 1){
            return true;
        }
        order.setAmount(order.getFrozenAmount());
        order.setFrozenAmount(0f);
        //冪等性,做一次與做多次結果相同
        /**
         * 第一次:  FA: 100 ->   FA:0 A: 100
         * 第二次:  FA: 0  -> FA: 0 A: 0 //不具備冪等性
         */
        order.setStatus(1);
        orderRepository.save(order);
        logger.info("orderActionImpl分支事務已提交, xid:" + actionContext.getXid());
        return true;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean rollback(BusinessActionContext actionContext) {
        String orderCode = (String)actionContext.getActionContext("orderCode");
        Order condition = new Order();
        condition.setOrderCode(orderCode);
        Example<Order> sample = Example.of(condition);
        Order order = orderRepository.findOne(sample).get();
        //冪等性校驗
        if(order.getStatus() == 2){
            return true;
        }
        order.setAmount(0f);
        order.setFrozenAmount(0f);
        order.setStatus(2);
        orderRepository.save(order);
        logger.info("orderActionImpl分支事務已回滾, xid:" + actionContext.getXid());
        return true;
    }
}

啓動類

@SpringBootApplication
@ImportResource("classpath:provider/*.xml")
public class OrderProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderProviderApplication.class, args);
    }
}

可以對該服務實現進行單元測試

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {OrderProviderApplication.class})
public class OrderProviderTestor {
    @Autowired
    private OrderAction orderAction;

    @Test
    public void testPreare(){
        orderAction.prepare(new BusinessActionContext(), UUID.randomUUID().toString(), "juice", 10, 30f);
    }

    @Test
    public void testCommit(){
        BusinessActionContext context = new BusinessActionContext();
        Map map = new HashMap();
        map.put("orderCode", UUID.randomUUID().toString());
        context.setActionContext(map);
        orderAction.commit(context);
    }
}

order端的代碼基本上就是這些,下面順便貼出storage端的代碼,基本上和order端代碼類似,

appication.yml

spring:
  application:
    name: storage-provider
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://106.15.37.145:3306/tcc_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  jpa:
    hibernate:
      naming:
        #開啓駝峯命名轉換
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
    show-sql: true
    #設置數據庫方言
    database-platform: org.hibernate.dialect.MySQLDialect
server:
  port: 8002

dubbo配置文件

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:dubbbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo
       http://code.alibabatech.com/schema/dubbo/dubbo.xsd" default-autowire="byName">
    
    <!--服務名稱 -->
    <dubbo:application name="storage-provider"/>

    <!--使用 zookeeper 註冊中心暴露服務,注意要先開啓 zookeeper-->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!-- 使用隨機端口暴露服務 -->
    <dubbo:protocol name="dubbo" port="-1"/>

    <!-- 服務提供者設置,提供固定10個線程提供服務,連接等待最大時長10秒 -->
    <dubbo:provider timeout="10000" threads="10" threadpool="fixed" loadbalance="roundrobin"/>

    <dubbo:service interface="com.congge.storageprovider.action.StorageAction" ref="storageActionImpl"/>
    
    <!-- 初始化Seata TCC全局事務掃描器 -->
    <bean class="io.seata.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="tcc-sample"/>
        <!-- 與file.conf的vgroup_mapping保持一致 -->
        <constructor-arg value="my_test_tx_group"/>
    </bean>

</beans>

服務接口

public interface StorageAction {
    
    @TwoPhaseBusinessAction(name="TccStorageAction" ,commitMethod = "commit" , rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext context,
                           @BusinessActionContextParameter(paramName = "goodsCode") String goodsCode,
                           @BusinessActionContextParameter(paramName = "quantity") int quantity);
    public boolean commit(BusinessActionContext context);
    public boolean rollback(BusinessActionContext context);
}

接口實現類

@Service("storageActionImpl")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class StorageActionImpl implements StorageAction {
    Logger logger = LoggerFactory.getLogger(this.getClass());
    @Resource
    private StorageRepository storageRepository;

    @Override

    public boolean prepare(BusinessActionContext context, String goodsCode, int quantity) {
        Storage condition = new Storage();
        condition.setGoodsCode(goodsCode);
        Example<Storage> example = Example.of(condition);
        Optional<Storage> one = storageRepository.findOne(example);
        Storage storage = null;
        if(one.isPresent()){
            storage = one.get();
        }else{
            throw new RuntimeException("[" + goodsCode + "]商品編碼不存在");
        }

        if(quantity > storage.getQuantity()){
            throw new RuntimeException("[" + goodsCode + "]庫存數量不足" + quantity);
        }
        storage.setFrozenQuantity(quantity);
        storageRepository.save(storage);
        logger.info("StorageActionImpl分支事務已就緒, xid:" + context.getXid());
        return true;
    }

    @Override
    public boolean commit(BusinessActionContext context) {
            String goodsCode = (String)context.getActionContext("goodsCode");
        Storage condition = new Storage();
        condition.setGoodsCode(goodsCode);
        Storage storage = storageRepository.findOne(Example.of(condition)).get();
        storage.setQuantity(storage.getQuantity() - storage.getFrozenQuantity());
        storage.setFrozenQuantity(0);
        storageRepository.save(storage);
        logger.info("StorageActionImpl分支事務提交, xid:" + context.getXid());
        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext context) {
        String goodsCode = (String)context.getActionContext("goodsCode");
        Storage condition = new Storage();
        condition.setGoodsCode(goodsCode);
        Optional<Storage> one = storageRepository.findOne(Example.of(condition));
        Storage storage = null;
        if(!one.isPresent()){
            return true;
        }else{
            storage = one.get();
        }
        storage.setFrozenQuantity(0);
        storageRepository.save(storage);
        logger.info("StorageActionImpl分支事務回滾, xid:" + context.getXid());
        return true;
    }
}

business服務模塊

business爲全局事務的發起方,可以理解爲通過調用business的某接口實現全局事務的發起、提交與回滾,這裏爲了簡單模擬演示我們就不再操作數據庫

application.yml

spring:
  application:
    name: bussiness-consumer
server:
  port: 8000

dubbo配置,這裏是作爲消費端進行配置

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo
       http://code.alibabatech.com/schema/dubbo/dubbo.xsd" default-autowire="byName">

    <dubbo:application name="bussiness-consumer"/>
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
    <dubbo:protocol name="dubbo" port="-1"/>
    <dubbo:provider timeout="10000" threads="10" threadpool="fixed" loadbalance="roundrobin"/>

    <bean class="io.seata.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="tcc-sample"/>
        <constructor-arg value="my_test_tx_group"/>
    </bean>
    
    <!-- dubbo客戶端接口 -->
    <dubbo:reference id="orderAction" interface="com.congge.orderprovider.action.OrderAction"/>
    <dubbo:reference id="storageAction" interface="com.congge.storageprovider.action.StorageAction"/>

</beans>

在business中,只需要調用order和storage中提供的接口即可,因此這裏我們只需提供一個實現類,將order和storage的接口注入即可

@Service
public class BussinessService {

    @Resource(name="storageAction")
    private StorageAction storageAction;
    @Resource(name="orderAction")
    private OrderAction orderAction;

    /**
     * sale方法執行成功,TC通知RM執行confirm方法
     * sale方法拋出RuntimeException,TC通知RM執行cancel方法
     */
    @GlobalTransactional //開啓全局TCC分佈式事務
    public void sale(String orderCode,String goodsCode,
                     int quantity,float amount){
        orderAction.prepare(new BusinessActionContext(), orderCode, goodsCode, quantity, amount);
        storageAction.prepare(new BusinessActionContext(), goodsCode, quantity);
        if(quantity == 1000){
            throw new RuntimeException("unknown exception");
        }
    }
}

business暴露出一個對外的接口

@RestController
public class TestController {

    @Resource
    private BussinessService bussinessService;

    @GetMapping("/tcc1")
    public String test1(){
        String uuid = UUID.randomUUID().toString();
        bussinessService.sale(uuid,"coke",10,30);
        return "SUCCESS";
    }

    @GetMapping("/tcc2")
    public String test2(){
        String uuid = UUID.randomUUID().toString();
        bussinessService.sale(uuid,"coke",10000,30000);
        return "SUCCESS";
    }

    @GetMapping("/tcc3")
    public String test3(){
        String uuid = UUID.randomUUID().toString();
        bussinessService.sale(uuid,"coke",100,300);
        return "SUCCESS";
    }

}

啓動類

@SpringBootApplication
@ImportResource("classpath:consumer/*.xml")
public class BussinessConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(BussinessConsumerApplication.class, args);
    }
}

下面我們將3個服務模塊的工程運行起來

首先在數據庫的storage表中初始化一條庫存數據,即一個爲juice的商品有5000件
在這裏插入圖片描述

正常測試1:

調用接口1,請求的庫存數量沒有超過實際的庫存數量,可以執行成功,輸入:http://localhost:8000/tcc1
在這裏插入圖片描述
同時數據庫新增一條訂單記錄
在這裏插入圖片描述

異常測試2:

調用接口2,請求的庫存數量超過實際的庫存數量,理論上說,訂單服務成功,庫存扣減失敗,觸發全局事務回滾,最終數據庫庫存扣減不成功,同時新增訂單記錄的狀態值爲2取消狀態,輸入:http://localhost:8000/tcc2

可以看到接口執行失敗
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

如果想要更清楚的搞明白背後的執行原理,我們可以到控制檯查看輸出日誌,通過這些日誌我們可以清晰的瞭解到seata參與的過程中各個分支事務的具體運行操作的步驟
在這裏插入圖片描述

本篇的講解到此結束,後續我們將會講述如何使用Hmily完成TCC的整合和使用,本篇到此結束,最後感謝觀看!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章