1 解決問題
當在Spring Cloud搭建的分布式系統中,如果某個業務涉及到多個服務的事務,無法保證當某一個服務異常時,其他所有業務服務都進行事務的回滾,就會導致業務數據不一致的問題
2 解決方案
使用阿里巴巴開源的分布式事務框架Seata,目前支持的注冊中心有nacos、eureka、zk、consul、etcd3、sofa等。
2.1 優點
1、Seata基於SQL解析實現了事務回滾的自動補償,無需開發者自己實現,降低了框架對業務的侵入性
2、事務協調者獨立部署成一個微服務,同樣降低了對業務的侵入性
2.2 缺點
1、Seata的日志回滾表,有一個字段用來存儲事務修改數據前后的數據鏡像,為了存儲內容很大的鏡像選擇了longblob類型,所以回滾表的插入性能不是很好,即使數據鏡像很小
2.3 Seata支持的分布式事務模式
1、AT模式:在傳統的二階段提交協議上進行優化,普通的二階段提交,執行過程中所有節點時同步阻塞的,導致速度很慢,AT模式進行了優化:在第一階段,節點直接提交本地事務,並記錄undo log,然后提交結果到Seata全局的事務管理器;在第二階段,如果其他服務異常,全局事務管理器異步發送通過undo log記錄發送回滾請求到各節點,進行事務的補償回滾,完畢后刪除undo log記錄。此模式的前提是本地數據源必須是jdbc連接的支持本地事務的數據庫
2、TCC模式:此模式可以自定義准備、提交、回滾的策略,相對於AT模式,可以支持任何數據源,在第一個准備階段會將數據加鎖,能保證隔離性,需要自己去開發各個階段的處理邏輯,對業務的侵入性很大,適合不支持事務的數據庫,比如impala
3、Saga模式:本質是二階段提交的實現版本之一,在第一階段直接提交本地事務,釋放鎖,保證高性能,代價是不保證事務的隔離性,適合業務流程長的事務,第二階段出現異常執行補償邏輯
4、XA模式:XA模式需要數據源支持XA協議
3 搭建流程
3.1 技術選型
使用eureka+feign+mybatis,Seata使用AT模式
3.2 Seata服務端搭建
1、https://github.com/seata/seata/releases
下載最新發布版本的壓縮包,並解壓
2、配置config目錄下的registry.conf
修改三個地方
registry.type:指定注冊中心,這里指定為eureka
registry.eureka.serviceUrl:eureka注冊地址
registry.eureka.application:Seata注冊到eureka的服務名
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "eureka" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "" password = "" } eureka { serviceUrl = "http://127.0.0.1:8080/eureka/" application = "seata-server" weight = "1" } redis { serverAddr = "localhost:6379" db = 0 password = "" cluster = "default" timeout = 0 } zk { cluster = "default" serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" apolloAccesskeySecret = "" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
2、配置config目錄下的file.conf
service.vgroupMapping.{事務群組}:Seata注冊到eureka的服務名
(注意這里的事務群組,后面要與客戶端配置的事務群組一致)
Seata注冊到eureka的服務名.grouplist
store.mode:指定事務信息存儲方式,這里指定為數據庫存儲
db下面的配置指定一些數據源的配置,要注意如果使用mysql5和mysql8的driverClassName是不同的
數據庫需要導入三張表,建表語句:https://github.com/seata/seata/tree/develop/script/server/db
service { #transaction service group mapping vgroupMapping.my_test_tx_group = "seata-server" #only support when registry.type=file, please don't set multiple addresses seata-server.grouplist = "127.0.0.1:8091" #degrade, current not support enableDegrade = false #disable seata disableGlobalTransaction = false } ## transaction log store, only used in seata-server store { ## store mode: file、db、redis mode = "db" ## 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.cj.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=UTC" user = "root" password = "root" 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 } }
3、如果需要覆蓋Seata服務的eureka配置,可以手動增加eureka-client.properties文件進行配置
4、前往bin目錄,啟動Seata服務端,Linux下使用seata-server.sh啟動,Windows下使用seata-server.bat啟動
3.3客戶端服務搭建
客戶端搭建有兩種方式,一種是直接將服務器的配置文件放到客戶端項目中,另一種是使用Springboot starter的方式引入。starter的方式更便於配置的統一管理,但是目前有一些starter屬性無效,還是要加入一點文件配置,比如disableGlobalTransaction
3.3.1 引入配置文件方式
1、客戶端為標准的Springboot項目,請自行配置好注冊eureka,feign調用,mybatis等配置
2、引入Seata依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.0</version>
</dependency>
3、配置bootstrap.yml
這里指定事務群組,與上方的服務器配置保持一致
spring:
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
4、配置數據源
每個業務數據庫需要創建undo_log表,記錄修改前后的鏡像用於回滾,建表語句如下
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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
注意使用Seata的數據源代理DataSourceProxy
@Configuration public class DataSourceConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } @Primary @Bean("dataSource") public DataSourceProxy dataSource(DataSource druidDataSource){ return new DataSourceProxy(druidDataSource); } @Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{ SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:/mapper/*.xml")); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
5、將服務端的配置文件file.conf和registry.conf直接放到客戶端新項目的resource目錄下
6、啟動客戶端項目,查看日志是否register success

7、在業務的調用函數上加上@GlobalTransactional注解,即可實現分布式事務
3.3.2 Springboot starter方式
1、客戶端為標准的Springboot項目,請自行配置好注冊eureka,feign調用,mybatis等配置
2、引入Seata依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
3、配置bootstrap.yml
seata: enabled: true # 開啟seata registry: # 指定注冊中心 type: eureka eureka: service-url: ${eureka.client.service-url.defaultZone} application: seata-server weight: 1 service: vgroup-mapping: seata-server # seata服務端的服務名 tx-service-group: my_test_tx_group # 事務群組,與服務端配置一致
4、配置數據源
每個業務數據庫需要創建undo_log表,記錄修改前后的鏡像用於回滾,建表語句如下
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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
由於starter默認是開啟自動數據源代理的,所有不需要額外配置數據源代理
5、由於部分starter配置不生效,需要將是否禁止全局事務的屬性通過配置file.conf文件引入,將此文件放入resource目錄下即可
service { disableGlobalTransaction = false }
6、啟動客戶端項目,查看日志是否register success

7、在業務的調用函數上加上@GlobalTransactional注解,即可實現分布式事務
