記一次 Atomikos 分布式事務的使用


由於項目上的需要,我要同時往orcale數據庫與sqlserver數據中插入數據,需要在一個事務之內完成這兩個庫的提交。參考了一下網上的各種JTA(Java Transaction API)實現之后,選擇了Atomikos的實現。

因為當時使用的時候繞的彎路大了點,所以寫篇文章記錄下基本的實現過程,方便日后查看。如果是第一次使用,強烈建議去Atomikos查看官方例子與指導,寫的很詳細。

前提

----XA是啥?
XA是由X/Open組織提出的分布式事務的架構(或者叫協議)。XA架構主要定義了(全局)事務管理器(Transaction Manager)和(局部)資源管理器(Resource Manager)之間的接口。XA接口是雙向的系統接口,在事務管理器(Transaction Manager)以及一個或多個資源管理器(Resource Manager)之間形成通信橋梁。也就是說,在基於XA的一個事務中,我們可以針對多個資源進行事務管理,例如一個系統訪問多個數據庫,或即訪問數據庫、又訪問像消息中間件這樣的資源。這樣我們就能夠實現在多個數據庫和消息中間件直接實現全部提交、或全部取消的事務。XA規范不是java的規范,而是一種通用的規范,
目前各種數據庫、以及很多消息中間件都支持XA規范。
JTA是滿足XA規范的、用於Java開發的規范。所以,當我們說,使用JTA實現分布式事務的時候,其實就是說,使用JTA規范,實現系統內多個數據庫、消息中間件等資源的事務。

JTA(Java Transaction API),是J2EE的編程接口規范,它是XA協議的JAVA實現。它主要定義了:

  • 一個事務管理器的接口javax.transaction.TransactionManager,定義了有關事務的開始、提交、撤回等>操作。
  • 一個滿足XA規范的資源定義接口javax.transaction.xa.XAResource,一種資源如果要支持JTA事務,就需要讓它的資源實現該XAResource接口,並實現該接口定義的兩階段提交相關的接口。
    如果我們有一個應用,它使用JTA接口實現事務,應用在運行的時候,就需要一個實現JTA的容器,一般情況下,這是一個J2EE容器,像JBoss,Websphere等應用服務器。但是,也有一些獨立的框架實現了JTA,例如Atomikos, bitronix都提供了jar包方式的JTA實現框架。這樣我們就能夠在Tomcat或者Jetty之類的服務器上運行使用JTA實現事務的應用系統。
    在上面的本地事務和外部事務的區別中說到,JTA事務是外部事務,可以用來實現對多個資源的事務性。它正是通過每個資源實現的XAResource來進行兩階段提交的控制。感興趣的同學可以看看這個接口的方法,除了commit, rollback等方法以外,還有end(), forget(), isSameRM(), prepare()等等。光從這些接口就能夠想象JTA在實現兩階段事務的復雜性。
                                                            -------------- REST微服務的分布式事務實現-分布式系統、事務以及JTA介紹

如果還想了解更多的關於分布式事務的實現方式的可以看一個這個,里面寫了7種思路。Spring的分布式事務實現-使用和不使用XA(翻譯)

環境: spring-boot 2.x + maven + atomikos + orcale + sqlserver + mybatis

maven:pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>con.demo</groupId>
    <artifactId>demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.13.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>com.github.noraui</groupId>
            <artifactId>ojdbc7</artifactId>
            <version>12.1.0.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>6.4.0.jre8</version>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

application.properties里配置3個數據源

server.port=8085
logging.file=logs\\msgexchange.log
logging.level.root=INFO
logging.level.org.springframework.web=INFO

logging.level.cn.gov.customs.msgexchange=DEBUG
mybatis.check-config-location=true
mybatis.config-locations=classpath:mybatis-config.xml

#primary datasource
spring.datasource.driverClassName = oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@ip:port:dbname
spring.datasource.username=username
spring.datasource.password=password
spring.datasource.initialSize=5 
spring.datasource.minIdle=10  
spring.datasource.maxActive=30  
spring.datasource.maxWait=60000  
spring.datasource.timeBetweenEvictionRunsMillis=60000  
spring.datasource.minEvictableIdleTimeMillis=300000  
spring.datasource.validationQuery=SELECT 1 FROM DUAL  
spring.datasource.testWhileIdle=true  
spring.datasource.testOnBorrow=false  
spring.datasource.testOnReturn=false  
spring.datasource.poolPreparedStatements=true  
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20  

#XADataSource orcale 數據源
spring.datasource.msg.xaDataSourceClassName=oracle.jdbc.xa.client.OracleXADataSource
spring.datasource.msg.url=jdbc:oracle:thin:@ip:port:dbname
spring.datasource.msg.user=user
spring.datasource.msg.password=password
spring.datasource.msg.uniqueResourceName=OracleXADataSource
spring.datasource.msg.initialSize=5 
spring.datasource.msg.minIdle=10  
spring.datasource.msg.maxActive=30  
spring.datasource.msg.maxWait=60000  
spring.datasource.msg.timeBetweenEvictionRunsMillis=60000  
spring.datasource.msg.minEvictableIdleTimeMillis=300000  
spring.datasource.msg.validationQuery=SELECT 1 FROM DUAL  
spring.datasource.msg.testWhileIdle=true  
spring.datasource.msg.testOnBorrow=false  
spring.datasource.msg.testOnReturn=false  
spring.datasource.msg.poolPreparedStatements=true  
spring.datasource.msg.maxPoolPreparedStatementPerConnectionSize=20

#sqlserver 數據源
spring.datasource.dps.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver 
spring.datasource.dps.url=jdbc:sqlserver://localhost:port;database=dbname
spring.datasource.dps.username=username
spring.datasource.dps.password=password
spring.datasource.dps.initialSize=5 
spring.datasource.dps.minIdle=10  
spring.datasource.dps.maxActive=30  
spring.datasource.dps.maxWait=60000  
spring.datasource.dps.timeBetweenEvictionRunsMillis=60000  
spring.datasource.dps.minEvictableIdleTimeMillis=300000  
spring.datasource.dps.validationQuery=SELECT 1 FROM DUAL  
spring.datasource.dps.testWhileIdle=true  
spring.datasource.dps.testOnBorrow=false  
spring.datasource.dps.testOnReturn=false  
spring.datasource.dps.poolPreparedStatements=true  
spring.datasource.dps.maxPoolPreparedStatementPerConnectionSize=20

這里很尷尬,配置了這么多,由於使用的並不是springboot-start里的自動裝配。我又沒有手動裝配,所有其實都沒有用上。。其中主數據源是平時單庫操作用的,他不需要使用分布式事務。
我使用了mybatis,所以現在來寫一下配置文件。

主數據源
@Configuration
@MapperScan(basePackages = {"com.demo.orcale.dao"}, sqlSessionTemplateRef = "SqlSessionTemplate")
public class MybatisConfig {

	@Bean(name = "MybatisDS")
	@ConfigurationProperties(prefix = "spring.datasource")
	@Primary
	public DataSource DataSource() {
		return DataSourceBuilder.create().build();
	}

	@Bean(name = "SqlSessionFactory")
	@Primary
	public SqlSessionFactory SqlSessionFactory(@Qualifier("MybatisDS") DataSource dataSource) throws Exception {
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
		bean.setDataSource(dataSource);
		return bean.getObject();
	}

	@Primary
	@Bean(name = "TransactionManager")
	public DataSourceTransactionManager TransactionManager(@Qualifier("MybatisDS") DataSource dataSource) {
		return new DataSourceTransactionManager(dataSource);
	}

	@Primary
	@Bean(name = "SqlSessionTemplate")
	public SqlSessionTemplate SqlSessionTemplate(@Qualifier("SqlSessionFactory") SqlSessionFactory sqlSessionFactory)
			throws Exception {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

這里是配置了一下單庫操作的主數據源,這個和分布式事務沒有一點關系,為了平時數據庫的增刪改查使用。

sqlserver數據源
@Configuration
@MapperScan(basePackages = ""com.demo.xa.sqlserver.dao", sqlSessionTemplateRef = "DpsSqlSessionTemplate")
@ConfigurationProperties(prefix = "spring.datasource.dps")
public class DPSMybatisConfig {
    private String url;
    private String username;
    private String password;

    @Bean(name = "DpsMybatisDS")
    public DataSource DataSource() {
        SQLServerXADataSource xaDataSource = new SQLServerXADataSource();
        xaDataSource.setURL(url);
        xaDataSource.setUser(username);
        xaDataSource.setPassword(password);

        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSource(xaDataSource);
        return ds;
    }

    @Bean(name = "DpsSqlSessionFactory")
    public SqlSessionFactory SqlSessionFactory(@Qualifier("DpsMybatisDS") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean.getObject();
    }

    @Bean(name = "DpsSqlSessionTemplate")
    public SqlSessionTemplate SqlSessionTemplate(@Qualifier("DpsSqlSessionFactory") SqlSessionFactory sqlSessionFactory)
            throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
orcale數據源
@Configuration
@MapperScan(basePackages = {"com.demo.xa.orcale.dao"}, sqlSessionTemplateRef = "SqlSessionTemplate")
public class MybatisConfig {

        //從配置文件里注入吧
	@Bean(name = "MybatisDS")
	public DataSource DataSource() {
		Properties properties = new Properties();
		properties.setProperty("URL", "jdbc:oracle:thin:@172.18.11.62:1521:test");
		properties.setProperty("user", "user");
		properties.setProperty("password", "password");

		AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
		ds.setXaProperties(properties);
		ds.setUniqueResourceName("OracleXADataSource");
		ds.setXaDataSourceClassName("oracle.jdbc.xa.client.OracleXADataSource");
		return ds;
	}

	@Bean(name = "SqlSessionFactory")
	public SqlSessionFactory SqlSessionFactory(@Qualifier("MybatisDS") DataSource dataSource) throws Exception {
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
		bean.setDataSource(dataSource);
		return bean.getObject();
	}

	@Bean(name = "SqlSessionTemplate")
	public SqlSessionTemplate SqlSessionTemplate(@Qualifier("SqlSessionFactory") SqlSessionFactory sqlSessionFactory)
			throws Exception {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

使用XA的時候只需要把數據源配置成對應數據庫的XA數據源就行了,其他的配置不用改

mysql就把數據源換成MysqlXADataSource完全沒區別。

定義個獨立的事務管理器,spring會為你管理的。(很遺憾沒有研究一下spring是如何接手事務管理的)

    @Bean(name = "xatx")
    @Primary
    public JtaTransactionManager regTransactionManager () {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        UserTransaction userTransaction = new UserTransactionImp();
        return new JtaTransactionManager(userTransaction, userTransactionManager);
    }

至此配置就配置完成了。然后看一下如何使用

    @Transactional(transactionManager = "xatx")
    @GetMapping("/test")
    public String test(){
        //sqlservice
        User user = new User();
        user.setNo(UUID.randomUUID().toString().substring(10));
        user.setName("bob");
        userMapper.insert(user);

        if (true){
            throw new RuntimeException("this is my RunTime error");
        }

        //orcale
        Car car = new Car();
        car.setNo(UUID.randomUUID().toString().substring(10));
        car.setName("sherry");
        carMapper.insert(car);

        return "操作成功";
    }

在事務中拋出任意的RuntimeException的時候都會觸發事務的回滾,要不兩個數據庫都提交,否則就都不提交。
其實搭建分布式事務的難點並不是在配置mybatis數據源這里。而是在配置數據庫上,orcale的還好說,只要給一下orcale賬戶的權限就完事,但是sqlserver數據庫的配置簡直了,老老實實跟着官網步驟走吧。
atomikos官網步驟: Configuring Microsoft SQL Server for XA
微軟官網:Understanding XA Transactions
上面這兩個才是最坑的。這里舉兩個我碰到的坑

問題1:沒有存儲過程xxx

這個問題基本看看是orcale還是sqlserver,如果是orcale的就是沒有數據庫的表權限。找到官網的4個授權語句加上,問題可以解決

grant select on sys.dba_pending_transactions to <user name>;
grant select on sys.pending_trans$ to <user name>;
grant select on sys.dba_2pc_pending to <user name>;
grant execute on sys.dbms_system to <user name>;

如果是sqlserver數據源報出的問題,那么找到驅動中的xa_install.sql文件,運行一下,他會建立很多觸發器與表,還會建立一個數據庫角色,你需要把sqlserver的登錄用戶賦予這個新建的角色。

問題2:函數 RECOVER: 失敗。狀態為: -3。錯誤:“*** SQLJDBC_XA DTC_ERROR Context: xa_recover, state=1
javax.transaction.xa.XAException: 函數 RECOVER: 失敗。狀態為: -3。錯誤:“*** SQLJDBC_XA DTC_ERROR Context: xa_recover, state=1, StatusCode:-3 (0xFFFFFFFD) ***”
	at com.microsoft.sqlserver.jdbc.SQLServerXAResource.DTC_XA_Interface(SQLServerXAResource.java:550) ~[sqljdbc4-4.0.jar:na]
	at com.microsoft.sqlserver.jdbc.SQLServerXAResource.recover(SQLServerXAResource.java:728) ~[sqljdbc4-4.0.jar:na]
	at com.atomikos.datasource.xa.XATransactionalResource.recoverXidsFromXAResource(XATransactionalResource.java:554) [transactions-jta-3.9.3.jar:na]
	at com.atomikos.datasource.xa.XATransactionalResource.recover(XATransactionalResource.java:512) [transactions-jta-3.9.3.jar:na]
	at com.atomikos.datasource.xa.XATransactionalResource.recoverXidsFromResourceIfNecessary(XATransactionalResource.java:615) 
............................

2018-08-08 09:11:33.221  WARN 2020 --- [           main] c.a.icatch.imp.TransactionServiceImp     : ERROR IN RECOVERY

com.atomikos.datasource.ResourceException: Error in recovery
	at com.atomikos.datasource.xa.XATransactionalResource.recoverXidsFromXAResource(XATransactionalResource.java:565) [transactions-jta-3.9.3.jar:na]
	at com.atomikos.datasource.xa.XATransactionalResource.recover(XATransactionalResource.java:512) [transactions-jta-3.9.3.jar:na]
	at com.atomikos.datasource.xa.XATransactionalResource.recoverXidsFromResourceIfNecessary(XATransactionalResource.java:615) [transactions-jta-3.9.3.jar:na]
	at com.atomikos.datasource.xa.XATransactionalResource.endRecovery(XATransactionalResource.java:583) [transactions-jta-3.9.3.jar:na]
	at com.atomikos.icatch.imp.TransactionServiceImp.recover(TransactionServiceImp.java:558) [transactions-3.9.3.jar:na]
	at com.atomikos.datasource.xa.XATransactionalResource.setRecoveryService(XATransactionalResource.java:435) [transactions-jta-3.9.3.jar:na]
	at com.atomikos.icatch.system.Configuration.installRecoveryService(Configuration.java:260) [transactions-3.9.3.jar:na]
	at com.atomikos.icatch.imp.TransactionServiceImp.prepareConfigurationForPresumedAbortIfNecessary(TransactionServiceImp.java:581) 

這個錯誤折騰了很久。原因基本就是程序使用的sqlserver連接驅動版本與數據庫dll的版本不一致,嘗試以下步驟:

1.檢查pom文件

mssql-jdbc版本,我這里是6.4.0.jre8。那么就去微軟官網下同版本驅動,把驅動里面的sqljdbc_xa.dll丟帶相應文件夾下,這個的詳細操作網上很多。

2.sqljdbc_auth.dll

把驅動中的sqljdbc_auth.dll丟到C:\Windows\System32下,然后重啟sqlserver服務。

3.都沒用..

是的,我試了一下都沒用,在這里卡了好幾個小時,但是我的直覺告訴我還是版本問題,所以我又去看了一眼pom文件。然后我發現了sqljdbc4這個包,那么mssql-jdbc包又是干嘛的,都是sqlserver的jdbc驅動嗎,是不是重復了?然后我把sqljdbc4包刪了,世界恢復了正常,程序不報異常了。這兩個應該都是sqlserver的jdbc驅動,我也沒找到區別在哪,但是官網頁面使用的是mssql-jdbc的驅動,保持一致就好了。

        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>sqljdbc4</artifactId>
            <version>4.0</version>
        </dependency>

以上就是我用XA遇到的一些問題,最坑的就是sqlserver數據庫這一塊的配置了。但是根本原因還是自己用之前沒有好好的讀一遍官網的使用說明文檔,白白的浪費了時間。教訓就是用之前有官網就去官網,官網有例子就改例子,有文檔就先讀文檔,別去百度里瞎搜。


結束語

分布式事務或者說,就分布式、微服務架構的系統如何保證數據的一致性這一點來考慮,當然這是一個很大的話題,完全應該獨立一篇文章出來討論。在MQ中,你可以選擇使用回執或者事務保持一致性。在SpringCloud你可以使用Hystrix的fallback或者TCC(Try-Confirm-Cancel)模式。又或者說存儲每一次的數據操作或者說儲存沒一個事件,那么當發生異常的時候我們能夠根據歷史事件重新生成數據的事件溯源(Event Sourcing)模式。等等這些都是用非事務的方式來確保數據一致。

附錄

微軟官網:Understanding XA Transactions
atomikos: Configuring TransactionsEssentials®

感謝博客的分析文章:
codin:codin


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM