SpringCloud系列之整合分散式事務Seata應用篇

吳碼發表於2020-07-06

前言

單體應用被拆分成各個獨立的業務模組後,就不得不要去面對分散式事務,好在阿里已經開源分散式事務元件Seata,雖還在迭代中,難免會有bug產生,但隨著社群發展及反饋,相信終究會越來越穩定,話不多說讓我們開始吧。

專案版本

spring-boot.version:2.2.5.RELEASE
spring-cloud.version:Hoxton.SR3
seata.version:1.2.0

專案說明

專案模組說明如下:

前端請求介面經由閘道器服務進行路由轉發後進入cloud-web模組,經cloud-web模組呼叫相應業務微服務模組,執行業務邏輯後響應前端請求。

Seata服務端部署

1.下載Seata服務端部署檔案
https://github.com/seata/seata/releases/download/v1.2.0/seata-server-1.2.0.zip
如嫌下載慢,可關注本文下方微信公眾號二維碼,關注後回覆“666”即可獲取開發常用工具包
2.解壓至本地目錄後,執行seata-server.bat指令碼,過程中無報錯則說明部署正常,Linux環境下操作類似不做展開說明

Seata客戶端整合
cloud-web

部分pom.xml,後續模組引入seata依賴一樣,後續模組不再單獨說明

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.2.0.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.2.0</version>
</dependency>

application.properties ,後續模組引入seata配置項大致一樣,根據業務模組調整3個配置項即可,後續模組不再單獨說明

# seata配置
seata.enabled=true
#seata.excludes-for-auto-proxying=firstClassNameForExclude,secondClassNameForExclude
seata.application-id=cloud-web
seata.tx-service-group=cloud-web_tx_group
seata.enable-auto-data-source-proxy=true
seata.use-jdk-proxy=false

seata.client.rm.async-commit-buffer-limit=1000
seata.client.rm.report-retry-count=5
seata.client.rm.table-meta-check-enable=false
seata.client.rm.report-success-enable=false
seata.client.rm.saga-branch-register-enable=false
seata.client.rm.lock.retry-interval=10
seata.client.rm.lock.retry-times=30
seata.client.rm.lock.retry-policy-branch-rollback-on-conflict=true

seata.client.tm.commit-retry-count=5
seata.client.tm.rollback-retry-count=5
seata.client.tm.degrade-check=false
seata.client.tm.degrade-check-allow-times=10
seata.client.tm.degrade-check-period=2000

seata.client.undo.data-validation=true
seata.client.undo.log-serialization=jackson
seata.client.undo.only-care-update-columns=true
seata.client.undo.log-table=undo_log

seata.client.log.exceptionRate=100
seata.service.vgroup-mapping.cloud-web_tx_group=default
seata.service.grouplist.default=${cloud-web.seata.service.grouplist.default}
seata.service.enable-degrade=false
seata.service.disable-global-transaction=false

seata.transport.shutdown.wait=3
seata.transport.thread-factory.boss-thread-prefix=NettyBoss
seata.transport.thread-factory.worker-thread-prefix=NettyServerNIOWorker
seata.transport.thread-factory.server-executor-thread-prefix=NettyServerBizHandler
seata.transport.thread-factory.share-boss-worker=false
seata.transport.thread-factory.client-selector-thread-prefix=NettyClientSelector
seata.transport.thread-factory.client-selector-thread-size=1
seata.transport.thread-factory.client-worker-thread-prefix=NettyClientWorkerThread
seata.transport.thread-factory.worker-thread-size=default
seata.transport.thread-factory.boss-thread-size=1
seata.transport.type=TCP
seata.transport.server=NIO
seata.transport.heartbeat=true
seata.transport.serialization=seata
seata.transport.compressor=none
seata.transport.enable-client-batch-send-request=true

seata.config.type=file
seata.registry.type=file

重點3個配置項需要調整下,其餘保持預設
seata.application-id=cloud-web
seata.tx-service-group=cloud-web_tx_group
seata.service.vgroup-mapping.cloud-web_tx_group=default

OrderController.java

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

    @Autowired
    OrderFacade orderFacade;

    @GlobalTransactional
    @GetMapping("/add")
    public String add(@RequestParam("cartId") Long cartId){
        orderFacade.addOrder(cartId);
        return "OK";
    }

}

在需要開啟分散式事務的方法上新增@GlobalTransactional註解即可

module-order

專案結構圖如下

OrderService.java

@RestController
public class OrderService implements OrderFacade {

    @Autowired
    private TbOrderMapper tbOrderMapper;
    @Autowired
    private CartFacade cartFacade;
    @Autowired
    private GoodsFacade goodsFacade;
    @Autowired
    private WalletFacade walletFacade;

    /**
     * <p >
     * 功能:新增訂單
     * </p>
     * @param cartId 購物車ID
     * @author wuyubin
     * @date  2020年05月22日
     * @return
     */
    @Override
    public void addOrder(Long cartId) {
        CartDTO cart = cartFacade.getCartById(cartId);
        TbOrder order = new TbOrder();
        order.setUserId(cart.getUserId());
        order.setGoodsId(cart.getGoodsId());
        order.setOrderNo(String.valueOf(System.currentTimeMillis()));
        order.setCreateTime(System.currentTimeMillis());
        order.setUpdateTime(order.getCreateTime());
        order.setIsDeleted(Byte.valueOf("0"));
        // 新增訂單
        tbOrderMapper.insert(order);
        // 刪除購物車
        cartFacade.deleteCartById(cartId);
        GoodsDTO goods = goodsFacade.getByGoodsId(cart.getGoodsId());
        // 扣減庫存
        goodsFacade.substractStock(goods.getId());
        // 扣減金額
        walletFacade.substractMoney(cart.getUserId(),goods.getMoney());
        throw new RuntimeException();
    }
module-cart

專案結構圖如下

CartService.java

@RestController
public class CartService implements CartFacade {

    Logger LOGGER = LoggerFactory.getLogger(CartService.class);

    @Autowired
    private TbCartMapper tbCartMapper;
    /**
     * <p >
     * 功能:增加商品至購物車
     * </p>
     * @param userId 使用者ID
     * @param goodsId 商品ID
     * @author wuyubin
     * @date  2020年05月22日
     * @return
     */
    @Override
    public String addCart(Long userId,Long goodsId) {
        TbCart cart = new TbCart();
        cart.setUserId(userId);
        cart.setGoodsId(goodsId);
        cart.setCreateTime(System.currentTimeMillis());
        cart.setUpdateTime(cart.getCreateTime());
        cart.setIsDeleted(Byte.valueOf("0"));
        tbCartMapper.insert(cart);
        return null;
    }
    /**
     * <p >
     * 功能:獲取購物車資訊
     * </p>
     * @param cartId 購物車ID
     * @author wuyubin
     * @date  2020年05月22日
     * @return
     */
    @Override
    public CartDTO getCartById(Long cartId) {
        CartDTO cartDTO = null;
        TbCart cart = tbCartMapper.selectById(cartId);
        if (null != cart) {
            cartDTO = new CartDTO();
            cartDTO.setUserId(cart.getUserId());
            cartDTO.setGoodsId(cart.getGoodsId());
        }
        return cartDTO;
    }
    /**
     * <p >
     * 功能:刪除購物車資訊
     * </p>
     * @param cartId 購物車ID
     * @author wuyubin
     * @date  2020年05月22日
     * @return
     */
    @Override
    public void deleteCartById(Long cartId) {
        tbCartMapper.deleteById(cartId);
    }
}
module-goods

專案結構圖如下

GoodsService.java

@RestController
public class GoodsService implements GoodsFacade {

    @Autowired
    private TbGoodsMapper tbGoodsMapper;
    /**
     * <p >
     * 功能:獲取商品資訊
     * </p>
     * @param goodsId 商品ID
     * @author wuyubin
     * @date  2020年05月22日
     * @return
     */
    @Override
    public GoodsDTO getByGoodsId(Long goodsId) {
        GoodsDTO goodsDTO = null;
        TbGoods goods = tbGoodsMapper.selectById(goodsId);
        if (null != goods) {
            goodsDTO = new GoodsDTO();
            BeanUtils.copyProperties(goods,goodsDTO);
        }
        return goodsDTO;
    }
    /**
     * <p >
     * 功能:扣減商品庫存
     * </p>
     * @param goodsId 商品ID
     * @author wuyubin
     * @date  2020年05月22日
     * @return
     */
    @Override
    public void substractStock(@RequestParam("goodsId") Long goodsId) {
        if (tbGoodsMapper.updateSubstractStockNumById(goodsId) != 1) {
            throw new RuntimeException("扣減庫存異常");
        }
    }
}
module-wallet

專案結構圖如下

WalletService.java

@RestController
public class WalletService implements WalletFacade {

    @Autowired
    private TbWalletMapper tbWalletMapper;

    /**
     * <p >
     * 功能:扣減使用者錢包金額
     * </p>
     * @param userId 使用者ID
     * @param money 金額
     * @author wuyubin
     * @date  2020年05月22日
     * @return
     */
    @Override
    public void substractMoney(Long userId, BigDecimal money) {
        if (tbWalletMapper.updateSubstractMoney(userId,money) != 1) {
            throw new RuntimeException("使用者金額異常");
        }
    }
}

表結構說明

undo_log 表:seata依賴表

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 DEFAULT CHARSET=utf8;

tb_cart 表:購物車表

CREATE TABLE `tb_cart`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `user_id` bigint(20) NULL DEFAULT NULL COMMENT '使用者ID',
  `goods_id` bigint(20) NULL DEFAULT NULL COMMENT '商品ID',
  `create_time` bigint(20) NULL DEFAULT NULL COMMENT '建立時間戳',
  `update_time` bigint(20) NULL DEFAULT NULL COMMENT '更新時間戳',
  `is_deleted` tinyint(4) NULL DEFAULT 0 COMMENT '刪除標誌 0:未刪除;1:已刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '購物車表' ROW_FORMAT = Dynamic;
-- 初始化資料
INSERT INTO `tb_cart` VALUES (1, 1, 1, 1590114829756, 1590114829756, 0);

tb_goods 表:商品表

CREATE TABLE `tb_goods`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名稱',
  `stock_num` bigint(20) NULL DEFAULT NULL COMMENT '商品庫存數量',
  `money` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品金額',
  `create_time` bigint(20) NULL DEFAULT NULL COMMENT '建立時間戳',
  `update_time` bigint(20) NULL DEFAULT NULL COMMENT '更新時間戳',
  `is_deleted` tinyint(4) NULL DEFAULT NULL COMMENT '刪除標誌 0:未刪除;1:已刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品表' ROW_FORMAT = Dynamic;
-- 初始化資料
INSERT INTO `tb_goods` VALUES (1, '鍵盤', 100, 100.00, 1590132270000, 1590377130, 0);

tb_wallet 表:錢包表

CREATE TABLE `tb_wallet`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `user_id` bigint(20) NULL DEFAULT NULL COMMENT '使用者ID',
  `money` decimal(10, 2) NULL DEFAULT NULL COMMENT '金額',
  `create_time` bigint(20) NULL DEFAULT NULL COMMENT '建立時間戳',
  `update_time` bigint(20) NULL DEFAULT NULL COMMENT '更新時間戳',
  `is_deleted` tinyint(4) NULL DEFAULT NULL COMMENT '刪除標誌 0:未刪除;1:已刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '錢包表' ROW_FORMAT = Dynamic;
-- 初始化資料
INSERT INTO `tb_wallet` VALUES (1, 1, 500.00, 1590132270000, 1590377130, 0);

tb_order 表:訂單表

CREATE TABLE `tb_order`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `order_no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '訂單編號',
  `user_id` bigint(20) NULL DEFAULT NULL COMMENT '使用者ID',
  `goods_id` bigint(20) NULL DEFAULT NULL COMMENT '商品ID',
  `create_time` bigint(20) NULL DEFAULT NULL COMMENT '建立時間',
  `update_time` bigint(20) NULL DEFAULT NULL COMMENT '更新時間',
  `is_deleted` tinyint(4) NULL DEFAULT 0 COMMENT '是否刪除 0:未刪除;1:已刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '訂單表' ROW_FORMAT = Dynamic;

所有服務啟動後,請求以下介面
http://localhost:9005/cloud-web/order/add?cartId=1

檢視各服務模組日誌,你會發現均有如下資訊輸出,提示已回滾

因為在order模組中addOrder方法下,我這邊人為丟擲一個執行時異常,看樣子事務已經生效了

我們看下資料庫中資料是否已回滾正常,核實後發現資料均以回滾




接下來我們把order模組中addOrder方法下將“throw new RuntimeException();”程式碼塊註釋掉,重啟訂單模組服務後再次訪問上述介面地址,發現訪問正常

檢視各服務模組日誌,你會發現均有如下資訊輸出,提示已提交成功

我們再一次核實下資料表中的資料
購物車表已將原先記錄邏輯刪除

訂單表新增一條訂單記錄

商品表庫存數量已扣減1

錢包表金額已扣減100

最後我們測試其中一個服務出現異常,驗證下事務是否回滾正常,我們將購物車表邏輯刪除恢復正常,將商品表庫存改成0,這時我們再請求上述介面地址,發現返回異常了,我們再核實下資料,發現資料表中的資料均以回滾。好啦,SpringCloud整合分散式事務Seata的示例就到這裡啦,後續有深入的研究再分享出來。

參考資料

https://github.com/seata/seata
https://seata.io/zh-cn/docs/overview/what-is-seata.html

系列文章

SpringCloud系列之配置中心(Config)使用說明

SpringCloud系列之服務註冊發現(Eureka)應用篇

SpringCloud系列之閘道器(Gateway)應用篇

SpringCloud系列之整合Dubbo應用篇

專案原始碼

在這裡插入圖片描述

相關文章