微服務痛點-基於Dubbo + Seata的分散式事務(TCC模式)

三升水發表於2020-12-29

前言

Seata 是一款開源的分散式事務解決方案,致力於提供高效能和簡單易用的分散式事務服務。Seata 將為使用者提供了 AT、TCC、SAGA 和 XA 事務模式,為使用者打造一站式的分散式解決方案。

對於Seata不太瞭解的朋友,可以看下我之前寫的文章:

TCC模式

TCC模式怎麼理解

TCC(Try-Confirm-Cancel)實際上是服務化的兩階段提交協議,業務開發者需要實現這三個服務介面,第一階段服務由業務程式碼編排來呼叫 Try 介面進行資源預留,所有參與者的 Try 介面都成功了,事務管理器會提交事務,並呼叫每個參與者的 Confirm 介面真正提交業務操作,否則呼叫每個參與者的Cancel介面回滾事務。

Seata 框架把每組 TCC 介面當做一個 Resource,稱為 TCC Resource。這套 TCC 介面可以是 RPC,也以是服務內 JVM 呼叫。在業務啟動時,Seata 框架會自動掃描識別到 TCC 介面的呼叫方和釋出方。如果是 RPC 的話,就是 sofa:reference、sofa:service、dubbo:reference、dubbo:service 等。

掃描到 TCC 介面的呼叫方和釋出方之後。如果是釋出方,會在業務啟動時向 TC 註冊 TCC Resource,與 DataSource Resource 一樣,每個資源也會帶有一個資源 ID。

如果是呼叫方,Seata 框架會給呼叫方加上切面,與 AT 模式一樣,在執行時,該切面會攔截所有對 TCC 介面的呼叫。每呼叫一次 Try 介面,切面會先向 TC 註冊一個分支事務,然後才去執行原來的 RPC 呼叫。當請求鏈路呼叫完成後,TC 通過分支事務的資源 ID 回撥到正確的參與者去執行對應 TCC 資源的 Confirm 或 Cancel 方法。

如何設計和異常控制

TCC 模式需要使用者根據自己的業務場景實現 Try、Confirm 和 Cancel 三個操作;事務發起方在一階段執行 Try 方式,在二階段提交執行 Confirm 方法,二階段回滾執行 Cancel 方法。

TCC 三個方法描述:

  • Try:資源的檢測和預留;
  • Confirm:執行的業務操作提交;要求 Try 成功 Confirm 一定要能成功;
  • Cancel:預留資源釋放;

業務模型分2階段設計

使用者接入 TCC ,最重要的是考慮如何將自己的業務模型拆成兩階段來實現。

以“扣錢”場景為例,在接入 TCC 前,對 A 賬戶的扣錢,只需一條更新賬戶餘額的 SQL 便能完成;但是在接入 TCC 之後,使用者就需要考慮如何將原來一步就能完成的扣錢操作,拆成兩階段,實現成三個方法,並且保證一階段 Try 成功的話 二階段 Confirm 一定能成功。

如上圖所示,

Try 方法作為一階段準備方法,需要做資源的檢查和預留。在扣錢場景下,Try 要做的事情是就是檢查賬戶餘額是否充足,預留轉賬資金,預留的方式就是凍結 A 賬戶的 轉賬資金。Try 方法執行之後,賬號 A 餘額雖然還是 100,但是其中 30 元已經被凍結了,不能被其他事務使用。

二階段 Confirm 方法執行真正的扣錢操作。Confirm 會使用 Try 階段凍結的資金,執行賬號扣款。Confirm 方法執行之後,賬號 A 在一階段中凍結的 30 元已經被扣除,賬號 A 餘額變成 70 元 。

如果二階段是回滾的話,就需要在 Cancel 方法內釋放一階段 Try 凍結的 30 元,使賬號 A 的回到初始狀態,100 元全部可用。

使用者接入 TCC 模式,最重要的事情就是考慮如何將業務模型拆成 2 階段,實現成 TCC 的 3 個方法,並且保證 Try 成功 Confirm 一定能成功。相對於 AT 模式,TCC 模式對業務程式碼有一定的侵入性,但是 TCC 模式無 AT 模式的全域性行鎖,TCC 效能會比 AT 模式高很多。

TCC設計 - 允許空回滾

首先是空回滾。什麼是空回滾?空回滾就是對於一個分散式事務,在沒有呼叫 TCC 資源 Try 方法的情況下,呼叫了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然後直接返回成

什麼樣的情形會造成空回滾呢?可以看圖中的第 2 步,前面講過,註冊分支事務是在呼叫 RPC 時,Seata 框架的切面會攔截到該次呼叫請求,先向 TC 註冊一個分支事務,然後才去執行 RPC 呼叫邏輯。如果 RPC 呼叫邏輯有問題,比如呼叫方機器當機、網路異常,都會造成 RPC 呼叫失敗,即未執行 Try 方法。但是分散式事務已經開啟了,需要推進到終態,因此,TC 會回撥參與者二階段 Cancel 介面,從而形成空回滾。

那會不會有空提交呢?理論上來說不會的,如果呼叫方當機,那分散式事務預設是回滾的。如果是網路異常,那 RPC 呼叫失敗,發起方應該通知 TC 回滾分散式事務,這裡可以看出為什麼是理論上的,就是說發起方可以在 RPC 呼叫失敗的情況下依然通知 TC 提交,這時就會發生空提交,這種情況要麼是編碼問題,要麼開發同學明確知道需要這樣做。

那怎麼解決空回滾呢?前面提到,Cancel 要識別出空回滾,直接返回成功。那關鍵就是要識別出這個空回滾。思路很簡單就是需要知道一階段是否執行,如果執行了,那就是正常回滾;如果沒執行,那就是空回滾。因此,需要一張額外的事務控制表,其中有分散式事務 ID 和分支事務 ID,第一階段 Try 方法裡會插入一條記錄,表示一階段執行了。Cancel 介面裡讀取該記錄,如果該記錄存在,則正常回滾;如果該記錄不存在,則是空回滾。

TCC設計 - 冪等控制

接下來是冪等。冪等就是對於同一個分散式事務的同一個分支事務,重複去呼叫該分支事務的第二階段介面,因此,要求 TCC 的二階段 Confirm 和 Cancel 介面保證冪等,不會重複使用或者釋放資源。如果冪等控制沒有做好,很有可能導致資損等嚴重問題。

什麼樣的情形會造成重複提交或回滾?從圖中可以看到,提交或回滾是一次 TC 到參與者的網路呼叫。因此,網路故障、參與者當機等都有可能造成參與者 TCC 資源實際執行了二階段防範,但是 TC 沒有收到返回結果的情況,這時,TC 就會重複呼叫,直至呼叫成功,整個分散式事務結束。

怎麼解決重複執行的冪等問題呢?一個簡單的思路就是記錄每個分支事務的執行狀態。在執行前狀態,如果已執行,那就不再執行;否則,正常執行。前面在講空回滾的時候,已經有一張事務控制表了,事務控制表的每條記錄關聯一個分支事務,那我們完全可以在這張事務控制表上加一個狀態欄位,用來記錄每個分支事務的執行狀態。

如圖所示,該狀態欄位有三個值,分別是初始化、已提交、已回滾。Try 方法插入時,是初始化狀態。二階段 Confirm 和 Cancel 方法執行後修改為已提交或已回滾狀態。當重複呼叫二階段介面時,先獲取該事務控制表對應記錄,檢查狀態,如果已執行,則直接返回成功;否則正常執行。

TCC設計 - 防懸掛

最後是防懸掛。按照慣例,我們們來先講講什麼是懸掛。懸掛就是對於一個分散式事務,其二階段 Cancel 介面比 Try 介面先執行。因為允許空回滾的原因,Cancel 介面認為 Try 介面沒執行,空回滾直接返回成功,對於 Seata 框架來說,認為分散式事務的二階段介面已經執行成功,整個分散式事務就結束了。但是這之後 Try 方法才真正開始執行,預留業務資源,前面提到事務併發控制的業務加鎖,對於一個 Try 方法預留的業務資源,只有該分散式事務才能使用,然而 Seata 框架認為該分散式事務已經結束,也就是說,當出現這種情況時,該分散式事務第一階段預留的業務資源就再也沒有人能夠處理了,對於這種情況,我們就稱為懸掛,即業務資源預留後沒法繼續處理。

什麼樣的情況會造成懸掛呢?按照前面所講,在 RPC 呼叫時,先註冊分支事務,再執行 RPC 呼叫,如果此時 RPC 呼叫的網路發生擁堵,通常 RPC 呼叫是有超時時間的,RPC 超時以後,發起方就會通知 TC 回滾該分散式事務,可能回滾完成後,RPC 請求才到達參與者,真正執行,從而造成懸掛。

怎麼實現才能做到防懸掛呢?根據懸掛出現的條件先來分析下,懸掛是指二階段 Cancel 執行完後,一階段才執行。也就是說,為了避免懸掛,如果二階段執行完成,那一階段就不能再繼續執行。因此,當一階段執行時,需要先檢查二階段是否已經執行完成,如果已經執行,則一階段不再執行;否則可以正常執行。那怎麼檢查二階段是否已經執行呢?大家是否想到了剛才解決空回滾和冪等時用到的事務控制表,可以在二階段執行時插入一條事務控制記錄,狀態為已回滾,這樣當一階段執行時,先讀取該記錄,如果記錄存在,就認為二階段已經執行;否則二階段沒執行。

Dubbo + Seata 實戰案例

關於環境準備和目錄結構,大家詳見: 微服務痛點 - 基於Dubbo + Seata的分散式事務(AT)模式

業務模型兩階段改造

Storage商品庫存

  1. 資料庫新增凍結商品庫存數
# 建立商品庫存表
create table if not exists storage.tcc_storage
(
    id bigint auto_increment
        primary key,
    commodity_code varchar(50) null comment '商品編碼',
    name varchar(255) null comment '商品名稱',
    count int null comment '商品庫存數',
    frozen_count int default 0 null comment '凍結商品庫存數'
);
  1. 將原來的扣減商品庫存一步邏輯修改成兩階段邏輯操作:
package cn.mushuwei.storage.api;

import cn.mushuwei.storage.api.dto.CommodityDTO;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

/**
 * @author jamesmsw
 * @date 2020/12/1 9:37 上午
 */
@LocalTCC
public interface StorageApi {


    /**
     * 扣減庫存準備
     *
     * @param actionContext 業務動作上下文
     * @param commodityDTO 庫存資訊
     * @return 是/否
     */
    @TwoPhaseBusinessAction(name = "decreaseStorageTcc", commitMethod = "decreaseStorageCommit", rollbackMethod = "decreaseStorageCancel")
    Boolean decreaseStoragePrepare(BusinessActionContext actionContext,
                                   @BusinessActionContextParameter(paramName = "commdityDTO") CommodityDTO commodityDTO);

    /**
     * 扣減庫存提交
     *
     * @param actionContext 業務動作上下文
     * @return 是/否
     */
    Boolean decreaseStorageCommit(BusinessActionContext actionContext);

    /**
     * 扣減庫存回滾
     *
     * @param actionContext 業務動作上下文
     * @return 是/否
     */
    Boolean decreaseStorageCancel(BusinessActionContext actionContext);
}

  1. 資料持久化操作邏輯修改:
<?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="cn.mushuwei.storage.dao.StorageDao">

    <update id="tccDecreaseStoragePrepare">
        update tcc_storage set count = count - #{count},
                           frozen_count = frozen_count + #{count}
        where commodity_code = #{commodityCode}
    </update>

    <update id="tccDecreaseStorageCommit">
        update tcc_storage set frozen_count = frozen_count - #{count}
        where commodity_code = #{commodityCode}
    </update>

    <update id="tccDecreaseStorageCancel">
        update tcc_storage set count = count + #{count},
                           frozen_count = frozen_count - #{count}
        where commodity_code = #{commodityCode}
    </update>
</mapper>

Account使用者

  1. **資料庫新增賬號凍結餘額: **
# 建立使用者賬戶表
create table if not exists tcc_account
(
	id bigint auto_increment
		primary key,
	user_id varchar(50) null comment '使用者編號',
	amount double(50,2) null comment '賬號餘額',
	frozen_amount double(50,2) default 0.00 null comment '賬號凍結餘額'
);
  1. 將原來的扣減使用者餘額一步邏輯修改成兩階段邏輯操作:
package cn.mushuwei.account.api;

import cn.mushuwei.account.api.dto.AccountDTO;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

/**
 * @author jamesmsw
 * @date 2020/12/1 5:20 下午
 */
@LocalTCC
public interface AccountApi {


    /**
     * 從賬號扣錢準備
     *
     * @param accountDTO
     * @param actionContext 業務動作上下文
     * @return 是/否
     */
    @TwoPhaseBusinessAction(name = "decreaseAccountTcc", commitMethod = "decreaseAccountCommit", rollbackMethod = "decreaseAccountCancel")
    Boolean decreaseAccountPrepare(BusinessActionContext actionContext,
                                   @BusinessActionContextParameter(paramName = "accountDTO") AccountDTO accountDTO);

    /**
     * 從賬號扣錢提交
     *
     * @param actionContext
     * @return 是/否
     */
    Boolean decreaseAccountCommit(BusinessActionContext actionContext);


    /**
     * 從賬號扣錢取消
     *
     * @param actionContext
     * @return 是/否
     */
    Boolean decreaseAccountCancel(BusinessActionContext actionContext);
}

  1. 資料持久化操作邏輯修改:
<?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="cn.mushuwei.account.dao.AccountDao">

    <!--    以下是tcc模式所需的資料庫操作 -->
    <update id="tccDecreaseAccountPrepare">
        update tcc_account set amount = amount - #{amount},
                           frozen_amount = frozen_amount + #{amount}
        where user_id = #{userId}
    </update>

    <update id="tccDecreaseAccountCommit">
        update tcc_account set frozen_amount = frozen_amount - #{amount}
        where user_id = #{userId}
    </update>

    <update id="tccDecreaseAccountCancel">
        update tcc_account set amount = amount + #{amount},
                           frozen_amount = frozen_amount - #{amount}
        where user_id = #{userId}
    </update>

</mapper>

Order訂單

  1. 資料庫新增訂單建立狀態:
create table if not exists `order`.tcc_order
(
    id bigint auto_increment
        primary key,
    order_no varchar(100) null comment '訂單號',
    user_id varchar(50) null comment '使用者編號',
    code varchar(100) null comment '商品編碼',
    count int null comment '商品數量',
    amount double(50,2) null comment '消費總金額',
    status tinyint null comment '狀態,1-預建立;2-建立成功;3-建立失敗'
);
  1. 將原來的建立訂單一步邏輯修改成兩階段邏輯操作:
package cn.mushuwei.order.api;

import cn.mushuwei.order.api.dto.OrderDTO;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

/**
 * @author jamesmsw
 * @date 2020/12/1 5:57 下午
 */
@LocalTCC
public interface OrderApi {


    /**
     * 建立訂單準備
     *
     * @param orderDTO
     * @param actionContext 業務動作上下文
     * @return
     */
    @TwoPhaseBusinessAction(name = "createOrderTcc", commitMethod = "createOrderCommit", rollbackMethod = "createOrderCancel")
    Boolean createOrderPrepare(BusinessActionContext actionContext,
                               @BusinessActionContextParameter(paramName = "orderDTO") OrderDTO orderDTO);

    /**
     * 建立訂單提交
     *
     * @param actionContext 業務動作上下文
     * @return
     */
    Boolean createOrderCommit(BusinessActionContext actionContext);

    /**
     * 建立訂單取消
     *
     * @param actionContext 業務動作上下文
     * @return
     */
    Boolean createOrderCancel(BusinessActionContext actionContext);
}

  • TwoPhaseBusinessAction註解標記這是個TCC介面,同時指定commitMethod,rollbackMethod的名稱BusinessActionContext是TCC事務中的上下文物件 BusinessActionContextParameter註解標記的引數會在上下文中傳播,即能通過BusinessActionContext物件在commit方法及cancle方法中取到該引數值

  • RM 的介面上面必須要有@LocalTCC 註解,且必須在介面上面

  1. 資料持久化操作邏輯修改:
<?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="cn.mushuwei.order.dao.OrderDao">

    <insert id="tccCreateOrderPrepare" keyProperty="id" useGeneratedKeys="true"
            parameterType="cn.mushuwei.order.entity.OrderDO">
        insert into `tcc_order` (order_no,
            user_id,
            code,
            count,
            amount,
            status)
        VALUES (#{order.orderNo},
                #{order.userId},
                #{order.code},
                #{order.count},
                #{order.amount},
                #{order.status})
    </insert>

    <update id="tccCreateOrderCommitOrCancel">
        update `tcc_order` set status = #{status}
        where order_no = #{orderNo}
    </update>

</mapper>

演示

啟動Dubbo、Seata、MySQ並初始化資料, 使各服務應用註冊到Seata上

  • Dubbo、Seata和MySQL服務
mushuwei@mushuweideMacBook-Pro-2 seata % docker ps
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                                                  NAMES
0c9c325a039c        mysql:latest           "docker-entrypoint.s…"   2 weeks ago         Up 7 minutes        0.0.0.0:3306->3306/tcp, 33060/tcp                      mysql5.7
b8031fa865cd        seataio/seata-server   "java -Djava.securit…"   2 weeks ago         Up 20 seconds       0.0.0.0:8091->8091/tcp                                 seata_seata-server_1
2af927368a15        apache/dubbo-admin     "java -XX:+UnlockExp…"   2 weeks ago         Up 2 hours          0.0.0.0:8080->8080/tcp                                 dubbo_admin_1
7afec07234c9        zookeeper              "/docker-entrypoint.…"   2 weeks ago         Up 2 hours          2888/tcp, 3888/tcp, 0.0.0.0:2181->2181/tcp, 8080/tcp   dubbo_zookeeper_1
  • 初始化資料
mysql> use storage;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from tcc_storage;
+----+----------------+------+-------+--------------+
| id | commodity_code | name | count | frozen_count |
+----+----------------+------+-------+--------------+
|  1 | cola           | ???? |  2000 |            0 |
+----+----------------+------+-------+--------------+
1 row in set (0.00 sec)

mysql> use account;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from tcc_account;
+----+---------+---------+---------------+
| id | user_id | amount  | frozen_amount |
+----+---------+---------+---------------+
|  1 | user123 | 1250.00 |          0.00 |
+----+---------+---------+---------------+
1 row in set (0.00 sec)

mysql> use order;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from tcc_order;
Empty set (0.00 sec)
  • 啟動Storage、Account、Order和Business

  • Seata上各應用的註冊情況
Starting seata_seata-server_1 ... done
Attaching to seata_seata-server_1
seata-server_1  | [0.001s][warning][gc] -Xloggc is deprecated. Will use -Xlog:gc:/var/log/seata_gc.log instead.
seata-server_1  | [0.015s][info   ][gc] Using G1
seata-server_1  | [0.841s][info   ][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 14M->4M(32M) 11.654ms
seata-server_1  | SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are
seata-server_1  | SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
seata-server_1  | SLF4J: See also http://www.slf4j.org/codes.html#replay
seata-server_1  | 08:16:30.938  INFO --- [                     main] io.seata.server.Server                   : The server is running in container.
seata-server_1  | 08:16:30.972  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is registry
seata-server_1  | 08:16:30.980  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is /seata-server/resources/registry.conf
seata-server_1  | [1.385s][info   ][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 15M->6M(32M) 14.280ms
seata-server_1  | 08:16:31.221  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is file.conf
seata-server_1  | 08:16:31.222  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is file.conf
seata-server_1  | WARNING: An illegal reflective access operation has occurred
seata-server_1  | WARNING: Illegal reflective access by net.sf.cglib.core.ReflectUtils$2 (file:/seata-server/libs/cglib-3.1.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
seata-server_1  | WARNING: Please consider reporting this to the maintainers of net.sf.cglib.core.ReflectUtils$2
seata-server_1  | WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
seata-server_1  | WARNING: All illegal access operations will be denied in a future release
seata-server_1  | [1.734s][info   ][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 16M->7M(32M) 6.400ms
seata-server_1  | [2.101s][info   ][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 18M->7M(32M) 4.828ms
seata-server_1  | 08:16:31.924  INFO --- [                     main] i.s.core.rpc.netty.NettyServerBootstrap  : Server started, listen port: 8091
seata-server_1  | 08:26:12.007  INFO --- [rverHandlerThread_1_1_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/storage', applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0xae1ea1b1, L:/172.20.0.2:8091 - R:/172.20.0.1:52380],client version:1.3.0
seata-server_1  | 08:26:12.080  INFO --- [rverHandlerThread_1_2_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/storage', applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0xae1ea1b1, L:/172.20.0.2:8091 - R:/172.20.0.1:52380],client version:1.3.0
seata-server_1  | 08:26:33.704  INFO --- [rverHandlerThread_1_3_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/account', applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0xd949a994, L:/172.20.0.2:8091 - R:/172.20.0.1:52396],client version:1.3.0
seata-server_1  | 08:26:33.758  INFO --- [rverHandlerThread_1_4_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/account', applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0xd949a994, L:/172.20.0.2:8091 - R:/172.20.0.1:52396],client version:1.3.0
seata-server_1  | 08:26:57.466  INFO --- [rverHandlerThread_1_5_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/order', applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xfd51f88b, L:/172.20.0.2:8091 - R:/172.20.0.1:52412],client version:1.3.0
seata-server_1  | 08:26:57.518  INFO --- [rverHandlerThread_1_6_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/order', applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xfd51f88b, L:/172.20.0.2:8091 - R:/172.20.0.1:52412],client version:1.3.0
seata-server_1  | 08:27:10.600  INFO --- [ettyServerNIOWorker_1_4_8] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0x0e0b6c24, L:/172.20.0.2:8091 - R:/172.20.0.1:52424],client version:1.3.0
seata-server_1  | 08:27:32.694  INFO --- [ettyServerNIOWorker_1_5_8] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0x2fd20474, L:/172.20.0.2:8091 - R:/172.20.0.1:52432],client version:1.3.0
seata-server_1  | 08:27:56.453  INFO --- [ettyServerNIOWorker_1_6_8] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xc8f6ba94, L:/172.20.0.2:8091 - R:/172.20.0.1:52436],client version:1.3.0
seata-server_1  | 08:28:15.847  INFO --- [rverHandlerThread_1_7_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='null', applicationId='seata-action-business', transactionServiceGroup='service_tx_group'},channel:[id: 0x9ef75d68, L:/172.20.0.2:8091 - R:/172.20.0.1:52444],client version:1.3.0
seata-server_1  | 08:28:15.863  INFO --- [ettyServerNIOWorker_1_7_8] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId='seata-action-business', transactionServiceGroup='service_tx_group'},channel:[id: 0x2b6c19d5, L:/172.20.0.2:8091 - R:/172.20.0.1:52440],client version:1.3.0

檢查各服務Service在Dubbo上的情況

正常流程-模擬使用者下單,看下各應用的二階段提交日誌

  • 執行business模組test/java目錄下的business.http檔案,對介面發起請求
POST localhost:8084/business/buy
Content-Type: application/json

{
  "userId" : "user123",
  "commodityCode" : "cola",
  "count" : 2,
  "amount" : 5.0
}
  • 各資料庫資料變化
mysql> use storage;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from storage;
+----+----------------+------+-------+
| id | commodity_code | name | count |
+----+----------------+------+-------+
|  1 | cola           | ???? |  1998 |
+----+----------------+------+-------+
1 row in set (0.00 sec)

mysql> use account;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from account;
+----+---------+---------+
| id | user_id | amount  |
+----+---------+---------+
|  1 | user123 | 1245.00 |
+----+---------+---------+
1 row in set (0.00 sec)

mysql> use order;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from order;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order' at line 1
mysql> select * from `order`;
+----+----------------------------------+---------+------+-------+--------+
| id | order_no                         | user_id | code | count | amount |
+----+----------------------------------+---------+------+-------+--------+
|  5 | dbde6ebfd72b4ad5aeba67d67ade6894 | user123 | cola |     2 |   5.00 |
+----+----------------------------------+---------+------+-------+--------+
1 row in set (0.00 sec)

  • 各應用下二階段提交情況,下面日誌以Storage應用為例
2020-12-28 17:59:40.778  INFO 28287 --- [ctor_RMROLE_1_1] io.seata.rm.AbstractRMHandler            : the rm client received response msg [version=1.5.0-SNAPSHOT,extraData=null,identified=true,resultCode=null,msg=null] from tc server.
2020-12-28 17:59:40.783 DEBUG 28287 --- [:20881-thread-2] c.m.s.d.S.tccDecreaseStoragePrepare      : ==>  Preparing: update tcc_storage set count = count - ?, frozen_count = frozen_count + ? where commodity_code = ?
2020-12-28 17:59:40.822 DEBUG 28287 --- [:20881-thread-2] c.m.s.d.S.tccDecreaseStoragePrepare      : ==> Parameters: 2(Integer), 2(Integer), cola(String)
2020-12-28 17:59:40.830 DEBUG 28287 --- [:20881-thread-2] c.m.s.d.S.tccDecreaseStoragePrepare      : <==    Updates: 1
2020-12-28 17:59:41.662  INFO 28287 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=172.20.0.2:8091:86882407747166208,branchId=86882409689128960,branchType=TCC,resourceId=decreaseStorageTcc,applicationData={"actionContext":{"action-start-time":1609149580502,"sys::prepare":"decreaseStoragePrepare","sys::rollback":"decreaseStorageCancel","sys::commit":"decreaseStorageCommit","commdityDTO":{"commodityCode":"cola","count":2},"host-name":"172.17.54.171","actionName":"decreaseStorageTcc"}}
2020-12-28 17:59:41.664  INFO 28287 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch committing: 172.20.0.2:8091:86882407747166208 86882409689128960 decreaseStorageTcc {"actionContext":{"action-start-time":1609149580502,"sys::prepare":"decreaseStoragePrepare","sys::rollback":"decreaseStorageCancel","sys::commit":"decreaseStorageCommit","commdityDTO":{"commodityCode":"cola","count":2},"host-name":"172.17.54.171","actionName":"decreaseStorageTcc"}}
2020-12-28 17:59:41.675 DEBUG 28287 --- [h_RMROLE_1_1_16] c.m.s.d.S.tccDecreaseStorageCommit       : ==>  Preparing: update tcc_storage set frozen_count = frozen_count - ? where commodity_code = ?
2020-12-28 17:59:41.676 DEBUG 28287 --- [h_RMROLE_1_1_16] c.m.s.d.S.tccDecreaseStorageCommit       : ==> Parameters: 2(Integer), cola(String)
2020-12-28 17:59:41.681 DEBUG 28287 --- [h_RMROLE_1_1_16] c.m.s.d.S.tccDecreaseStorageCommit       : <==    Updates: 1
2020-12-28 17:59:41.704  INFO 28287 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractResourceManager      : TCC resource commit result : true, xid: 172.20.0.2:8091:86882407747166208, branchId: 86882409689128960, resourceId: decreaseStorageTcc
2020-12-28 17:59:41.705  INFO 28287 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed

異常流程-模擬使用者下單,看下各應用的二階段提交日誌

  • 修改BusinessServiceImpl類,並重啟
 private boolean flag;

    @Override
    @GlobalTransactional(timeoutMills = 300000, name = "seata-demo-business")
    public Boolean handleBusiness(BusinessDTO businessDTO) {
        flag = false;
        log.info("開始全域性事務,XID = " + RootContext.getXID());
        CommodityDTO commodityDTO = new CommodityDTO();
        commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
        commodityDTO.setCount(businessDTO.getCount());
        boolean storageResult =  storageApi.decreaseStorage(commodityDTO);

        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setUserId(businessDTO.getUserId());
        orderDTO.setCommodityCode(businessDTO.getCommodityCode());
        orderDTO.setOrderCount(businessDTO.getCount());
        orderDTO.setOrderAmount(businessDTO.getAmount());
        boolean orderResult = orderApi.createOrder(orderDTO);

        //開啟註釋測試事務發生異常後,全域性回滾功能
        if (!flag) {
            throw new RuntimeException("測試拋異常後,分散式事務回滾!");
        }

        if (!storageResult || !orderResult) {
            throw new RuntimeException("失敗");
        }
        return true;
    }
  • 執行business模組test/java目錄下的business.http檔案,對介面發起請求
POST http://localhost:8084/business/buy

HTTP/1.1 500 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 21 Dec 2020 08:46:24 GMT
Connection: close

{
  "timestamp": "2020-12-21T08:46:24.678+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "",
  "path": "/business/buy"
}
  • 各資料庫資料變化

無任何變化

  • 各應用下二階段提交情況,下面日誌以Storage應用為例
2020-12-28 18:04:12.125  WARN 28287 --- [erverWorker-3-1] o.a.d.remoting.transport.AbstractServer  :  [DUBBO] All clients has disconnected from /172.17.54.171:20881. You can graceful shutdown now., dubbo version: 2.7.8, current host: 172.17.54.171
2020-12-28 18:04:12.126  INFO 28287 --- [erverWorker-3-1] o.a.d.r.t.netty4.NettyServerHandler      :  [DUBBO] The connection of /172.17.54.171:53500 -> /172.17.54.171:20881 is disconnected., dubbo version: 2.7.8, current host: 172.17.54.171
2020-12-28 18:05:00.344  INFO 28287 --- [erverWorker-3-2] o.a.d.r.t.netty4.NettyServerHandler      :  [DUBBO] The connection of /172.17.54.171:54267 -> /172.17.54.171:20881 is established., dubbo version: 2.7.8, current host: 172.17.54.171
2020-12-28 18:05:13.544  INFO 28287 --- [:20881-thread-5] c.m.storage.provider.StorageApiImpl      : commodityDTO: CommodityDTO(id=null, commodityCode=cola, name=null, count=2), actionContext: [xid:172.20.0.2:8091:86883805524140032,branch_Id:86883805897433088,action_name:decreaseStorageTcc,action_context:{action-start-time=1609149913535, sys::prepare=decreaseStoragePrepare, sys::rollback=decreaseStorageCancel, sys::commit=decreaseStorageCommit, commdityDTO=CommodityDTO(id=null, commodityCode=cola, name=null, count=2), host-name=172.17.54.171, actionName=decreaseStorageTcc}]
2020-12-28 18:05:13.557 DEBUG 28287 --- [:20881-thread-5] c.m.s.d.S.tccDecreaseStoragePrepare      : ==>  Preparing: update tcc_storage set count = count - ?, frozen_count = frozen_count + ? where commodity_code = ?
2020-12-28 18:05:13.558 DEBUG 28287 --- [:20881-thread-5] c.m.s.d.S.tccDecreaseStoragePrepare      : ==> Parameters: 2(Integer), 2(Integer), cola(String)
2020-12-28 18:05:13.562 DEBUG 28287 --- [:20881-thread-5] c.m.s.d.S.tccDecreaseStoragePrepare      : <==    Updates: 1
2020-12-28 18:05:13.782  INFO 28287 --- [h_RMROLE_1_2_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=172.20.0.2:8091:86883805524140032,branchId=86883805897433088,branchType=TCC,resourceId=decreaseStorageTcc,applicationData={"actionContext":{"action-start-time":1609149913535,"sys::prepare":"decreaseStoragePrepare","sys::rollback":"decreaseStorageCancel","sys::commit":"decreaseStorageCommit","commdityDTO":{"commodityCode":"cola","count":2},"host-name":"172.17.54.171","actionName":"decreaseStorageTcc"}}
2020-12-28 18:05:13.784  INFO 28287 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 172.20.0.2:8091:86883805524140032 86883805897433088 decreaseStorageTcc
2020-12-28 18:05:13.789 DEBUG 28287 --- [h_RMROLE_1_2_16] c.m.s.d.S.tccDecreaseStorageCancel       : ==>  Preparing: update tcc_storage set count = count + ?, frozen_count = frozen_count - ? where commodity_code = ?
2020-12-28 18:05:13.789 DEBUG 28287 --- [h_RMROLE_1_2_16] c.m.s.d.S.tccDecreaseStorageCancel       : ==> Parameters: 2(Integer), 2(Integer), cola(String)
2020-12-28 18:05:13.793 DEBUG 28287 --- [h_RMROLE_1_2_16] c.m.s.d.S.tccDecreaseStorageCancel       : <==    Updates: 1
2020-12-28 18:05:13.815  INFO 28287 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractResourceManager      : TCC resource rollback result : true, xid: 172.20.0.2:8091:86883805524140032, branchId: 86883805897433088, resourceId: decreaseStorageTcc
2020-12-28 18:05:13.815  INFO 28287 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked

以上程式碼,我已經上傳到GitHub中了,大家詳見: https://github.com/sanshengshui/seata-dubbo-action,TCC模式在TCC分支上。

到此,基於Dubbo + Seata的分散式事務已經講解完畢。

參考文章

相關文章