分布式事务管理基础

Posted 高国藩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式事务管理基础相关的知识,希望对你有一定的参考价值。

分布式事务管理,多数据源问题

章节前言

在使用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/

以上是关于分布式事务管理基础的主要内容,如果未能解决你的问题,请参考以下文章

安迈云深耕分布式存储与计算,迎接十四五开局年

营在开局,提升豹发力 - vivo活动插件管理平台

开局一张图,学一学项目管理神器Maven

开局一张图,学一学项目管理神器Maven

开局一张图,学一学项目管理神器Maven!

H3C 开局设置