分佈式事務管理,多數據源問題
章節前言
在使用spring-boot開發過程中,我們會存在多數據源的問題,已經不同服務器中對應的數據源訪問時候的一致性和事務問題,現在我們來討論第一個問題,在使用多數據源問題中,如何保證ACID的特性。
X/Open DTP模型與XA接口
1. X/Open DTP 模型
如果一個業務操作涉及對多個數據源進行操作,那麼使用原來單一的數據庫的事務(本地事務)就會不能滿足數據的一致性要求,爲此,該模型中定義了DTP模型來規範全局事務。下面介紹模型中的幾個組件,分別如下:
- AP(Application Program,應用程序):代表需要使用分佈式事務的應用服務。
- RM(Resource Manager,資源管理器):比如數據庫或者文件系統。
- TM(Transaction Manager,事務管理器):這個大家都比較熟悉,是事務的核心,主要是給事務分配唯一標識,提供提交,異常回滾等操作。
- CRMs(Communication Resource Manager,通信資源管理器):負責管理多個應用在TM domain(一組使用同一個Tm的實例集合)之內或者跨TM domain訪問之間的通信,該通信使用的是OSI TP。
- 通信協議:由CRMs通信資源管理器所支持的通信協議。
2. OSI TP與2PC(分佈式事務協議)
2PC協議具體如下:
在階段一:事務管理器TM請求所有的資源管理器RM預提交各自的事務分支,RM如果能夠執行提交,那麼它會記錄相關的事務日誌;如果RM不能提交事務,則返回失敗,同時回滾已經處理的操作,然後釋放事務分支的資源。
在階段二:事務管理器TM向所有的RM發送提交事務或者回滾事務分支的請求,如果第一階段資源管理器RM返回的都是成功,澤發送提交事務請求;只要有一個是返回失敗,就發送回滾事務請求。
採用兩階段提交協議,簡單,但是缺點也有:
- 兩個階段都是同步阻塞,事務能力弱。
- 事務管理器TM在過程中負責協調管理,如果自身發生故障,那麼RM就會阻塞狀態。
- 第三點,也是最重要一點,如果再第二階段,由於網絡抖動等原因,部分RM沒有收到請求,數據一致性將被破壞
2. XA接口與JTA(分佈式事務協議)
XA接口是 X/Open DTP 在2PC的基礎上給事務管理器TM與資源管理器RM之間的通信定義接口,說白了,他就是定義了TM和RM之間通信的協議,主要就是通信。那麼在Java中,實現該接口定義的通信的服務就是JTA(Java Transaction Api)
分佈式事務解決方案
分佈式事務TCC模式
- Tentative Operation:爲了在多個實體之間達成一致,要求一個實體必須接受另外一個實體的請求。什麼意思嗯?也就是說,在兩個服務中,必須能夠接受請求,這種請求包括,確認提交請求和失敗回滾請求。
- Confirmation:如果請求方認爲Tentative Operation沒有問題,那麼發送確認請求,最終確定這個操作。
- Cancellation:同理,失敗發送取消請求。
TCC模式與2PC模式
與2PC模式不同的是,tcc將2pcprepare
操作(階段一)從事務管理中抽離,規範爲try操作,並且變爲Reserved狀態,同理也將撤銷prepare
的操作規範爲cancel操作。
TCC模式與ACID
tcc模式並不滿足Isolation的特性,也即是tcc是採用預留資源的方式來做try以及cancel操作的,那麼在到達final狀態錢,其他事務會讀取到髒數據。
其次,因爲tcc要求confirm、cancle等操作必須是冪等的,所以tcc的實現是最終一致性的事務。
分佈式事務SAGA模式
簡單介紹一下,該模式中的實現爲LLT
,它指的是持有數據庫資源相對較長的長活事務,它將一個綜合事務拆分爲多個子事務,每個子事務擁有自身的事務能力一致性,要麼可以成功提交,要麼通過補償事務來進行恢復操作,從而達到最終一致性,那麼這個LLT
就被稱爲SAGA。
SaGa與2Pc
與2Pc相比,SaGa與Tcc通過希望ACID的部分特性來提升分佈式事務的可用性,比如犧牲了I這個隔離來提高性能,另外SaGa與Tcc可以理解爲服務層面的事務模式,將分佈式事務控制由數據庫提升到服務層。
SaGa與Tcc
兩者最明顯的區別就是Tcc採用的是預留資源的方式,狀態中有Reserved
的狀態,但是Saga澤沒有,事務直接提交,然後採用補償事務的方式來撤回。與2Pc相比,2Pc採用事務的階段提交和回滾操作,整個都在一個事務中,而SaGa卻是將大事務拆分爲一個個的本地事務。
SaGa與ACID
前面我們說過,該模式不滿足Isolation的特性,因爲將LLT
這個大事務分隔成爲一個個小事務,所以,存在髒讀的數據可能性。
SaGa開源框架
支持SAGA的有一下幾個開源框架:
- Axon framework:Java客戶端編寫;
- Eventuate.io:
實戰解決多數據源事務問題
使用JTA處理分佈式事務
Spring Boot通過Atomkos或Bitronix的內嵌事務管理器支持跨多個XA資源的分佈式JTA事務,當部署到恰當的J2EE應用服務器時也會支持JTA事務。
當發現JTA環境時,Spring Boot將使用Spring的 JtaTransactionManager 來管理事務。自動配置的JMS,DataSource和JPA beans將被升級以支持XA事務。可以使用標準的Spring idioms,比如 @Transactional ,來參與到一個分佈式事務中。如果處於JTA環境,但仍想使用本地事務,你可以將 spring.jta.enabled 屬性設置爲 false 來禁用JTA自動配置功能。
使用Atomikos事務管理器
Atomikos是一個非常流行的開源事務管理器,並且可以嵌入到Spring Boot應用中。可以使用 spring-boot-starter-jta-atomikos Starter去獲取正確的Atomikos庫。Spring Boot會自動配置Atomikos,並將合適的 depends-on 應用到Spring Beans上,確保它們以正確的順序啓動和關閉。
默認情況下,Atomikos事務日誌將被記錄在應用home目錄(應用jar文件放置的目錄)下的 transaction-logs 文件夾中。可以在 application.properties 文件中通過設置 spring.jta.log-dir 屬性來定義該目錄,以 spring.jta.atomikos.properties 開頭的屬性能用來定義Atomikos的 UserTransactionServiceIml 實現,具體參考AtomikosProperties javadoc。
注 爲了確保多個事務管理器能夠安全地和相應的資源管理器配合,每個Atomikos實例必須設置一個唯一的ID。默認情況下,該ID是Atomikos實例運行的機器上的IP地址。爲了確保生產環境中該ID的唯一性,需要爲應用的每個實例設置不同的 spring.jta.transaction-manager-id 屬性值。
使用Bitronix事務管理器
Bitronix是一個流行的開源JTA事務管理器實現,可以使用 ·spring-bootstarter-jta-bitronix· starter爲項目添加合適的Birtronix依賴。和Atomikos類似,Spring Boot將自動配置Bitronix,並對beans進行後處理(post-process)以確保它們以正確的順序啓動和關閉。
默認情況下,Bitronix事務日誌( part1.btm 和 part2.btm )將被記錄到應用home目錄下的 transaction-logs 文件夾中,可以通過設置 spring.jta.log-dir 屬性來自定義該目錄。以 spring.jta.bitronix.properties 開頭的屬性將被綁定到 bitronix.tm.Configuration bean,可以通過這完成進一步的自定義,具體參考Bitronix文檔。
注 爲了確保多個事務管理器能夠安全地和相應的資源管理器配合,每個Bitronix實例必須設置一個唯一的ID。默認情況下,該ID是Bitronix實例運行的機器上的IP地址。爲了確保生產環境中該ID的唯一性,需要爲應用的每個實例設置不同的 spring.jta.transaction-manager-id 屬性值。
使用Narayana事務管理器
Narayana是一個流行的開源JTA事務管理器實現,目前只有JBoss支持。可以使用 spring-boot-starter-jta-narayana starter添加合適的Narayana依賴,像Atomikos和Bitronix那樣,Spring Boot將自動配置Narayana,並對beans後處理(post-process)以確保正確啓動和關閉。
Narayana事務日誌默認記錄到應用home目錄(放置應用jar的目錄)的 transaction-logs 目錄下,可以通過設置 application.properties 中的 spring.jta.log-dir 屬性自定義該目錄。以 spring.jta.narayana.properties 開頭的屬性可用於自定義Narayana配置,具體參考NarayanaProperties。
注 爲了確保多事務管理器能夠安全配合相應資源管理器,每個Narayana實例必須配置唯一的ID,默認ID設爲 1 。爲確保生產環境中ID唯一性,可以爲應用的每個實例配置不同的 spring.jta.transaction-manager-id 屬性值。
使用J2EE管理的事務管理器
如果將Spring Boot應用打包爲一個 war 或 ear 文件,並將它部署到一個J2EE的應用服務器中,那就能使用應用服務器內建的事務管理器。Spring Boot將嘗試通過查找常見的JNDI路徑( java:comp/UserTransaction ,java:comp/TransactionManager 等)來自動配置一個事務管理器。如果使用應用服務器提供的事務服務,通常需要確保所有的資源都被應用服務器管理,並通過JNDI暴露出去。Spring Boot通過查找JNDI路徑 java:/JmsXA 或 java:/XAConnectionFactory 獲取一個 ConnectionFactory 來自動配置JMS,並且可以使用 spring.datasource.jndi-name 屬性配置 DataSource 。
使用spring-boot進行編碼
創建數據庫表
DROP DATABASE IF EXISTS `jta-income`;
CREATE DATABASE `jta-income`;
USE `jta-income`;
DROP TABLE IF EXISTS `income`;
CREATE TABLE `income` (
`id` INT(20) NOT NULL AUTO_INCREMENT,
`userId` INT(20) NOT NULL,
`amount` FLOAT(8,2) NOT NULL,
`operateDate` DATETIME NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
DROP DATABASE IF EXISTS `jta-user`;
CREATE DATABASE `jta-user`;
USE `jta-user`;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` INT(20) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
mvn項目依賴環境
<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>com.freud.test</groupId>
<artifactId>spring-boot-23</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-23</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
boot啓動配置
spring:
application:
name: test-23
jpa:
show-sql: true
jta:
enabled: true
atomikos:
datasource:
jta-user:
xa-properties.url: jdbc:mysql://localhost:3306/jta-user
xa-properties.user: root
xa-properties.password: root
xa-data-source-class-name: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
unique-resource-name: jta-user
max-pool-size: 25
min-pool-size: 3
max-lifetime: 20000
borrow-connection-timeout: 10000
jta-income:
xa-properties.url: jdbc:mysql://localhost:3306/jta-income
xa-properties.user: root
xa-properties.password: root
xa-data-source-class-name: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
unique-resource-name: jta-income
max-pool-size: 25
min-pool-size: 3
max-lifetime: 20000
borrow-connection-timeout: 10000
server:
port: 9090
核心config配置Java
package com.freud.test.springboot.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.atomikos.jdbc.AtomikosDataSourceBean;
@Configuration
@EnableConfigurationProperties
@EnableAutoConfiguration
@MapperScan(basePackages = "com.freud.test.springboot.mapper.income", sqlSessionTemplateRef = "jtaIncomeSqlSessionTemplate")
public class DataSourceJTAIncomeConfig {
@Bean
@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.jta-income")
public DataSource dataSourceJTAIncome() {
return new AtomikosDataSourceBean();
}
@Bean
public SqlSessionFactory jtaIncomeSqlSessionFactory(@Qualifier("dataSourceJTAIncome") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
bean.setTypeAliasesPackage("com.freud.test.springboot.mapper.income");
return bean.getObject();
}
@Bean
public SqlSessionTemplate jtaIncomeSqlSessionTemplate(
@Qualifier("jtaIncomeSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
簡單源碼分析
可以看到整個框架編碼中,並沒有定義TransactionManager
的實現類,那麼它是如何實現使用@Transactional
註解來實現事務控制的呢?答案在AbstractJtaUserTransactionService
這個源碼文件中。
public abstract class AbstractJtaUserTransactionService extends
AbstractUserTransactionService
{
public AbstractJtaUserTransactionService()
{
super();
}
public void init ( TSInitInfo info ) throws SysException
{
super.init ( info );
String autoRegisterProperty = getTrimmedProperty (
AbstractUserTransactionServiceFactory.AUTOMATIC_RESOURCE_REGISTRATION_PROPERTY_NAME, info
.getProperties () );
boolean autoRegister = "true".equals ( autoRegisterProperty );
if ( Configuration.getResources ().hasMoreElements () && !autoRegister ) {
AcceptAllXATransactionalResource defaultRes = new AcceptAllXATransactionalResource (
"com.atomikos.icatch.DefaultResource" );
Configuration.addResource ( defaultRes );
}
}
public void shutdown ( boolean force ) throws IllegalStateException
{
super.shutdown(force);
TransactionManagerImp.installTransactionManager ( null, false );
UserTransactionServerImp.getSingleton ().shutdown ();
}
/**
* @see UserTransactionService
*/
public TransactionManager getTransactionManager ()
{
return com.atomikos.icatch.jta.TransactionManagerImp
.getTransactionManager ();
}
}
通過查看源碼,我們知道,該框架自動幫我們實現了一個com.atomikos.icatch.jta.TransactionManagerImp
的事務管理器,所以,在項目中我們可以直接使用註解@Transactional
即可。
其他內容請點擊鏈接項目源碼進行測驗,到此已經實現了多數據源的訪問已經事務的處理能力。
項目實戰源碼下載
結句
很多開發者並不清楚上述各種協議和模式的區別,那麼我們來做一個總結:
- 首先開局中的
X/Open DTP模型與XA接口
是定義了分佈式事務的一種模型,我們提到的JTA是在J2ee對XA接口的實現,而JTA是支持2pc協議的。 - 另外在數據庫集羣中,集羣數據庫事務的ACID難以滿足,所以需要使用新的理論CAP 、base理論來滿足集羣數據庫,例如多庫之間的事務問題,和多個服務之間的事務問題。
- 2pc機制需要RM提供底層支持(一般是兼容XA),而TCC機制則不需要。
- 我們使用的atomikos框架是對Jta的一種實現框架,本身支持了2pc協議。
- 現在也有很多支持了tcc協議的開源框架,可參考《重新定義spring cloud》第24章節,在560頁中查看內容。
- 相對於tcc來說,JTA是一種比較笨重的接口,但tcc的實現難度較大
參考資料
《重新定義spring cloud》第24章節,在560頁,主講分佈式事務。
Spring Boot Reference Guide : http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/
Spring-boot下的mybatis多數據源JTA配置 : http://blog.csdn.net/pichunhan/article/details/70846695
JTA 深度歷險 - 原理與實現 : https://www.ibm.com/developerworks/cn/java/j-lo-jta/