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