一、背景
在上一節中,我們學習了Seata
的集群部署,在這篇文章中,我們使用SpringBoot
整合Seata
實現分布式事務功能,此處使用的是Seata
的AT
模式。
二、實現功能
我們存在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
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.
- 查看nacos配置列表,seata配置是否已經導入成功
- 查看nacos服務列表,serverAddr是否已經注冊成功
- 檢查client端的registry.conf里面的namespace,registry.nacos.namespace和config.nacos.namespace填入nacos的命名空間ID,默認"",server端和client端對應,namespace 為public是nacos的一個保留控件,如果您需要創建自己的namespace,最好不要和public重名,以一個實際業務場景有具體語義的名字來命名
- nacos上服務列表,serverAddr地址對應ip地址應為seata啟動指定ip地址,如:sh seata-server.sh -p 8091 -h 122.51.204.197 -m file
- 查看seata/conf/nacos-config.txt 事務分組service.vgroupMapping.trade_group=default配置與項目分組配置名稱是否一致
- telnet ip 端口 查看端口是都開放,以及防火牆狀態
2、使用 AT 模式需要的注意事項有哪些 ?
- 必須使用代理數據源,有 3 種形式可以代理數據源:
- 依賴 seata-spring-boot-starter 時,自動代理數據源,無需額外處理。
- 依賴 seata-all 時,使用 @EnableAutoDataSourceProxy (since 1.1.0) 注解,注解參數可選擇 jdk 代理或者 cglib 代理。
- 依賴 seata-all 時,也可以手動使用 DatasourceProxy 來包裝 DataSource。
- 配置 GlobalTransactionScanner,使用 seata-all 時需要手動配置,使用 seata-spring-boot-starter 時無需額外處理。
- 業務表中必須包含單列主鍵,若存在復合主鍵,請參考問題 13 。
- 每個業務庫中必須包含 undo_log 表,若與分庫分表組件聯用,分庫不分表。
- 跨微服務鏈路的事務需要對相應 RPC 框架支持,目前 seata-all 中已經支持:Apache Dubbo、Alibaba Dubbo、sofa-RPC、Motan、gRpc、httpClient,對於 Spring Cloud 的支持,請大家引用 spring-cloud-alibaba-seata。其他自研框架、異步模型、消息消費事務模型請結合 API 自行支持。
- 目前AT模式支持的數據庫有:MySQL、Oracle、PostgreSQL和 TiDB。
- 使用注解開啟分布式事務時,若默認服務 provider 端加入 consumer 端的事務,provider 可不標注注解。但是,provider 同樣需要相應的依賴和配置,僅可省略注解。
- 使用注解開啟分布式事務時,若要求事務回滾,必須將異常拋出到事務的發起方,被事務發起方的 @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