SpringCloud整合分佈式事務Seata 1.4.1 支持微服務全局異常攔截

項目依賴

  • SpringBoot 2.5.5
  • SpringCloud 2020.0.4
  • Alibaba Spring Cloud 2021.1
  • Mybatis Plus 3.4.0
  • Seata 1.4.1需要與服務器部署的Seata版本保持一致
  • 。。。。

Seata介紹

什麼是Seata

Seata三大組件

  • TC:Transaction Coordinator事務協調器,管理全局的分支事務的狀態,用於全局性事務的提交和回滾
  • TM:Transaction Manager 事務管理器,用戶開啓、提交或者回滾【全局事務】
  • RM:Resource Manager資源管理器,用於分支事務上的資源管理,向TC註冊分支事務,上報分支事務的狀態,接收TC的命令來提交或者回滾分支事務
    • 傳統XA協議實現2PC方案的RM是在數據庫層,RM本質上就是數據庫自身
    • Seata的RM是以jar包的形式嵌入在應用程序裏面

架構:TC爲單獨部署的Server服務端,TM和RM爲嵌入到應用中的Client客戶端

 

XID

  • TM請求TC開啓一個全局事務,TC會生成一個XID作爲該全局事務的編號XID,XID會在微服務的調用鏈路中傳播,保證將多個微服務對的子事務關聯在一起

Seata部署安裝

下載Seata地址

http://seata.io/zh-cn/blog/download.html

  注:我這邊下載的是1.4.1,seata部署版本需要與SpringBoot依賴的版本相對應!!!!!!

Seata部署

前期準備

  準備好Nacos、mysql

  注:nacos配置中心數據是持久化到mysql的!!!!

部署&修改配置

修改存儲模式DB

  上傳至服務器,目錄爲:/usr/local/software

# 1、創建目錄
mkdir -p /usr/local/software

# 2、解壓
unzip seata-server-1.4.1.zip

# 3、修改存儲模式 DB
cd seata/conf/
vi file.conf

 

  注:修改爲自己的mysql!!!!

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "file"

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://47.116.143.16:3306/seata?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}

  將seata需要的3張表導入數據庫中,分別是:global_table、branch_table、lock_table

官網地址:http://seata.io/zh-cn/docs/user/quickstart.html

github地址:https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
seata的mysql腳本

修改Seata 配置中心&註冊中心

  修改Seata的配置

# 修改Seata配置
cd /usr/local/software/seata/conf
vi registry.conf

  注:修改成自己的nacos信息

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  loadBalance = "RandomLoadBalance"
  loadBalanceVirtualNodes = 10

  nacos {
    application = "seata-server"
    serverAddr = "47.116.143.16:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "47.116.143.16:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
}

  因爲Seata的配置中心是nacos,需要把Seata的配置,通過腳本推送到nacos中

官網地址:https://seata.io/zh-cn/docs/user/configuration/nacos.html

腳本地址:https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh

config.txt地址(可以暫時不修改配置參數,直接到nacos中修改配置):https://github.com/seata/seata/blob/develop/script/config-center/config.txt

  將Seata配置參數推送到nacos配置中心

# 1、將github中的nacos-config.sh,傳到服務器上,目錄爲:/usr/local/software/seata/conf
# 我這邊使用的是,將腳本文件拷出,在服務創建文件夾,賦予權限
touch nacos-config.sh
chmod +x nacos-config.sh

# 2、將config.txt,放到服務器上,目錄爲:/usr/local/software/seata

  執行腳本

sh nacos-config.sh -h 47.116.143.16 -p 8848 -g SEATA_GROUP -u nacos -w nacos


-h:nacos主機地址
-p:nacos端口號
-g:nacos分組
-t:nacos命名空間
-u:nacos賬號
-w:nacos密碼

  推送成功,已將Seata配置參數推送到Nacos配置中心

  在nacos配置中心裏,修改Seata參數,具體修改參考官網如下

  具體config.txt裏的參數解釋https://seata.io/zh-cn/docs/user/configurations.html

新建2個配置需要與微服務中的配置對應上

service.vgroupMapping.${spring.alibaba.seata.tx-service-group}=default




如下
service.vgroupMapping.order_service_group=default
service.vgroupMapping.product_service_group=default

注意:分組爲:SEATA_GROUP

啓動Seata服務

  • ./seata-server.sh啓動,默認端口8091(守護進程方式啓動 nohup ./seata-server.sh &)

注意:如果seata部署在服務器,微服務在本地啓動的話,2個服務不在一個局域網下,因此沒法通信,啓動Seata時,需要指定ip和端口號

sh seata-server.sh -p 8091 -h 47.116.143.16

Seata AT模式日期序列化問題解決方案

後端服務引入kryo依賴

      <dependency>
            <groupId>com.esotericsoftware</groupId>
            <artifactId>kryo</artifactId>
            <version>4.0.2</version>
        </dependency>
        <dependency>
            <groupId>de.javakaffee</groupId>
            <artifactId>kryo-serializers</artifactId>
            <version>0.42</version>
        </dependency>

修改Seata在nacos配置中心配置

將
client.undo.logSerialization=jackson

修改爲
client.undo.logSerialization=kryo

微服務整合Seata

前期準備

  在每個微服務所連的庫,新建一張表

-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
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;

聚合工程搭建

  。。。。

項目結構

  • ybchen-common:公共模塊
  • ybchen-order-service:訂單微服務
  • ybchen-product-service:商品微服務

數據庫分表爲:order(訂單微服務庫)、product(商品微服務庫)、seata(Seata全局事務涉及的表)、nacos(Nacos配置中心,mysql持久化)

Seata依賴

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <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.4.1</version>
        </dependency>

        <!-- seata 自身序列化bug問題-開始 -->
        <dependency>
            <groupId>com.esotericsoftware</groupId>
            <artifactId>kryo</artifactId>
            <version>4.0.2</version>
        </dependency>
        <dependency>
            <groupId>de.javakaffee</groupId>
            <artifactId>kryo-serializers</artifactId>
            <version>0.42</version>
        </dependency>
        <!-- seata 自身序列化bug問題-結束 -->

分佈式事務演示

關鍵代碼片段

order-service

    @Autowired
    OrderMapper orderMapper;
    @Autowired
    ProductStockControllerFeign productStockControllerFeign;

    @Override
    //開啓分佈式事務 Seta AT模式
    @GlobalTransactional
    public ReturnT<String> add() {
        OrderDO orderDO = new OrderDO();
        int outTradeNo = new Random().nextInt(1000);
        orderDO.setOutTradeNo("T" + outTradeNo);
        orderDO.setCreateTime(new Date());
        int rows = orderMapper.insert(orderDO);
        if (rows > 0) {
            //扣減商品庫存
            ReturnT<String> reduceReturn = productStockControllerFeign.reduce();
            if (ReturnT.isSuccess(reduceReturn)) {
                log.info("購買成功");
                //TODO 模擬異常方式二
//                int num = 1 / 0;
                return ReturnT.buildSuccess("購買成功");
            }
            // 解決全局攔截器問題,通過接口響應狀態碼,來判斷是否主動拋異常!!!!!!!
            if (reduceReturn.getCode() != 0) {
                log.info("扣減商品庫存失敗,接口響應:{}", reduceReturn);
                throw new BizException(110, "扣減商品庫存失敗");
            }
            log.info("扣減商品庫存失敗");
            return ReturnT.buildError("扣減商品庫存失敗");
        }
        log.info("購買失敗");
        return ReturnT.buildError("購買失敗");
    }

product-service

   @Autowired
    ProductStockMapper productStockMapper;


    @Override
    public ReturnT<String> reduceProductStock() {
        ProductStockDO stockDO = new ProductStockDO();
        stockDO.setProductId(10086);
        stockDO.setBuyNum(1);
        stockDO.setCreateTime(new Date());
        int rows = productStockMapper.insert(stockDO);
        //TODO 模擬異常方式一
//        int num = 1 / 0;
        if (rows > 0) {
            log.info("扣減商品庫存成功,rows=" + rows);
            return ReturnT.buildSuccess("扣減商品庫存成功");
        } else {
            log.info("扣減商品庫存失敗,rows=" + rows);
            return ReturnT.buildError("扣減失敗");
        }
    }

正常情況

  場景描述:product微服務和order微服務均正常,2個微服務的事務全部提交成功,2個庫都插入數據成功

異常情況一(product微服務異常)

  場景描述:product微服務發生異常,order微服務正常情況,出現異常情況時,需要2個微服務的事務全部回滾,2個庫插入的數據都回滾

異常情況二(order微服務異常)

  場景描述:order微服務發生異常,product微服務正常,出現異常情況時,需要2個微服務的事務全部回滾,2個庫插入的數據都回滾

異常情況三(product微服務未啓動)

  場景描述:order微服務正常啓動,product微服務未啓動,需要把order微服務插入的數據回滾

項目源碼

https://github.com/543210188/ybchen-seata

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