Seata整合SpringBoot和Mybatis


一、背景

上一節中,我們學習了Seata的集群部署,在這篇文章中,我們使用SpringBoot整合Seata實現分布式事務功能,此處使用的是SeataAT模式。

二、實現功能

我們存在2個服務 賬戶服務 account-service訂單服務 order-service,在訂單服務中調用 賬戶服務。

訂單服務中調用賬戶服務是通過 RestTemplate來實現的。

測試場景:

1、賬戶服務正常,訂單服務正常,結果:賬戶服務正常扣款,產生訂單。

2、賬戶服務正常,訂單服務正常,在整個分布式事務中發生了異常,結果: 賬戶服務沒有扣款,沒有產生訂單。

三、每個服務使用到的技術

1、賬戶服務

SpringBoot、Seata、Mybatis、nacos、druid

2、訂單服務

SpringBoot、Seata、Mybatis、nacos、Hikari

其中 SpringBoot 整合 Seata 是通過 seata-spring-boot-starter 這個來實現的,不使用 seata-all來實現。

四、服務實現

1、賬戶服務實現

賬戶服務,提供一個簡單的扣除賬戶余額的功能,比較簡單。

注意項:

1、開啟自動數據源代理。

2、引入druid,不需要自動配置數據源。

3、注意事務分組

1、引入jar包

此處只引入幾個 核心的 包,其余的包沒有列在下方,比如mybatis等,注意和seata整合使用的是seata-spring-boot-starter

<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <version>1.4.2</version>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid-spring-boot-starter</artifactId>
  <version>1.2.4</version>
</dependency>
<dependency>
  <groupId>com.alibaba.nacos</groupId>
  <artifactId>nacos-client</artifactId>
  <version>1.3.2</version>
</dependency>

2、項目配置

配置

3、建表語句

create database seata_account;
use seata_account;
create table account(
    id int unsigned auto_increment primary key comment '主鍵',
    name varchar(20) comment '用戶名',
    balance bigint comment '賬戶余額,單位分'
) engine=InnoDB comment '賬戶表';
insert into account(id,name,balance) values (1,'張三',100000);
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB COMMENT ='AT transaction mode undo table';

每個業務庫必須存在一張 undo_log

2、訂單服務實現

提供一個接口,實現產生訂單,扣除賬戶余額。

注意事項:

1、訂單服務 關閉默認的數據源代理,自己配置數據源代理。

2、使用 Hikari數據源來實現,因為使用的是 AT模式,所以需要使用 DataSourceProxy 來代理數據源。

3、訂單服務調用賬戶服務是采用的 RestTemplate,因此需要手動配置RestTemplate的攔截器,實現xid的傳輸。

4、在seata1.4.2中存在一個bug,如果業務表中數據類型是datetime類型,可能undolog無法序列化成功,可以采用timestamp或別的方式來處理。

5、業務庫中需要存在 undo_log 表。

1、引入jar包

此處不引入 druid,注意和seata整合使用的是seata-spring-boot-starter

<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <version>1.4.2</version>
</dependency>
<dependency>
  <groupId>com.alibaba.nacos</groupId>
  <artifactId>nacos-client</artifactId>
  <version>1.3.2</version>
</dependency>

2、項目配置

配置

3、配置數據源代理

package com.huan.seata.config;

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/** * @author huan.fu 2021/9/24 - 上午10:34 */
@Configuration
public class DataSourceConfig {
    
    @Autowired
    private DataSourceProperties dataSourceProperties;
    
    @Bean
    public DataSource dataSourceProxy() {
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setJdbcUrl(dataSourceProperties.getUrl());
        hikariDataSource.setUsername(dataSourceProperties.getUsername());
        hikariDataSource.setPassword(dataSourceProperties.getPassword());
        hikariDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        return new DataSourceProxy(hikariDataSource);
    }
}

AT模式種,數據源代理一定要是 DataSourceProxy這個。

4、配置RestTemplate傳遞xid

RestTemplate傳遞xid

5、@GlobalTransactional分布式事務

@Service
@RequiredArgsConstructor
@Slf4j
public class BusinessServiceImpl implements BusinessService {
    
    private final OrderService orderService;
    private final RestTemplate restTemplate;
    
    @Override
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createAccountOrder(Integer accountId, Long amount, boolean hasException) {
        System.out.println("createAccountOrder:" + RootContext.getXID());
        // 1、遠程扣減賬戶余額
        remoteDebit(accountId, amount);
        
        // 2、下訂單
        orderService.createOrder(accountId, amount);
        
        if (hasException) {
            throw new RuntimeException("發生了異常,分布式事物需要會滾");
        }
    }
    
    private void remoteDebit(Integer accountId, Long amount) {
        String url = "http://localhost:50001/account/debit?id=" + accountId + "&amount=" + amount;
        String result = restTemplate.getForObject(url, String.class);
        log.info("遠程扣減庫存結果:[{}]", result);
    }
}

3、事務分組需要和配置中心對應上

此處以訂單服務來演示,如何和配置中心對應上的。

事務分組

每個服務的事務分組可能不一樣,但是需要和配置中心對應上。
比如:
order-service 中的配置分組為:seata.tx-service-group=tx_order_service_group
配置中心必須存在 service.vgroupMapping.tx_order_service_group=default 配置項,default是集群,是服務端配置文件中指定的

五、演示

1、沒有發生異常

訪問:http://localhost:50002/createOrder?accountId=1&amount=10&hasException=false

正常創建訂單,和扣除余額。

沒有發生異常

2、發生異常

訪問: http://localhost:50002/createOrder?accountId=1&amount=10&hasException=true

不產生訂單,不扣除余額。

發生異常

六、可能遇到的問題

1.Nacos 作為 Seata 配置中心時,項目啟動報錯找不到服務。如何排查,如何處理?

A: 異常:io.seata.common.exception.FrameworkException: can not register RM,err:can not connect to services-server.

  1. 查看nacos配置列表,seata配置是否已經導入成功
  2. 查看nacos服務列表,serverAddr是否已經注冊成功
  3. 檢查client端的registry.conf里面的namespace,registry.nacos.namespace和config.nacos.namespace填入nacos的命名空間ID,默認"",server端和client端對應,namespace 為public是nacos的一個保留控件,如果您需要創建自己的namespace,最好不要和public重名,以一個實際業務場景有具體語義的名字來命名
  4. nacos上服務列表,serverAddr地址對應ip地址應為seata啟動指定ip地址,如:sh seata-server.sh -p 8091 -h 122.51.204.197 -m file
  5. 查看seata/conf/nacos-config.txt 事務分組service.vgroupMapping.trade_group=default配置與項目分組配置名稱是否一致
  6. telnet ip 端口 查看端口是都開放,以及防火牆狀態

2、使用 AT 模式需要的注意事項有哪些 ?

  1. 必須使用代理數據源,有 3 種形式可以代理數據源:
  • 依賴 seata-spring-boot-starter 時,自動代理數據源,無需額外處理。
  • 依賴 seata-all 時,使用 @EnableAutoDataSourceProxy (since 1.1.0) 注解,注解參數可選擇 jdk 代理或者 cglib 代理。
  • 依賴 seata-all 時,也可以手動使用 DatasourceProxy 來包裝 DataSource。
  1. 配置 GlobalTransactionScanner,使用 seata-all 時需要手動配置,使用 seata-spring-boot-starter 時無需額外處理。
  2. 業務表中必須包含單列主鍵,若存在復合主鍵,請參考問題 13 。
  3. 每個業務庫中必須包含 undo_log 表,若與分庫分表組件聯用,分庫不分表。
  4. 跨微服務鏈路的事務需要對相應 RPC 框架支持,目前 seata-all 中已經支持:Apache Dubbo、Alibaba Dubbo、sofa-RPC、Motan、gRpc、httpClient,對於 Spring Cloud 的支持,請大家引用 spring-cloud-alibaba-seata。其他自研框架、異步模型、消息消費事務模型請結合 API 自行支持。
  5. 目前AT模式支持的數據庫有:MySQL、Oracle、PostgreSQL和 TiDB。
  6. 使用注解開啟分布式事務時,若默認服務 provider 端加入 consumer 端的事務,provider 可不標注注解。但是,provider 同樣需要相應的依賴和配置,僅可省略注解。
  7. 使用注解開啟分布式事務時,若要求事務回滾,必須將異常拋出到事務的發起方,被事務發起方的 @GlobalTransactional 注解感知到。provide 直接拋出異常 或 定義錯誤碼由 consumer 判斷再拋出異常。

3、AT 模式和 Spring @Transactional 注解連用時需要注意什么 ?

@Transactional 可與 DataSourceTransactionManager 和 JTATransactionManager 連用分別表示本地事務和XA分布式事務,大家常用的是與本地事務結合。當與本地事務結合時,@Transactional和@GlobalTransaction連用,@Transactional 只能位於標注在@GlobalTransaction的同一方法層次或者位於@GlobalTransaction 標注方法的內層。這里分布式事務的概念要大於本地事務,若將 @Transactional 標注在外層會導致分布式事務空提交,當@Transactional 對應的 connection 提交時會報全局事務正在提交或者全局事務的xid不存在。

4、數據庫開啟自動更新時間戳導致臟數據無法回滾

由於業務提交,seata記錄當前鏡像后,數據庫又進行了一次時間戳的更新,導致鏡像校驗不通過。

**解決方案1: **關閉數據庫的時間戳自動更新。數據的時間戳更新,如修改、創建時間由代碼層面去維護,比如MybatisPlus就能做自動填充。

解決方案2: update語句別把沒更新的字段也放入更新語句。

5、Seata 使用注冊中心注冊的地址有什么限制?

Seata 注冊中心不能注冊 0.0.0.0 或 127.0.0.1 的地址,當自動注冊為上述地址時可以通過啟動參數 -h 或容器環境變量SEATA_IP來指定。當和業務服務處於不同的網絡時注冊地址可以指定為 NAT_IP或公網IP,但需要保證注冊中心的健康檢查探活是通暢的。

以上的幾個問題,來自seata官網 : http://seata.io/zh-cn/docs/overview/faq.html

七、代碼地址

代碼地址:https://gitee.com/huan1993/spring-cloud-parent/tree/master/seata/seata-springboot-mybatis


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM