分佈式事務管理基礎(一)

章節前言

在使用spring-boot開發過程中,我們會存在多數據源的問題,已經不同服務器中對應的數據源訪問時候的一致性和事務問題,現在我們來討論第一個問題,在使用多數據源問題中,如何保證ACID的特性。

X/Open DTP模型與XA接口

1. X/Open DTP 模型

如果一個業務操作涉及對多個數據源進行操作,那麼使用原來單一的數據庫的事務(本地事務)就會不能滿足數據的一致性要求,爲此,該模型中定義了DTP模型來規範全局事務。下面介紹模型中的幾個組件,分別如下:

  1. AP(Application Program,應用程序):代表需要使用分佈式事務的應用服務。
  2. RM(Resource Manager,資源管理器):比如數據庫或者文件系統。
  3. TM(Transaction Manager,事務管理器):這個大家都比較熟悉,是事務的核心,主要是給事務分配唯一標識,提供提交,異常回滾等操作。
  4. CRMs(Communication Resource Manager,通信資源管理器):負責管理多個應用在TM domain(一組使用同一個Tm的實例集合)之內或者跨TM domain訪問之間的通信,該通信使用的是OSI TP。
  5. 通信協議:由CRMs通信資源管理器所支持的通信協議。

2. OSI TP與2PC(分佈式事務協議)

2PC協議具體如下:
在階段一:事務管理器TM請求所有的資源管理器RM預提交各自的事務分支,RM如果能夠執行提交,那麼它會記錄相關的事務日誌;如果RM不能提交事務,則返回失敗,同時回滾已經處理的操作,然後釋放事務分支的資源。
在階段二:事務管理器TM向所有的RM發送提交事務或者回滾事務分支的請求,如果第一階段資源管理器RM返回的都是成功,澤發送提交事務請求;只要有一個是返回失敗,就發送回滾事務請求。
採用兩階段提交協議,簡單,但是缺點也有:

  1. 兩個階段都是同步阻塞,事務能力弱。
  2. 事務管理器TM在過程中負責協調管理,如果自身發生故障,那麼RM就會阻塞狀態。
  3. 第三點,也是最重要一點,如果再第二階段,由於網絡抖動等原因,部分RM沒有收到請求,數據一致性將被破壞

2. XA接口與JTA(分佈式事務協議)

XA接口是 X/Open DTP 在2PC的基礎上給事務管理器TM與資源管理器RM之間的通信定義接口,說白了,他就是定義了TM和RM之間通信的協議,主要就是通信。那麼在Java中,實現該接口定義的通信的服務就是JTA(Java Transaction Api)

分佈式事務解決方案

分佈式事務TCC模式

  1. Tentative Operation:爲了在多個實體之間達成一致,要求一個實體必須接受另外一個實體的請求。什麼意思嗯?也就是說,在兩個服務中,必須能夠接受請求,這種請求包括,確認提交請求和失敗回滾請求。
  2. Confirmation:如果請求方認爲Tentative Operation沒有問題,那麼發送確認請求,最終確定這個操作。
  3. 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的有一下幾個開源框架:

  1. Axon framework:Java客戶端編寫;
  2. 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即可。
其他內容請點擊鏈接項目源碼進行測驗,到此已經實現了多數據源的訪問已經事務的處理能力。
項目實戰源碼下載

結句

很多開發者並不清楚上述各種協議和模式的區別,那麼我們來做一個總結:

  1. 首先開局中的X/Open DTP模型與XA接口是定義了分佈式事務的一種模型,我們提到的JTA是在J2ee對XA接口的實現,而JTA是支持2pc協議的。
  2. 另外在數據庫集羣中,集羣數據庫事務的ACID難以滿足,所以需要使用新的理論CAP 、base理論來滿足集羣數據庫,例如多庫之間的事務問題,和多個服務之間的事務問題。
  3. 2pc機制需要RM提供底層支持(一般是兼容XA),而TCC機制則不需要。
  4. 我們使用的atomikos框架是對Jta的一種實現框架,本身支持了2pc協議。
  5. 現在也有很多支持了tcc協議的開源框架,可參考《重新定義spring cloud》第24章節,在560頁中查看內容。
  6. 相對於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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章