springboot+mybatis+druid+atomikos 处理分布式事务

       前言:在上一篇文章《springboot+mybatis+druid 多数据源整合》中我们进行了多数据源的集成,根据不同的mapper文件可以操作不同的数据源,但是这样也就带来了一个问题,怎么保证数据的一致性?通常事务回滚机制是回滚指定数据源的数据,如果在service层调用不同的mapper操作不同的数据源,出现异常的情况下有一个数据源肯定无法回滚,这样就不能保证数据的一致性了!这个时候JTA就派上用场了。

       一:JTA:Java Transaction API,JTA允许应用程序执行分布式事务处理——在两个或多个网络计算机资源上访问并且更新数据,对JTA接口主要有三种实现:

       1.1、Atomikos事务管理器”: Atomikos是一个非常流行的开源事务管理器,并且可以嵌入到Spring Boot应用中。可以使用 spring-boot-starter-jta-atomikos Starter去获取正确的Atomikos库。Spring Boot会自动配置Atomikos,并将合适的 depends-on 应用到Spring Beans上,确保它们以正确的顺序启动和关闭。

       1.2、Bitronix事务管理器:Bitronix是一个流行的开源JTA事务管理器实现,可以使用 ·spring-bootstarter-jta-bitronix· starter为项目添加合适的Birtronix依赖。和Atomikos类似,Spring Boot将自动配置Bitronix,并对beans进行后处理(post-process)以确保它们以正确的顺序启动和关闭。

       1.3、Narayana事务管理器:Narayana是一个流行的开源JTA事务管理器实现,目前只有JBoss支持。可以使用 spring-boot-starter-jta-narayanastarter添加合适的Narayana依赖,像Atomikos和Bitronix那样,Spring Boot将自动配置Narayana,并对beans后处理(post-process)以确保正确启动和关闭。

       二:我们这里使用 Atomikos 来实现分布式事务的管理。

       2.1、首先添加 Atomikos maven 依赖:



	org.springframework.boot
	spring-boot-starter-jta-atomikos

      2.2、更改 application.yml 内容,需要注意的地方是将spring.datasource.type的值从com.alibaba.druid.pool.DruidDataSource更改为com.alibaba.druid.pool.xa.DruidXADataSource类:

server:
  port: 8088
  context-path: /yjbj

## 配置数据源相关信息
spring:
  datasource:
    type: com.alibaba.druid.pool.xa.DruidXADataSource
    druid:
      ## 连接池配置
      one:
        ## JDBC配置
        name: DBconfig1
        url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
        username: root
        password: 123456
        driverClassName: com.mysql.jdbc.Driver
        filters: stat
        maxActive: 20
        initialSize: 1
        maxWait: 60000
        minIdle: 1
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: select 'x'
        testWhileIdle: true
        testOnBorrow: true
        testOnReturn: true
        poolPreparedStatements: true
        maxOpenPreparedStatements: 20
      two:
        ## JDBC配置
        name: DBconfig2
        url: jdbc:mysql://127.0.0.1:3306/slave?useUnicode=true&characterEncoding=utf8&useSSL=false
        username: root
        password: 123456
        driverClassName: com.mysql.jdbc.Driver
        filters: stat
        maxActive: 20
        initialSize: 1
        maxWait: 60000
        minIdle: 1
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: select 'x'
        testWhileIdle: true
        testOnBorrow: true
        testOnReturn: true
        poolPreparedStatements: true
        maxOpenPreparedStatements: 20

  redis:
    host: 127.0.0.1  # redis服务所在的地址
    port: 6379
    password:   # redis的密码默认为空
    pool:
      max-active: 8  #连接池最大连接数(使用负值表示没有限制)
      max-idle: 8  #连接池最大空闲数
      min-idle: 1  #连接池最小空闲数
      max-wait: 60000  #获取连接的超时等待事件
    timeout: 30000  #连接redis服务器的最大等待时间
  druid: #druid监控页面用户名和密码
    name: admin
    pass: sailing123

## 该配置节点为独立的节点
mybatis:
  mapper-locations: classpath:mapperXML/*.xml  # 注意:一定要对应mapper映射xml文件的所在路径
  type-aliases-package: com.sailing.springbootmybatis.bean # 注意:对应实体类的路径
  configuration:
    map-underscore-to-camel-case: true
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

## 配置mybatis分页插件
pagehelper:
  helperDialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=conutSql

       2.3、修改初始化数据源的代码,这里我将创建数据源的代码提取到一个类DataSourceConfig中:

package com.sailing.springbootmybatis.config.datasource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;

import javax.sql.DataSource;
import java.util.Properties;

/**
 * @author baibing
 * @project: springboot-mybatis
 * @package: com.sailing.springbootmybatis.config
 * @Description: 数据源配置类
 * @date 2018/11/9 11:33
 */
@Configuration
public class DataSourceConfig {

    @Autowired
    private Environment env;

    @Value("${spring.datasource.type}")
    private String dataSourceType;

    /**
     * 配置主数据源,多数据源中必须要使用@Primary指定一个主数据源
     * 其次DataSource里用的是DruidXADataSource ,而后注册到AtomikosDataSourceBean并且返回
     * @return
     */
    @Primary
    @Bean(name = "datasourceOne")
    public DataSource datasourceOne() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        Properties prop = build(env, "spring.datasource.druid.one.");
        ds.setXaDataSourceClassName(dataSourceType);
        ds.setPoolSize(5);
        ds.setXaProperties(prop);
        return ds;
    }

    /**
     * 配置次数据源
     * 其次DataSource里用的是DruidXADataSource ,而后注册到AtomikosDataSourceBean并且返回
     * @return
     */
    @Bean(name = "dataSourceTwo")
    public DataSource datasourceTwo() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        Properties prop = build(env, "spring.datasource.druid.two.");
        ds.setXaDataSourceClassName(dataSourceType);
        ds.setPoolSize(5);
        ds.setXaProperties(prop);
        return ds;
    }

    private Properties build(Environment env, String prefix) {
        Properties prop = new Properties();
        prop.put("name", env.getProperty(prefix + "name"));
        prop.put("url", env.getProperty(prefix + "url"));
        prop.put("username", env.getProperty(prefix + "username"));
        prop.put("password", env.getProperty(prefix + "password"));
        prop.put("driverClassName", env.getProperty(prefix + "driverClassName", ""));
        prop.put("filters", env.getProperty(prefix + "filters"));
        prop.put("maxActive", env.getProperty(prefix + "maxActive", Integer.class));
        prop.put("initialSize", env.getProperty(prefix + "initialSize", Integer.class));
        prop.put("maxWait", env.getProperty(prefix + "maxWait", Integer.class));
        prop.put("minIdle", env.getProperty(prefix + "minIdle", Integer.class));
        prop.put("timeBetweenEvictionRunsMillis",
                env.getProperty(prefix + "timeBetweenEvictionRunsMillis", Integer.class));
        prop.put("minEvictableIdleTimeMillis", env.getProperty(prefix + "minEvictableIdleTimeMillis", Integer.class));
        prop.put("validationQuery", env.getProperty(prefix + "validationQuery"));
        prop.put("testWhileIdle", env.getProperty(prefix + "testWhileIdle", Boolean.class));
        prop.put("testOnBorrow", env.getProperty(prefix + "testOnBorrow", Boolean.class));
        prop.put("testOnReturn", env.getProperty(prefix + "testOnReturn", Boolean.class));
        prop.put("poolPreparedStatements", env.getProperty(prefix + "poolPreparedStatements", Boolean.class));
        prop.put("maxOpenPreparedStatements", env.getProperty(prefix + "maxOpenPreparedStatements", Integer.class));
        return prop;
    }
}

      2.4、分别创建两个数据源对应的 sqlSessionFactory 和 sqlSessionTemplate :

package com.sailing.springbootmybatis.config.datasource;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * @author baibing
 * @project: springboot-mybatis
 * @package: com.sailing.springbootmybatis.config
 * @Description: SqlSessionTemplateOne配置类
 * @date 2018/10/18 17:05
 */
@Configuration
//下面的sqlSessionTemplateRef 值需要和生成的SqlSessionTemplate bean name相同,如果没有指定name,那么就是方法名
@MapperScan(basePackages = {"com.sailing.springbootmybatis.mapper.one"}, sqlSessionTemplateRef = "sqlSessionTemplateOne")
public class SqlSessionTemplateOneConfig {

    @Value("${mybatis.mapper-locations}")
    private String mapper_location;

    @Value("${mybatis.type-aliases-package}")
    private String type_aliases_package;

    @Value("${mybatis.configuration.map-underscore-to-camel-case}")
    private boolean mapUnderscoreToCamelCase;

//    @Value("${mybatis.configuration.log-impl}")
    private String logImpl;

    //将MybatisConfig类中初始化的对象注入进来
    @Autowired
    private ConfigurationCustomizer customizer;

    private Logger logger = LoggerFactory.getLogger(SqlSessionTemplateOneConfig.class);

    /**
     * 自定义sqlSessionFactory配置(因为没有用到MybatisAutoConfiguration自动配置类,需要手动配置)
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactoryOne(@Qualifier("datasourceOne") DataSource dataSource) throws Exception {
        logger.info("mapper文件地址:" + mapper_location);
        //在基本的 MyBatis 中,session 工厂可以使用 SqlSessionFactoryBuilder 来创建。
        // 而在 MyBatis-spring 中,则使用SqlSessionFactoryBean 来替代:
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        //如果重写了 SqlSessionFactory 需要在初始化的时候手动将 mapper 地址 set到 factory 中,否则会报错:
        //org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapper_location));
        //下面这个setTypeAliasesPackage无效,是mybatis集成springBoot的一个bug,暂时未能解决
        bean.setTypeAliasesPackage(type_aliases_package);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        logger.info("mybatis配置驼峰转换为:" + mapUnderscoreToCamelCase);
        configuration.setMapUnderscoreToCamelCase(mapUnderscoreToCamelCase);
//        logger.info("mybatis配置logImpl为:" + logImpl);
//        configuration.setLogImpl((Class)Class.forName(logImpl));
        //因为没有用mybatis-springBoot自动装配,所以需要手动将configuration装配进去,要不然自定义的map key驼峰转换不起作用
        customizer.customize(configuration);
        bean.setConfiguration(configuration);
        return bean.getObject();
    }

    /**
     * SqlSessionTemplate 是 SqlSession接口的实现类,是spring-mybatis中的,实现了SqlSession线程安全
     *
     * @param sqlSessionFactory
     * @return
     */
    @Bean
    public SqlSessionTemplate sqlSessionTemplateOne(@Qualifier("sqlSessionFactoryOne") SqlSessionFactory sqlSessionFactory) {
        SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory);
        return template;
    }
}
package com.sailing.springbootmybatis.config.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.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * @author baibing
 * @project: springboot-mybatis
 * @package: com.sailing.springbootmybatis.config
 * @Description: SqlSessionTemplateTwo配置类
 * @date 2018/10/18 17:28
 */
@Configuration
@MapperScan(basePackages = {"com.sailing.springbootmybatis.mapper.two"}, sqlSessionTemplateRef = "sqlSessionTemplateTwo")
public class SqlSessionTemplateTwoConfig {

    @Value("${mybatis.mapper-locations}")
    private String mapper_location;

    @Bean
    public SqlSessionFactory sqlSessionFactoryTwo(@Qualifier("dataSourceTwo") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        //如果重写了 SqlSessionFactory 需要在初始化的时候手动将 mapper 地址 set到 factory 中,否则会报错:
        //org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapper_location));
        return bean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplateTwo(@Qualifier("sqlSessionFactoryTwo") SqlSessionFactory sqlSessionFactory) {
        SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory);
        return template;
    }
}

这里需要说一下,第一个类SqlSessionTemplateOneConfig中创建SqlSessionFactory的方法里有几行代码是设置mybatis返回为map的时候将key进行驼峰转换的作用,需要的同学可以参考我的另一个博客《spring boot+mybatis查询结果为map的时候将key转换为驼峰形式方法》,不想看的可以参照 SqlSessionTemplateTwoConfig 中创建sqlSessionFactory的内容,只需要将引用的数据源换成 @Qualifier("dataSourceOne") 即可。

       2.5、配置完以上还需要配置事务管理器,因为是由Atomikos统一管理事务,所以无论有几个数据源都只需配置一个事务管理器:

package com.sailing.springbootmybatis.config.datasource;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.UserTransaction;

/**
 * @author baibing
 * @project: springboot-mybatis
 * @package: com.sailing.springbootmybatis.config
 * @Description: 多数据源事务管理器配置类
 * @date 2018/11/9 11:45
 */
@Configuration
public class TransactionManagerConfig {

    /**
     * 分布式事务使用JTA管理,不管有多少个数据源只要配置一个 JtaTransactionManager
     * @return
     */
    @Bean
    public JtaTransactionManager transactionManager(){
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        UserTransaction userTransaction = new UserTransactionImp();
        return new JtaTransactionManager(userTransaction, userTransactionManager);
    }
}

       2.6、最后在classpath下创建 jta.properties 文件,进行 Atomikos 的属性配置:

com.atomikos.icatch.service=com.atomikos.icatch.standalone.UserTransactionServiceFactory
com.atomikos.icatch.max_timeout=600000
com.atomikos.icatch.default_jta_timeout=600000
com.atomikos.icatch.log_base_dir=transaction-logs
com.atomikos.icatch.log_base_name=springboot-mybatis
com.atomikos.icatch.serial_jta_transactions=false

       2.7、测试是否进行回滚:

    /**
     * 测试多数据源使用JTA是否能回滚
     * @param id 用户id
     * @return
     */
    @Override
    public ResponseData deleteUser(Integer id) {
        userinfoMapper.deleteByPrimaryKey(id);
        peopleMapper.deleteByPrimaryId(id);
        System.out.println(10/0);
        return BuildResponseUtil.buildSuccessResponse();
    }

证实是可以回滚的!

项目下载地址:项目下载地址

或访问:github

你可能感兴趣的:(springboot系列,springboot,JTA,druid,分布式事务,atomikos)