SpringBoot+Dubbo+Seata 輕松搞定分布式事務提交數(shù)據(jù)不一致難題!
一、背景介紹
在上篇文章中,我們對 Seata 的架構設計、部署方式以及使用操作做了一個簡單的介紹,相信大家對它已經(jīng)有了初步的了解。
我們知道,在現(xiàn)有的 Spring Cloud 體系中,有兩種技術方式可以實現(xiàn)服務的遠程調用。
- 方式一:通過 Http 工具向目標服務接口發(fā)起遠程調用,比如OpenFeign、Http Client等工具。
- 方式二:通過 Dubbo 工具向目標服務接口發(fā)起遠程調用,由于 Dubbo 采用 TCP 協(xié)議進行通信,相對 HTTP 方式來說,通信效率會更高一些,應用也更廣泛
由于國內很多的項目采用 Dubbo 來實現(xiàn)服務的遠程調用,下面我們以此為例,詳細的介紹一下如何將 Dubbo 服務接入 Seata 來實現(xiàn)分布式事務操作。
二、方案實踐
我們以之前的工程為例,對其進行適度改造,改造后服務之間的交互流程可以用如下圖來簡要概括。
具體的實施過程如下。
2.1、創(chuàng)建服務接口
首先,創(chuàng)建一個簡單的 Maven 工程,命名為seata-dubbo-api,將需要對外暴露的服務接口寫入到這里。示例接口如下:
public interface StockApi {
/**
* 庫存扣減
* @param productCode
* @param count
* @return
*/
boolean deduct(String productCode, int count);
}
服務接口創(chuàng)建完成之后,接下來我們再來創(chuàng)建庫存服務和訂單服務。
2.2、創(chuàng)建庫存服務
然后,建一個 Spring Boot 工程,命名為seata-dubbo-stock,并在pom.xml中引入相關的依賴內容,示例如下:
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring-boot.version>2.2.5.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
<!-- SpringBoot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql 驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- Nacos 服務發(fā)現(xiàn) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Dubbo -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<!-- seata 分布式事務組件 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- 關聯(lián)構建的api包 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>seata-dubbo-api</artifactId>
<version>3.0-SNAPSHOT</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- 引入 springBoot 版本號 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 引入 spring cloud 版本號 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 引入 spring cloud alibaba 適配的版本號 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
接著,創(chuàng)建一個application.properties文件并配置相關配置項,示例如下:
spring.application.name=seata-dubbo-stock
server.port=9002
# 添加數(shù)據(jù)源配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata-stock
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置mybatis全局配置文件掃描
mybatis.config-locatinotallow=classpath:mybatis/mybatis-config.xml
# 配置mybatis的xml配置文件掃描目錄
mybatis.mapper-locatinotallow=classpath:mybatis/mapper/*.xml
# 設置Nacos的服務地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 指定 Dubbo 服務實現(xiàn)類的掃描基準包
dubbo.scan.base-packages=com.example.cloud.nacos.dubbo.seata
# 指定 Dubbo 服務暴露的協(xié)議
dubbo.protocol.name=dubbo
# 指定 Dubbo 服務協(xié)議端口,-1 表示自增端口,從 20880 開始
dubbo.protocol.port=-1
# 指定 Dubbo 服務注冊中心
dubbo.registry.address=nacos://${spring.cloud.nacos.discovery.server-addr}
# 添加Seata 配置項
# Seata 應用編號,默認為spring.application.name
seata.application-id=seata-dubbo-stock
# Seata 事務組編號,用于 TC 集群名
seata.tx-service-group=my_test_tx_group
# Seata 服務配置項,配置對應的虛擬組和分組的映射,其中127.0.0.1:8091為 seata 服務端的監(jiān)聽端口
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
再然后,創(chuàng)建一個 Dubbo 服務并實現(xiàn)上文創(chuàng)建的服務接口,示例如下:
@com.alibaba.dubbo.config.annotation.Service
publicclass StockApiImpl implements StockApi {
@Autowired
private StockService stockService;
@Override
public boolean deduct(String productCode, int count) {
try {
stockService.deduct(productCode, count);
// 正??鄢龓齑?,返回 true
returntrue;
} catch (Exception e) {
// 失敗扣除庫存,返回 false
returnfalse;
}
}
}
其中service和mapper層代碼和之前的庫存服務工程一樣,在此就不再重復粘貼了。
最后,創(chuàng)建一個服務啟動類并添加@EnableDiscoveryClient注解,以便將服務注冊到 Nacos。
@EnableDiscoveryClient
@MapperScan("com.example.cloud.nacos.dubbo.seata")
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
將服務啟動起來,在瀏覽器中訪問http://127.0.0.1:8848/nacos,如果不出意外的話,在 Nacos 服務列表可以看到注冊的 dubbo 服務。
2.3、創(chuàng)建訂單服務
訂單服務的創(chuàng)建過程與上文類似。
創(chuàng)建一個 Spring Boot 工程,命名為seata-dubbo-order,其pom.xml所需要的依賴內容和服務啟動類,與上文完全一致,在此就不重復粘貼了。
其中的web、service和mapper層代碼和之前的訂單服務工程也完全一致,在此就不再重復粘貼了。
下面,我們重點對OrderService服務進行改造,將通過 HTTP 工具調用遠程服務接口的邏輯移除,改成用 Dubbo 方式實現(xiàn)服務的遠程調用,示例如下:
@Component
publicclass OrderService {
@Autowired
private OrderMapper orderMapper;
@com.alibaba.dubbo.config.annotation.Reference
private StockApi stockApi;
@GlobalTransactional
public void create(String userId, String productCode, int orderCount) throws Exception {
// 通過dubbo服務,實現(xiàn)遠程扣減庫存
stockApi.deduct(productCode, orderCount);
Order order = new Order();
order.setUserId(userId);
order.setProductCode(productCode);
order.setCount(orderCount);
order.setMoney(orderCount * 100);
// 創(chuàng)建訂單
orderMapper.insert(order);
}
}
與上文類似,在application.properties配置文件中添加相關的配置項,示例如下:
spring.application.name=seata-dubbo-order
server.port=9001
# 添加數(shù)據(jù)源配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata-stock
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置mybatis全局配置文件掃描
mybatis.config-locatinotallow=classpath:mybatis/mybatis-config.xml
# 配置mybatis的xml配置文件掃描目錄
mybatis.mapper-locatinotallow=classpath:mybatis/mapper/*.xml
# 設置Nacos的服務地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 指定 Dubbo 服務實現(xiàn)類的掃描基準包
dubbo.scan.base-packages=com.example.cloud.nacos.dubbo.seata
# 指定 Dubbo 服務暴露的協(xié)議
dubbo.protocol.name=dubbo
# 指定 Dubbo 服務協(xié)議端口,-1 表示自增端口,從 20880 開始
dubbo.protocol.port=-1
# 指定 Dubbo 服務注冊中心
dubbo.registry.address=nacos://${spring.cloud.nacos.discovery.server-addr}
# 關閉dubbo客戶端服務有效性檢查
dubbo.consumer.check=false
# 添加Seata 配置項
# Seata 應用編號,默認為spring.application.name
seata.application-id=seata-dubbo-order
# Seata 事務組編號,用于 TC 集群名
seata.tx-service-group=my_test_tx_group
# Seata 服務配置項,配置對應的虛擬組和分組的映射,其中127.0.0.1:8091為 seata 服務端的監(jiān)聽端口
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
將服務啟動,再次訪問http://127.0.0.1:8848/nacos的服務列表,可以看到seata-dubbo-order也成功注冊到服務中心,界面如下。
2.4、服務測試
最后,我們還是一起來驗證一下如下兩種情況,看看是否能如期實現(xiàn)。
分布式事務正常提交
分布式事務異?;貪L
2.4.1、分布式事務正常提交
首先,重新初始化數(shù)據(jù)庫,數(shù)據(jù)庫中原始數(shù)據(jù)情況如下。
- seata-stock庫中的庫存數(shù)據(jù)
- seata-order庫中的訂單數(shù)據(jù)
接著,在瀏覽器中訪問http://127.0.0.1:9001/order/create?userId=張三&productCode=wahaha&orderCount=1,它會執(zhí)行如下兩個動作:
- 第一個:調用庫存服務,將產(chǎn)品產(chǎn)品編碼為wahaha的庫存減 1;
- 第二個:如果庫存扣減成功,插入一條產(chǎn)品編碼為wahaha數(shù)量為 1 的訂單信息;
發(fā)起接口請求后,再次回看數(shù)據(jù)庫,看看目標數(shù)據(jù)表中的數(shù)據(jù)情況。
- seata-stock庫中的庫存數(shù)據(jù)
- seata-order庫中的訂單數(shù)據(jù)
從數(shù)據(jù)結果來看,與預期一致。
我們還可以通過查看服務的日志信息,來觀察分支事務的操作情況。
其中Branch commit result信息代表分支事務的二階段操作。
2.4.2、分布式事務異?;貪L
測試完正常流程之后,下面我們再來驗證一下異常流程。
修改OrderService類中create()方法代碼,在創(chuàng)建訂單完成之后,試圖拋出異常,測試一下扣減的庫存數(shù)據(jù)是否能正?;貪L。
首先,我們還是對數(shù)據(jù)庫中原始數(shù)據(jù)進行截個圖。
- seata-stock庫中的庫存數(shù)據(jù)
- seata-order庫中的訂單數(shù)據(jù)
然后,再次在瀏覽器中訪問http://127.0.0.1:9001/order/create?userId=張三&productCode=wahaha&orderCount=1。
預期的結果是:兩個庫的數(shù)據(jù)應該都不會發(fā)生變化!
再次回看數(shù)據(jù)庫,觀察目標數(shù)據(jù)表中的數(shù)據(jù)情況。
- seata-stock庫中的庫存數(shù)據(jù)
- seata-order庫中的訂單數(shù)據(jù)
數(shù)據(jù)在5秒之內是執(zhí)行成功的,為了便于觀察數(shù)據(jù)變化,我們在上文拋異常的位置停頓了 5 秒。
過 5 秒后,再次回看數(shù)據(jù)庫表中的數(shù)據(jù)情況,結果如下。
- seata-stock庫中的庫存數(shù)據(jù)
- seata-order庫中的訂單數(shù)據(jù)
從數(shù)據(jù)最終結果來看,與預期是一致的。
在瀏覽器中訪問http://127.0.0.1:7091,登陸 Seata TC Server 服務監(jiān)控臺,還可以看到全局事務的注冊信息和狀態(tài)。
三、Seata 服務地址配置化
隨著 Seata 的集群部署數(shù)量的增加,微服務中的Seata 服務地址配置可能會越來越臃腫,此時我們可能希望借助服務注冊中心來加載 Seata TC Server 的地址,這個時候如何實現(xiàn)呢?
正如之前我們所介紹的,Seata TC Server 對主流的注冊中心也提供了集成,Seata 客戶端可以通過注冊中心獲取 Seata TC Server 所在的服務實例。
引入注冊中心之后,Seata 的交互流程可以用如下圖來概括。
下面我們以服務注冊中心 Nacos 為例,簡單的介紹一下它的配置方式。
3.1、Seata 服務端配置方式
打開 Seata 安裝包中conf/application.example.yml文件,找到store.registry相關配置屬性。
將其復制出來,然后拷貝到conf/application.yml文件中。
最后,重啟 Seata TC Server 服務即可。
訪問 nacos 的服務控制臺,如果看到 Seata 服務,說明服務注冊成功了。
3.2、Seata 客戶端端配置方式
以seata-dubbo-order服務為例,修改application.properties配置文件中seata相關的配置項,示例如下:
# Seata 應用編號,默認為spring.application.name
seata.application-id=seata-dubbo-stock
# Seata 事務組編號,用于 TC 集群名
seata.tx-service-group=my_test_tx_group
# Seata 服務配置項,配置對應的虛擬組和分組的映射,此處必須填寫default
seata.service.vgroup-mapping.my_test_tx_group=default
# 設置 seata 注冊中心類型為nacos,默認為 file
seata.registry.type=nacos
# 設置 seata 服務端中配置 nacos 相關信息
seata.registry.nacos.applicatinotallow=seata-server
seata.registry.nacos.server-addr=127.0.0.1:8848
seata.registry.nacos.group=SEATA_GROUP
此處的調整主要是增加 seata 的注冊中心配置,客戶端通過配置的注冊中心來獲取 Seata TC Server 服務實例地址。
最后將相關的服務進行重啟,再次在瀏覽器中訪問http://127.0.0.1:9001/order/create?userId=張三&productCode=wahaha&orderCount=1。
不出意外的話,數(shù)據(jù)測試結果與上文一致。
3.3、錯誤排查
如果測試中遇到類似如下異常信息。
這種情況通常是 seata 客戶端版本與服務端版本不兼容導致的,可以嘗試升級 seata 客戶端版本,以便與 seata 服務端進行適配。
以本文工程為例,Seata TC Server 服務端采用的1.5.2版本,而 Seata 客戶端采用的是1.1.0版本,可見兩者版本相差太大,當發(fā)起接口請求時就出現(xiàn)了上文的錯誤信息。
通過查閱版本號適配情況,Seata 客戶端的1.3.0版本可以與 Seata 服務端進行兼容,因此可以直接升級spring-cloud-alibaba的版本號,示例如下:
<!--原來是 2.2.1.RELEASE版本,將其升級為 2.2.3.RELEASE-->
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
由于spring-cloud-alibaba的2.2.3.RELEASE版本中集成的seata客戶端版本號為1.3.0,當重啟服務再次發(fā)起接口請求時,一切恢復正常。
因此當代碼和配置都沒有問題時,服務無法啟動或者運行錯誤,通常情況與版本號有很大的關系??梢詸z查一下工程中的版本號與官方要求的版本號是否出現(xiàn)不兼容現(xiàn)象。
四、小結
最后總結一下,本文主要圍繞 dubbo 整合 seata 實現(xiàn)服務分布式事務操作做了一次知識內容的總結和整理,內容比較多,如果有描述不對的地方,歡迎大家留言指出。
如果當前的服務工程采用的是 openFeign 來實現(xiàn)服務遠程調用,也可以通過集成spring-cloud-starter-alibaba-seata依賴包實現(xiàn)分布式事務操作,其實現(xiàn)原理也是在遠程調用的請求頭部中插入全局事務 ID,依次傳遞到下游服務中,從而保證全局事務的統(tǒng)一提交和回滾操作。
五、參考
1、https://seata.apache.org/zh-cn/docs/overview/what-is-seata/
2、https://www.iocoder.cn/Seata/install/
3、https://www.iocoder.cn/Spring-Boot/Seata/