SpringCloud下基于Seata AT的分布式事務(wù)實(shí)踐
Seata是Spring Cloud Alibaba中一款開源的分布式事務(wù)解決方案,本文具體就Seata的AT模式進(jìn)行介紹、實(shí)踐

在Seata的設(shè)計(jì)架構(gòu)中有三個(gè)角色,具體如下
- TC(Transaction Coordinator): 事務(wù)協(xié)調(diào)者。維護(hù)全局和分支事務(wù)的狀態(tài),驅(qū)動(dòng)全局事務(wù)提交或回滾
- TM(Transaction Manager): 事務(wù)管理器。定義全局事務(wù)的范圍,用于開始、提交、回滾全局事務(wù)
- RM(Resource Manager): 資源管理器。管理分支事務(wù)處理的資源,與TC通訊以注冊分支事務(wù)和報(bào)告分支事務(wù)的狀態(tài),并驅(qū)動(dòng)分支事務(wù)進(jìn)行提交或回滾
TC是Seata的服務(wù)端需獨(dú)立部署,而TM、RM則是作為Seata的客戶端與各微服務(wù)進(jìn)行集成。三者之間的流程關(guān)系如下圖所示。具體地,Seata的分布式事務(wù)模型是基于 2PC(兩階段提交,Tow-Phase Commit) 協(xié)議,基本執(zhí)行流程如下
- TM向TC申請開啟一個(gè)分布式事務(wù),事務(wù)創(chuàng)建成功后會生成一個(gè)全局唯一的事務(wù)ID,即所謂的XID
- RM向TC注冊分支事務(wù),匯報(bào)資源準(zhǔn)備狀態(tài)
- TM通知TC 提交/回滾 分布式事務(wù),事務(wù)一階段結(jié)束
- TC匯總各分支事務(wù)信息,決定分布式事務(wù)是提交還是回滾
- TC通知所有RM 提交/回滾 資源,分布式事務(wù)的二階段結(jié)束

具體地,Seata支持AT、TCC、Saga、XA四種模式。這里就AT模式進(jìn)行展開說明,其是一種無侵入的分布式事務(wù)解決方案,使得開發(fā)者只需關(guān)注自己的業(yè)務(wù)SQL即可。Seata會自動(dòng)進(jìn)行二階段的提交/回滾。流程如下
- 一階段: Seata對業(yè)務(wù)SQL進(jìn)行攔截、語義解析,進(jìn)而確定業(yè)務(wù)SQL需要操作的相關(guān)業(yè)務(wù)數(shù)據(jù)記錄。然后在執(zhí)行業(yè)務(wù)SQL前,將相關(guān)業(yè)務(wù)數(shù)據(jù)記錄保存為Before Image。在執(zhí)行業(yè)務(wù)SQL后,再將其保存成After Image。并最終生成行鎖。上述操作會在一個(gè)數(shù)據(jù)庫的本地事務(wù)內(nèi)完成,以保證一階段操作的原子性

- 二階段提交: 二階段提交時(shí),因?yàn)闃I(yè)務(wù)SQL在一階段已經(jīng)提交至各數(shù)據(jù)庫。故Seata只需將一階段保存的快照數(shù)據(jù)和行鎖刪掉,完成數(shù)據(jù)清理即可

- 二階段回滾: 二階段回滾時(shí),首先需要對數(shù)據(jù)庫當(dāng)前相關(guān)的數(shù)據(jù)與After Image進(jìn)行比對,如果完全一致,這說明未發(fā)生臟寫。即沒有被除當(dāng)前全局事務(wù)之外的其他操作修改過,可以放心進(jìn)行回滾。而具體回滾則是通過Before Image生成逆向SQL來進(jìn)行反向補(bǔ)償,并最終刪除相應(yīng)快照數(shù)據(jù)和行鎖

搭建Seata Server環(huán)境
基于Docker Compose的服務(wù)部署
Seata Server事實(shí)上就是上文提到的事務(wù)協(xié)調(diào)者TC。這里通過Docker Compose來進(jìn)行部署,如下所示。可以看到我們不僅創(chuàng)建了Seata Server服務(wù),還創(chuàng)建了MySQL、Nacos服務(wù)。后面會一一進(jìn)行解釋
#?Compose?版本
version:?'3.8'
#?定義Docker服務(wù)
services:
??#?Seata?服務(wù)
??Seata-Service-1:
????image:?seataio/seata-server:1.3.0
????container_name:?Seata-Service-1
????ports:
??????-?"9091:8091"
????networks:
??????seata_service_net:
????????ipv4_address:?120.120.120.21
????depends_on:
??????-?MySQL-Service-1
??????-?Nacos-Service-1
??#?MySQL?服務(wù)
??MySQL-Service-1:
????image:?mysql:5.7
????container_name:?MySQL-Service-1
????ports:
??????-?"9306:3306"
????environment:
??????MYSQL_ROOT_PASSWORD:?12345
????networks:
??????seata_service_net:
????????ipv4_address:?120.120.120.22
??#?Nacos?服務(wù)
??Nacos-Service-1:
????image:?nacos/nacos-server:1.4.2
????container_name:?Nacos-Service-1
????ports:
??????-?"9848:8848"
????environment:
??????MODE:?standalone
????networks:
??????seata_service_net:
????????ipv4_address:?120.120.120.23
#?定義網(wǎng)絡(luò)
networks:
??seata_service_net:
????ipam:
??????config:
????????-?subnet:?120.120.120.0/24
配置Seata Server的持久化
Seata-Server支持多種持久化方式包括文件、DB、Redis等,默認(rèn)為文件File。這里我們使用剛剛部署MySQL-Service-1服務(wù)進(jìn)行持久化。進(jìn)入Seata-Service-1容器,修改/seata-server/resources下的file.conf文件,將存儲模式修改為db,同時(shí)修改相應(yīng)的數(shù)據(jù)庫連接信息。如下所示,可以看到這里datasource我們選擇了druid

然后,通過數(shù)據(jù)庫客戶端連接MySQL-Service-1實(shí)例。首先創(chuàng)建file.conf文件中所連接的數(shù)據(jù)庫seataServer,然后在該數(shù)據(jù)庫中執(zhí)行建表語句。其中SQL腳本可通過Github進(jìn)行獲取,地址如下所示
#?下載地址:?Seata?Server使用DB進(jìn)行持久化的SQL初始化腳本
https://github.com/seata/seata/blob/1.3.0/script/server/db/mysql.sql
效果如下所示

配置Seata Server的注冊中心、配置中心
前面提到,我們還創(chuàng)建了一個(gè)Nacos容器,即Nacos-Service-1實(shí)例。其是用于作為整個(gè)分布式環(huán)境的配置中心、注冊中心。同樣進(jìn)入Seata-Service-1容器,修改/seata-server/resources下的registry.conf文件。將注冊中心、配置中心均設(shè)置Nacos。詳細(xì)配置如下所示

導(dǎo)入配置信息至Nacos
事實(shí)上對于Seata而言,其配置信息支持兩種形式:本地文件、配置中心。對于后者而言,我們需要將Seata的相關(guān)配置項(xiàng)導(dǎo)入到配置中心。同樣,我們需要通過Github來下載配置文件config.txt及相應(yīng)的導(dǎo)入腳本nacos-config.sh
#?下載地址:?配置中心的配置項(xiàng)
https://github.com/seata/seata/blob/1.3.0/script/config-center/config.txt
#?下載地址:?用于將配置項(xiàng)導(dǎo)入至Nacos的腳本
https://github.com/seata/seata/blob/1.3.0/script/config-center/nacos/nacos-config.sh
對于配置文件config.txt而言,有以下兩點(diǎn)需要注意
- 將配置項(xiàng)store.mode存儲模式修改為db,同時(shí)修改以store.db為前綴的相關(guān)配置項(xiàng),保證其與file.conf文件中相關(guān)數(shù)據(jù)庫的配置一致
- 配置項(xiàng)service.vgroupMapping.my_test_tx_group=default的含義是,事務(wù)分組my_test_tx_group使用名為default的Seata Server集群。換言之,my_test_tx_group即為事務(wù)分組的名稱,支持自定義。這里我們直接使用默認(rèn)的事務(wù)分組名。而Seata Server集群名default實(shí)際上就是來自registry.conf文件的cluster配置項(xiàng)

在完成配置文件config.txt的修改后,即可利用Shell腳本導(dǎo)入至Nacos中。值得一提的是,配置文件config.txt應(yīng)與Shell腳本的上一級目錄保持平行。然后在Shell腳本所在目錄中執(zhí)行如下命令即可
#?執(zhí)行Shell腳本
sh?nacos-config.sh?-h?localhost?-p?9848
該Shell腳本支持的選項(xiàng)如下所示
- -h: Nacos服務(wù)的IP地址,默認(rèn)為localhost
- -p: Nacos服務(wù)的Port端口,默認(rèn)為8848
- -g: Nacos分組名,默認(rèn)為SEATA_GROUP
- -t: Nacos命名空間ID。默認(rèn)為“”,即使用public命名空間
- -u: Nacos服務(wù)的用戶名
- -w: Nacos服務(wù)的密碼
效果如下所示

至此,Seata server相關(guān)環(huán)境及配置就完成了。最后,重啟Seata-Service-1容器以讓修改生效即可。通過Nacos的Web管理頁面可以看到,Seata服務(wù)已經(jīng)注冊到Nacos

搭建order服務(wù)
POM依賴
這里通過SpringBoot搭建一個(gè)微服務(wù)——order服務(wù)。這里給出關(guān)鍵性的依賴及版本,如下所示
<dependencyManagement>
??<dependencies>
??
????
????<dependency>
??????<groupId>org.springframework.bootgroupId>
??????<artifactId>spring-boot-dependenciesartifactId>
??????<version>2.3.2.RELEASEversion>
??????<type>pomtype>
??????<scope>importscope>
????dependency>
??
????
????<dependency>
??????<groupId>org.springframework.cloudgroupId>
??????<artifactId>spring-cloud-dependenciesartifactId>
??????<version>Hoxton.SR8version>
??????<type>pomtype>
??????<scope>importscope>
????dependency>
??
????
????<dependency>
??????<groupId>com.alibaba.cloudgroupId>
??????<artifactId>spring-cloud-alibaba-dependenciesartifactId>
??????<version>2.2.3.RELEASEversion>
??????<type>pomtype>
??????<scope>importscope>
????dependency>
??dependencies>
dependencyManagement>
<dependencies>
??
??<dependency>
????<groupId>com.alibaba.cloudgroupId>
????<artifactId>spring-cloud-starter-alibaba-seataartifactId>
????<exclusions>
??????<exclusion>
????????<groupId>io.seatagroupId>
????????<artifactId>seata-spring-boot-starterartifactId>
??????exclusion>
????exclusions>
??dependency>
??
??<dependency>
????<groupId>io.seatagroupId>
????<artifactId>seata-spring-boot-starterartifactId>
????<version>1.3.0version>
??dependency>
??
??<dependency>
????<groupId>com.alibaba.cloudgroupId>
????<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
??dependency>
??
??<dependency>
????<groupId>com.alibaba.cloudgroupId>
????<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
??dependency>
??
??<dependency>
????<groupId>com.alibabagroupId>
????<artifactId>fastjsonartifactId>
????<version>1.2.76version>
??dependency>
??
??<dependency>
????<groupId>com.baomidougroupId>
????<artifactId>mybatis-plus-boot-starterartifactId>
????<version>3.4.1version>
??dependency>
??
dependencies>
服務(wù)配置
order服務(wù)的配置文件application.yml,如下所示。這里關(guān)于Seata數(shù)據(jù)源的代理,我們選擇自動(dòng)代理的方式。此外配置文件中的相關(guān)IP、端口信息均為容器內(nèi)部的IP、Port。因?yàn)閷τ赟pringBoot服務(wù)我們也會通過Docker的方式進(jìn)行構(gòu)建、打包及部署
server:
??port:?89
spring:
??application:
????name:?order
??datasource:
????type:?com.alibaba.druid.pool.DruidDataSource
????driver-class-name:?com.mysql.jdbc.Driver
????url:?jdbc:mysql://120.120.120.42:3306/order?allowPublicKeyRetrieval=true&useSSL=false
????username:?root
????password:?12345
??cloud:
????nacos:
??????discovery:
????????#?注冊中心?Nacos?地址信息
????????server-addr:?120.120.120.23:8848
????alibaba:
??????seata:
????????#?配置所使用的事務(wù)分組名稱
????????tx-service-group:?my_test_tx_group
#?Mybatis-Plus?配置
mybatis-plus:
??mapper-locations:?classpath:mapper/*.xml
#?Seata?Server配置
seata:
??#?Seata服務(wù)端所在注冊中心的配置信息
??registry:
????#?注冊中心類型
????type:?nacos
????nacos:
??????#?Seata服務(wù)端的服務(wù)名
??????application:?seata-server
??????#?Seata服務(wù)端所在的注冊中心信息
??????server-addr:?120.120.120.23:8848
??????username:?nacos
??????password:?nacos
??????group:?SEATA_GROUP
??#?Seata服務(wù)端所在配置中心的配置信息
??config:
????type:?nacos
????nacos:
??????server-addr:?120.120.120.23:8848
??????username:?nacos
??????password:?nacos
??????group:?SEATA_GROUP
??#?使能Seata自動(dòng)代理數(shù)據(jù)源
??enable-auto-data-source-proxy:?true
#?Actuator配置:?開啟所有端點(diǎn)
management:
??endpoints:
????web:
??????exposure:
????????include:?"*"
??????base-path:?/actuator
Controller層
在order服務(wù)中通過添加一個(gè)Controller類用于進(jìn)行測試,核心代碼實(shí)現(xiàn)如下。addRecord方法邏輯很簡單。首先向自己的數(shù)據(jù)庫插入一條記錄,然后再調(diào)用另外一個(gè)服務(wù)pyament的接口。由于該方法是作為分布式事務(wù)的發(fā)起者,故需要在方法上添加 @GlobalTransactional 注解,以開啟一個(gè)分布式事務(wù)
@RestController
@RequestMapping("order2")
public?class?OrderController2?{
????//?使用?注冊中心的服務(wù)名
????public?static?final?String?PAYMENT_URL?=?"http://payment";
????@Autowired
????private?RestTemplate?restTemplate;
????@Autowired
????private?OrderRecordMapper?orderRecordMapper;
????@GlobalTransactional
????@GetMapping("/addRecord")
????public?String?addRecord(@RequestParam?String?name,?@RequestParam?Integer?total)?{
????????OrderRecord?orderRecord?=?OrderRecord.builder()
????????????.name(name)
????????????.total(total)
????????????.build();
????????//?save方法通過MybatisPlus中的自定義SQL實(shí)現(xiàn)
????????orderRecordMapper.save(orderRecord);
????????String?msg?=?restTemplate.getForObject(PAYMENT_URL?+"/pay3/test1?name={1}",?String.class,?name);
????????return?"OK";
????}
}
...
@Configuration
public?class?RestTemplateConfig?{
????@Bean
????@LoadBalanced
????public?RestTemplate?restTemplate()?{
????????return?new?RestTemplate();
????}
}
...
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("orderRecord")?????//?指定數(shù)據(jù)庫的表名
public?class?OrderRecord?{
????@TableId
????private?int?id;
????private?String?name;
????private?int?total;
}
服務(wù)部署
首先將SpringBoot服務(wù)打包為Docker鏡像,然后通過Docker Compose進(jìn)行服務(wù)部署。為保證各服務(wù)、容器間的網(wǎng)絡(luò)互通互聯(lián),這里order服務(wù)的容器同樣需要使用Seata Server所在的名為seata_service_net的自定義網(wǎng)絡(luò)。由于docker-compose.yml中自定義網(wǎng)絡(luò)在創(chuàng)建后,其最終的網(wǎng)絡(luò)名稱是包含項(xiàng)目名的。故首先用docker network ls查看該網(wǎng)絡(luò)的全名。如下所示,即該網(wǎng)絡(luò)全名為seata-service_seata_service_net

在分布式環(huán)境下,每個(gè)微服務(wù)都是使用自己的數(shù)據(jù)庫。這一點(diǎn)在order服務(wù)的application.yml配置文件中也可以看到。故在docker-compose.yml中我們同樣需要為order服務(wù)創(chuàng)建一個(gè)MySQL實(shí)例。如下所示
#?Compose?版本
version:?'3.8'
#?定義Docker服務(wù)
services:
??#?Web服務(wù)
??Order-Service:
????image:?aaron1995/spring_boot_order:1.0
????container_name:?Order-Service
????ports:
??????-?"8089:89"
????networks:
??????seata-service_seata_service_net:
????????ipv4_address:?120.120.120.41
????depends_on:
??????-?Order-MySQL
??#?MySQL?服務(wù)
??Order-MySQL:
????image:?mysql:5.7
????container_name:?Order-MySQL
????ports:
??????-?"9307:3306"
????environment:
??????MYSQL_ROOT_PASSWORD:?12345
????networks:
??????seata-service_seata_service_net:
????????ipv4_address:?120.120.120.42
#?定義網(wǎng)絡(luò)
networks:
??#?聲明名為seata-service_seata_service_net的網(wǎng)絡(luò)是一個(gè)已存在的網(wǎng)絡(luò)
??seata-service_seata_service_net:
????external:?true
數(shù)據(jù)庫初始化
通過數(shù)據(jù)庫客戶端連接Order服務(wù)的數(shù)據(jù)庫,即Order-MySQL容器。首先order服務(wù)所連接的數(shù)據(jù)庫order,然后在該數(shù)據(jù)庫中執(zhí)行相關(guān)業(yè)務(wù)的建表語句
#?建庫建表
create?database?`order`;
use?`order`;
create?table?orderRecord?(
????id?int?not?null?auto_increment,
????name?varchar(255)?null,
????total?int?null,
????primary?key?(id)
);
當(dāng)然上述這些并無什么特別,只是業(yè)務(wù)方面需要。而為了保證Seata在事務(wù)出現(xiàn)異常時(shí)可以實(shí)現(xiàn)對業(yè)務(wù)數(shù)據(jù)進(jìn)行回滾,我們還需要在業(yè)務(wù)的數(shù)據(jù)庫中建立undo_log表。類似地,該SQL腳本也可通過Github進(jìn)行獲取,下載地址如下所示
#?下載地址:?業(yè)務(wù)數(shù)據(jù)庫中undo_log表的建表SQL腳本
https://github.com/seata/seata/blob/1.3.0/script/client/at/db/mysql.sql
效果如下所示

搭建payment服務(wù)
為了驗(yàn)證分布式事務(wù),自然不能只有一個(gè)微服務(wù)。故這里類似地我們再搭建一個(gè)payment服務(wù)。當(dāng)然基本搭建過程與order服務(wù)并無明顯差異。首先在POM依賴方面,payment服務(wù)的POM依賴與order服務(wù)一致,同樣也需要引入Seata、Nacos等相關(guān)依賴。其次在服務(wù)配置方面,payment服務(wù)的application.yml配置文件中關(guān)于Seata、Nacos相關(guān)的配置自然與order服務(wù)并無二致。但需調(diào)整修改其所連接的數(shù)據(jù)庫信息,如下所示。即使用自身的數(shù)據(jù)庫
server:
??port:?8011
spring:
??application:
????name:?payment
??datasource:
????type:?com.alibaba.druid.pool.DruidDataSource
????driver-class-name:?com.mysql.jdbc.Driver
????url:?jdbc:mysql://120.120.120.52:3306/payment?allowPublicKeyRetrieval=true&useSSL=false
????username:?root
????password:?12345
在payment服務(wù)中添加相應(yīng)的Controller方法
@RestController
@RequestMapping("pay3")
public?class?PaymentController3?{
????@Autowired
????private?PayRecordMapper?payRecordMapper;
????@GetMapping("/test1")
????public?String?test1(@RequestParam?String?name)?{
????????//?更新自身數(shù)據(jù)庫中id為1的記錄
????????PayRecord?payRecord?=?PayRecord.builder()
????????????.id(1)
????????????.serial(?name?+",?"+?UUID.randomUUID().toString()?)
????????????.build();
????????payRecordMapper.updateById(payRecord);
????????if(name.equals("Tony"))?{
????????????throw?new?RuntimeException("發(fā)生業(yè)務(wù)異常");
????????}
????????return?"OK";
????}
}
...
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("payRecord")?????//?指定數(shù)據(jù)庫的表名
public?class?PayRecord?{
????private?int?id;
????private?String?serial;
}
類似地,將payment打包為Docker鏡像后,通過docker compose進(jìn)行部署,如下所示
#?Compose?版本
version:?'3.8'
#?定義Docker服務(wù)
services:
??#?Web服務(wù)
??Payment-Service:
????image:?aaron1995/spring_boot_payment:1.0
????container_name:?Payment-Service
????ports:
??????-?"8015:8011"
????networks:
??????seata-service_seata_service_net:
????????ipv4_address:?120.120.120.51
????depends_on:
??????-?Payment-MySQL
??#?MySQL?服務(wù)
??Payment-MySQL:
????image:?mysql:5.7
????container_name:?Payment-MySQL
????ports:
??????-?"9308:3306"
????environment:
??????MYSQL_ROOT_PASSWORD:?12345
????networks:
??????seata-service_seata_service_net:
????????ipv4_address:?120.120.120.52
#?定義網(wǎng)絡(luò)
networks:
??#?聲明名為seata-service_seata_service_net的網(wǎng)絡(luò)是一個(gè)已存在的網(wǎng)絡(luò)
??seata-service_seata_service_net:
????external:?true
最后,在payment服務(wù)所使用的數(shù)據(jù)庫Payment-MySQL容器上完成建庫建表操作。不僅包含業(yè)務(wù)表,也包含上文提到的undo_log表。如下所示,由于PaymentController3的test1方法的業(yè)務(wù)邏輯是更新id為1記錄,故這里也提前插入便于后續(xù)演示

測試驗(yàn)證
現(xiàn)在各服務(wù)部署完成后,從Nacos頁面可以看到Seata Server、order、payment服務(wù)均已注冊上線

當(dāng)向order服務(wù)的接口發(fā)送HTTP請求時(shí),由于name不為Tony未拋出異常。order的表中新增了一條記錄。而payment表id為1的數(shù)據(jù)也被正確地更新了

而當(dāng)HTTP請求的name參數(shù)為Tony時(shí),payment服務(wù)發(fā)生異常。不僅payment表未發(fā)生更新,而且order的表中也沒有新增數(shù)據(jù)。即被正常回滾

- 在本次實(shí)踐過程中,發(fā)現(xiàn)通過Mybatis Plus Mapper內(nèi)置的insert方法進(jìn)行插入的數(shù)據(jù)在發(fā)生異常時(shí)無法進(jìn)行回滾,故在order服務(wù)中添加記錄是通過在相應(yīng)的xml文件自定義SQL實(shí)現(xiàn)的。后者在發(fā)生異常時(shí),可以對插入的數(shù)據(jù)進(jìn)行回滾
