交易管理

此範例示範如何透過 Seata 實現分散式 Dubbo 服務的交易管理,以確保資料一致性。

什麼是 Seata

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

1. 範例架構說明

用戶購買商品業務,整個業務包含 3 個微服務

  • 庫存服務:扣除指定商品的庫存數量。
  • 訂單服務:根據購買請求產生訂單。
  • 帳戶服務:扣除用戶帳戶金額。

image.png

StorageService (庫存服務)

public interface StorageService {

     /**
      * Deducted storage quantity
      */
     void deduct(String commodityCode, int count);
}

OrderService (訂單服務)

public interface OrderService {

     /**
      * Create Order
      */
     Order create(String userId, String commodityCode, int orderCount);
}

AccountService (帳戶服務)

public interface AccountService {

     /**
      * Borrow from user account
      */
     void debit(String userId, int money);
}

二、主要業務邏輯

BusinessService (業務服務)

public class BusinessServiceImpl implements BusinessService {

     private StorageService storageService;

     private OrderService orderService;

     /**
      * Purchasing
      */
     public void purchase(String userId, String commodityCode, int orderCount) {
         // Deduct storage amount
         storageService.deduct(commodityCode, orderCount);
         // Create Order
         orderService.create(userId, commodityCode, orderCount);
     }
}

StorageService (庫存服務)

public class StorageServiceImpl implements StorageService {

     private JdbcTemplate jdbcTemplate;
  
     @Override
     public void deduct(String commodityCode, int count) {
         // Modify the database: deduct the amount of storage
         jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?",
                 new Object[]{count, commodityCode});
     }
}

OrderService (訂單服務)

public class OrderServiceImpl implements OrderService {

     private AccountService accountService;
    
     private JdbcTemplate jdbcTemplate;

     public Order create(String userId, String commodityCode, int orderCount) {
         // calculate the amount
         int orderMoney = calculate(commodityCode, orderCount);

         // The amount deducted from the user account
         accountService.debit(userId, orderMoney);

         // Modify the database: create a new order
         final Order order = new Order();
         order.userId = userId;
         order.commodityCode = commodityCode;
         order.count = orderCount;
         order.money = orderMoney;
         KeyHolder keyHolder = new GeneratedKeyHolder();
         jdbcTemplate. update(con -> {
             PreparedStatement pst = con. prepareStatement(
                     "insert into order_tbl (user_id, commodity_code, count, money) values (?, ?, ?, ?)",
                     PreparedStatement. RETURN_GENERATED_KEYS);
             pst.setObject(1, order.userId);
             pst.setObject(2, order.commodityCode);
             pst.setObject(3, order.count);
             pst.setObject(4, order.money);
             return pst;
         }, keyHolder);
         order.id = keyHolder.getKey().longValue();
         return order;
     }
}

AccountService (帳戶服務)

public class AccountServiceImpl implements AccountService {
        
     private JdbcTemplate jdbcTemplate;
    
     @Override
     public void debit(String userId, int money) {
         // Modify the database: deduct the amount from the user account
         jdbcTemplate.update("update account_tbl set money = money - ? where user_id = ?", new Object[]{money, userId});
     }
}

3. 快速入門範例

步驟 1:下載原始碼

git clone -b master https://github.com/apache/dubbo-samples.git
cd ./dubbo-samples-transaction/

步驟 2:透過 docker-compose 啟動 Seata-Server 和 MySQL

在本範例中,我們使用 docker-compose 快速啟動 seata-server 和 mysql 等服務。

cd src/main/resources/docker
docker-compose up

步驟 3:建置用例

執行 maven 命令打包 demo 專案

mvn clean package

步驟 4:啟動 AccountService

java -classpath ./target/dubbo-samples-transaction-1.0-SNAPSHOT.jar org.apache.dubbo.samples.starter.DubboAccountServiceStarter

步驟 5:啟動 OrderService

java -classpath ./target/dubbo-samples-transaction-1.0-SNAPSHOT.jar org.apache.dubbo.samples.starter.DubboOrderServiceStarter

步驟 6:啟動 StorageService

java -classpath ./target/dubbo-samples-transaction-1.0-SNAPSHOT.jar org.apache.dubbo.samples.starter.DubboStorageServiceStarter

步驟 7:啟動 BusinessService

java -classpath ./target/dubbo-samples-transaction-1.0-SNAPSHOT.jar org.apache.dubbo.samples.starter.DubboBusinessTester

4. 範例核心流程

image.png

步驟 1:修改業務程式碼

這裡只需要在業務發起方的方法上寫一行註釋 @GlobalTransactional

     @GlobalTransactional
     public void purchase(String userId, String commodityCode, int orderCount) {
          …
     }

步驟 2:安裝資料庫

  • 需求:MySQL(InnoDB 儲存引擎)。

提示:實際上,範例中的 3 個微服務需要 3 個獨立的資料庫,但為了方便起見,我們使用同一個實體資料庫,並配置 3 個邏輯連線字串。

修改以下 xml 檔案中的資料庫 url、使用者名稱和密碼

dubbo-account-service.xml dubbo-order-service.xml dubbo-storage-service.xml

     <property name="url" value="jdbc:mysql://x.x.x.x:3306/xxx" />
     <property name="username" value="xxx" />
     <property name="password" value="xxx" />

步驟 3:為 Seata 建立 undo_log 表

UNDO_LOG 這個表用於 Seata 的 AT 模式。

-- Note that when the Seata version is upgraded to 0.3.0+, the normal index will be changed to a unique index.
CREATE TABLE `undo_log` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
   `branch_id` bigint(20) NOT NULL,
   `xid` varchar(100) NOT NULL,
   `context` varchar(128) NOT NULL,
   `rollback_info` longblob NOT NULL,
   `log_status` int(11) NOT NULL,
   `log_created` datetime NOT NULL,
   `log_modified` datetime NOT NULL,
   `ext` varchar(100) DEFAULT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `commodity_code` varchar(255) DEFAULT NULL,
   `count` int(11) DEFAULT 0,
   PRIMARY KEY (`id`),
   UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `user_id` varchar(255) DEFAULT NULL,
   `commodity_code` varchar(255) DEFAULT NULL,
   `count` int(11) DEFAULT 0,
   `money` int(11) DEFAULT 0,
   PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `user_id` varchar(255) DEFAULT NULL,
   `money` int(11) DEFAULT 0,
   PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

步驟 5:啟動 Seata-Server 服務

Usage: sh seata-server.sh(for linux and mac) or cmd seata-server.bat(for windows) [options]
   Options:
     --host, -h
       The host to bind.
       Default: 0.0.0.0
     --port, -p
       The port to listen.
       Default: 8091
     --storeMode, -m
       log store mode: file, db
       Default: file
     --help

e.g.

sh seata-server.sh -p 8091 -h 127.0.0.1 -m file

上次修改時間:2023 年 1 月 2 日:增強英文文件 (#1798) (95a9f4f6c1c)