前文我們提到了Seata的AT和TCC模式,本文中我們針對這兩個模式進行深入分析和開發實踐。
根據 官方文檔 及提供的 博客 我們先回顧一下AT模式下分布式事務的原理
AT 模式的一階段、二階段提交和回滾均由 Seata 框架自動生成,用戶只需編寫「業務 SQL」,便能輕鬆接入分布式事務,AT 模式是一種對業務無任何侵入的分布式事務解決方案。
本文demo使用的環境是基於
首先將 seata-server 在伺服器搭建起來,由於我們使用 nacos作為seata的註冊中心、apollo為註冊中心,所以先將這兩個組件搭建起來,具體的安裝方法請分別參考各自的官方文檔。 nacos apollo
nacos 和 apollo 搭起來以後,我們開始搭建 seata-server 以下是 docker-compose 的配置:
version: "3.1"services: seata-server: image: seataio/seata-server:latest hostname: seata-server ports: - 8091:8091 environment: - SEATA_PORT=8091 - SEATA_IP={你的IP} - SEATA_CONFIG_NAME=file:/seata-server/resources/registry volumes: - ./seata/registry.conf:/seata-server/resources/registry.conf expose: - 8091
修改 registry.conf 配置文件,由於我們使用 nacos 作為註冊中心,apollo 作為配置中心,所以需要修改到以下配置:
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "你的IP:埠" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "" password = "" }}config { # file、nacos 、apollo、zk、consul、etcd3 type = "apollo" apollo { appId = "seata-server" apolloMeta = "http://你的IP:埠" namespace = "application" env= "dev" apolloAccesskeySecret = "" } }
注意:seata-server 是可以配置資料庫存儲 seata 所用數據的,我們為了方便利用本地 file 的方式存儲數據,所以沒有再做資料庫的配置。如需修改可以修改配置文件 file.conf
下面是 file.conf 的默認配置:
store { ## store mode: file、db、redis mode = "file" ## file store property file { ## store location dir dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions maxBranchSessionSize = 16384 # globe session size , if exceeded throws exceptions maxGlobalSessionSize = 512 # file buffer size , if exceeded allocate new buffer fileWriteBufferCacheSize = 16384 # when recover batch read size sessionReloadReadSize = 100 # async, sync flushDiskMode = async } ## database store property db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "mysql" password = "mysql" minConn = 5 maxConn = 100 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } ## redis store property redis { host = "127.0.0.1" port = "6379" password = "" database = "0" minConn = 1 maxConn = 10 maxTotal = 100 queryLimit = 100 }}
啟動 nacos、apollo、seata-server
當顯示以下信息時,代表seata-server啟動了。
這時我們查看 nacos ,也註冊上了
apollo中我們添加一個名為 service.vgroup-mapping.demo-service-seata的key ,value為 default,至於這個的作用,我們後面再說。
我們的 demo 中包含三個服務
服務間調用使用的是Spring Cloud OpenFeign,除了 SpringBoot 和Spring Cloud 等基礎 bom 要依賴外,還需要加入 seata 的依賴,我的pom,大致如下:
<properties> <spring-boot-dependencies.version>2.3.2.RELEASE</spring-boot-dependencies.version> <spring-cloud-dependencies.version>Hoxton.SR8</spring-cloud-dependencies.version> <spring-cloud-alibaba-dependencies.version>2.2.3.RELEASE</spring-cloud-alibaba-dependencies.version></properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${spring-boot-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 實現對 Spring MVC 的自動化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 引入 Spring Cloud Alibaba Seata 相關依賴,使用 Seata 實現分布式事務,並實現對其的自動配置 --> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <!-- 引入 Spring Cloud Alibaba Nacos Discovery 相關依賴,將 Nacos 作為註冊中心,並實現對其的自動配置 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 引入 Spring Cloud OpenFeign 相關依賴,使用 OpenFeign 提供聲明式調用,並實現對其的自動配置 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
至於項目中所用ORM框架,資料庫連接池等就因人而異了,我用的是mybatis-plus和hikari,資料庫用的是 mysql5.7。
針對上面的三個服務分別創建三個資料庫,order、user、storage,並在每個庫中分別創建一個業務表 t_order、t_user、t_storage 這裡就不貼建庫表的腳本了,大家可以按照自己的設計自己建,需要注意的是每個庫都需要再創建一個 undo_log 表,這是為seata做分布式事務回滾所用。
CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
每個服務中 application.yml 中對應 seata 的配置如下
spring: profiles: active: dev cloud: nacos: discovery: namespace: public password: nacos server-addr: IP:PORT networkInterface: eth1 username: nacos# Seata 配置項,對應 SeataProperties 類seata: application-id: ${spring.application.name} # Seata 應用編號,默認為 ${spring.application.name} tx-service-group: demo-service-seata # Seata 事務組編號,用於 TC 集群名 # Seata 服務配置項,對應 ServiceProperties 類 service: # 虛擬組和分組的映射 vgroup-mapping: demo-service-seata: default # Seata 註冊中心配置項,對應 RegistryProperties 類 registry: type: nacos # 註冊中心類型,默認為 file nacos: cluster: default # 使用的 Seata 分組 namespace: # Nacos 命名空間 serverAddr: 你的IP:埠 # Nacos 服務地址
這裡有幾點需要注意:
至此我們的環境搭建和準備工作就結束了。
我們設計這樣一個同步的業務流程,創建訂單前先扣減庫存,再扣減帳戶餘額,然後再創建訂單,demo設計上參考了 芋道源碼。大致流程如下圖:
通過入口進入orderServicer後,進行上面的三步流程,分別調用兩個微服務,再調自己的訂單服務,這裡注意兩點:
接下來是扣減庫存微服務部分,簡單做了下扣減,小於10拋出異常
然後是帳戶微服務部分
最後是訂單
代碼都比較簡單,有幾個點需要注意下
由於在資料庫本地事務隔離級別 讀已提交(Read Committed) 或以上的基礎上,Seata(AT 模式)的默認全局隔離級別是 讀未提交(Read Uncommitted) 。
所以這種隔離性會帶來問題(注意這裡說的是全局事務):
其實上面這部分,官方文檔也寫的很清楚,尤其對於隔離性的解析:
上圖有些地方理解起來要注意:
我的測試中事務的正常執行和回滾都沒有問題,如果你觀察各資料庫的 undo_log 表,可能會發現沒有數據,但實際情況是數據是插入後又很快清除了,所以你沒看到,如果你觀察主鍵的 auto_increment 可以看到一直在增長。由於我用了阿里雲的RDS,可以通過SQL洞察看到SQL的執行歷史,這裡看到sql確實執行過。
XID是全局事務ID,有時候我們需要獲得並進行一些操作,那麼可以這樣做
String xid = RootContext.getXID();RootContext.unbind();//解綁//中途做一些與事務無關的事。比如日誌服務等等 排除掉,然後RootContext.bind(xid);//再綁回來
@GlobalTransactional也有自己的隔離級別和rollback等,可根據業務情況自行設置
package io.seata.spring.annotation;import io.seata.tm.api.transaction.Propagation;import java.lang.annotation.ElementType;import java.lang.annotation.Inherited;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD, ElementType.TYPE})@Inheritedpublic @interface GlobalTransactional { int timeoutMills() default 60000; String name() default ""; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; Propagation propagation() default Propagation.REQUIRED;}
[1]
seata官方文檔:http://seata.io/zh-cn/docs/overview/what-is-seata.html
[2]
分布式事務 Seata 及其三種模式詳解:http://seata.io/zh-cn/blog/seata-at-tcc-saga.html
[3]
nacos官方文檔:https://nacos.io/zh-cn/
[4]
apollo的github地址:https://github.com/ctripcorp/apollo
[5]
解決nacos註冊內網地址問題:https://www.cnblogs.com/liboware/p/11973321.html
[6]
官網文檔:http://seata.io/zh-cn/docs/user/configurations.html
[7]
芋道源碼:http://www.iocoder.cn/Spring-Cloud-Alibaba/Seata/
[8]
官網參數配置:http://seata.io/zh-cn/docs/user/configurations.html