【万字长文】SpringBoot整合Atomikos实现多数据源分布式事务(提供Gitee源码)

前言:在最近的实际开发的过程中,遇到了在多数据源的情况下要保证原子性的问题,这个问题当时遇到了也是思考了一段时间,后来通过搜集大量资料与学习,最后是采用了分布式事务来解决这个问题,在讲解之前,在我往期的博客提前搭好了一个SpringBoot整合MyBatis搭建MySQL多数据源的教程,本篇博客我是在原有的这个项目的基础之上进行的改造,主要新增了几个配置项进行实现的,大家可以先简单的看一下这篇文章!

文章链接:【万字长文】SpringBoot整合MyBatis搭建MySQL多数据源完整教程(提供Gitee源码)

在文章的最后我也把整合好的完整代码都进行了提供!

目录

一、什么是Atomikos

二、什么是XA

三、项目整体结构截图 

四、导入pom依赖

五、配置mybatis-config.xml文件

六、修改yml文件

七、配置类

7.1、Atomikos配置类

7.2、DynamicSqlSessionTemplate动态切换数据源配置类

7.3、Druid配置属性

7.4、DruidConfig多数据源核心配置类

7.5、MyBatis配置类

八、运行测试

九、Gitee源码 

十、总结


一、什么是Atomikos

Atomikos是一个开源的分布式事务管理器,它可以为分布式系统提供事务管理的解决方案。Atomikos的主要作用有:

1、支持分布式事务,确保跨多个数据源的事务一致性在分布式系统中,一个业务操作可能涉及多个数据库或服务。Atomikos可以协调多个数据源,要么一起提交事务,要么一起回滚,从而保证分布式环境下数据的一致性。

2、支持各种数据库和事务APIAtomikos支持JTA规范,可以通过JTA接口与应用程序集成。同时它对各种数据库如MySQLPostgreSQL都提供了支持,还支持REST事务等。

3、确保事务的ACID特性Atomikos通过两阶段提交协议,可以确保分布式事务满足原子性、一致性、隔离性的特性,防止分布式事务状态不一致。

4、高可用和故障转移Atomikos本身可以通过设置主备模式来提供高可用性,同时还可以与负载均衡器集成实现故障转移和高可用。

5、管理和监控平台Atomikos自带了管理控制台和日志监控功能,可以方便查看事务信息、统计数据、运行情况等。

总之,Atomikos是一个非常强大和成熟的分布式事务管理器,它为构建健壮可靠的分布式系统提供了关键的事务保证。在分布式场景下,Atomikos可以说是事务处理的不二之选!

二、什么是XA

XA(eXtendedArchitecture)是一种分布式事务处理的标准规范和架构。

主要特征:

1、支持在多个数据库之间进行分布式事务管理。

2、事务处理符合ACID特性。

3、通过事务管理器(TransactionManager)调度事务。

4、通过XA数据源(XADataSource)抽象数据库的事务行为。

5、通过两阶段提交协议(2PC)保证分布式事务的一致性。

其关键角色包括:

1、事务管理器(TM):协调多个数据库的事务,管理全局事务。

2、应用程序(AP):通过XA接口与TM交互,驱动全局事务。

3、资源管理器(RM):数据库的一个抽象,对事务进行持久化保证。

4、XA数据源:实现XA接口,封装了RM,使数据库与TM进行交互。

XA的实现(如Atomikos)通过两阶段提交,能够很好地解决分布式事务的一致性问题,是实现分布式事务的重要方式之一。

三、项目整体结构截图 

仅仅实在原有搭建好的多数据源的框架上进行了改造!

四、导入pom依赖

<!-- atomikos分布式事务 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
    <version>2.7.14</version>
</dependency>

五、配置mybatis-config.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 全局参数 -->
    <settings>
        <!-- 使全局的映射器启用或禁用缓存 -->
        <setting name="cacheEnabled"             value="true"   />
        <!-- 允许JDBC 支持自动生成主键 -->
        <setting name="useGeneratedKeys"         value="true"   />
        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
        <setting name="defaultExecutorType"      value="SIMPLE" />
		<!-- 指定 MyBatis 所用日志的具体实现 -->
        <setting name="logImpl"                  value="SLF4J"  />
        <!-- 使用驼峰命名法转换字段 -->
		<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
	</settings>
	
</configuration>

这是目录结构,在resource目录下新建一个mybatis目录。

六、修改yml文件

主要修改的是MyBatis的配置,其他不动!

# MyBatis配置
mybatis:
  # 搜索指定包别名
  typeAliasesPackage: com.example.**.domain
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath:mapper/*/*.xml
  # 加载全局的配置文件
  configLocation: classpath:mybatis/mybatis-config.xml

七、配置类

我先简单的梳理一下5个配置类的作用和它们之间的联系。

1、DruidProperties这是Druid数据库连接池的配置属性类,里面定义了各个数据源的url、username等信息。

2、AtomikosConfig这是Atomikos事务管理器的配置类,创建了Atomikos的事务管理器AtomikosJtaTransactionManager。

3、DruidConfig该类利用DruidProperties创建了主从数据源masterDataSource和slaveDataSource,并用AtomikosDataSourceBean封装了它们。

4、MyBatisConfig这是MyBatis的配置类,它利用masterDataSource和slaveDataSource分别创建了两个SqlSessionFactory。

5、DynamicSqlSessionTemplate该类继承Spring的SqlSessionTemplate,实现了一个动态切换数据源的SqlSessionTemplate。它内部通过一个ThreadLocal持有当前数据源,在执行sql时,会根据数据源切换到对应的SqlSessionFactory。这样就实现了基于不同数据源的动态切换。

总结一下,DruidProperties和AtomikosConfig提供了基础配置,DruidConfig创建了Atomikos数据源,MyBatisConfig创建了SqlSessionFactory,DynamicSqlSessionTemplate实现动态切换数据源和SqlSessionFactory,从而达到事务管理+动态多数据源的效果。

7.1、Atomikos配置类

1、这个Bean创建了UserTransactionImp,它实现了JTA的UserTransaction接口,用于Begin/Commit/Rollback事务。

    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable
    {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

2、这个Bean创建了Atomikos的事务管理器UserTransactionManager,它实现了JTA的TransactionManager接口。这个是核心的事务管理器。

    @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
    public TransactionManager atomikosTransactionManager() throws Throwable
    {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);
        return userTransactionManager;
    }

3、这个Bean创建了JtaTransactionManager,它实现了Spring的PlatformTransactionManager接口。它依赖userTransaction和atomikosTransactionManager两个Bean。JtaTransactionManager作为桥接,把Atomikos的JTA事务管理机制桥接到Spring体系中,让Spring能够通过声明式事务(例如@Transactional))来使用Atomikos进行分布式事务管理。 

    @Bean(name = "transactionManager")
    @DependsOn({ "userTransaction", "atomikosTransactionManager" })
    public PlatformTransactionManager transactionManager() throws Throwable
    {
        UserTransaction userTransaction = userTransaction();
        TransactionManager atomikosTransactionManager = atomikosTransactionManager();
        return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
    }

完整代码:

package com.example.multiple.config;

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.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

/**
 * JTA 事务配置
 *
 */
@Configuration
public class AtomikosConfig
{
    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable
    {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
    public TransactionManager atomikosTransactionManager() throws Throwable
    {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);
        return userTransactionManager;
    }

    @Bean(name = "transactionManager")
    @DependsOn({ "userTransaction", "atomikosTransactionManager" })
    public PlatformTransactionManager transactionManager() throws Throwable
    {
        UserTransaction userTransaction = userTransaction();
        TransactionManager atomikosTransactionManager = atomikosTransactionManager();
        return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
    }
}

7.2、DynamicSqlSessionTemplate动态切换数据源配置类

这个DynamicSqlSessionTemplate类是一个自定义的SqlSessionTemplate,它的作用是动态切换MyBatisSqlSessionFactory,从而实现动态数据源切换。

主要逻辑是通过一个Interceptor拦截SqlSession方法调用,在调用前从Context中获取当前数据源,然后使用对应数据源的SqlSessionFactory创建SqlSession

这么做的好处是不同数据源的MyBatis操作可以通过同一个SqlSessionTemplate实例处理,避免代码里出现多个模板。

其和Atomikos的关系在于:Atomikos要管理多个数据源时,需要给每个数据源配置独立的SqlSessionFactory如果不使用动态模板,就需要在代码里维护多个SqlSessionTemplateDynamicSqlSessionTemplate可以根据当前数据源动态切换SqlSessionFactory,所以只需要配置一个DynamicSqlSessionTemplate实例即可。

Atomikos多数据源的场景下,可以避免代码里出现多个SqlSessionTemplate

所以,DynamicSqlSessionTemplate配合Atomikos使用,可以简化代码:

1、为每个数据源配置独立SqlSessionFactory。

2、使用DynamicSqlSessionTemplate+动态数据源Context统一管理。

3、不同数据源的数据库操作通过同一个模板即可执行。

完整代码:

package com.example.multiple.config.datasource;

import org.apache.ibatis.exceptions.PersistenceException;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.session.*;
import org.mybatis.spring.MyBatisExceptionTranslator;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.dao.support.PersistenceExceptionTranslator;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.List;
import java.util.Map;

import static java.lang.reflect.Proxy.newProxyInstance;
import static org.apache.ibatis.reflection.ExceptionUtil.unwrapThrowable;
import static org.mybatis.spring.SqlSessionUtils.*;


/**
 * 自定义SqlSessionTemplate,动态切换数据源
 */
public class DynamicSqlSessionTemplate extends SqlSessionTemplate
{
    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    private final SqlSession sqlSessionProxy;
    private final PersistenceExceptionTranslator exceptionTranslator;
    private Map<Object, SqlSessionFactory> targetSqlSessionFactorys;
    private SqlSessionFactory defaultTargetSqlSessionFactory;

    public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory)
    {
        this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
    }

    public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType)
    {
        this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(
                sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
    }

    public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                                     PersistenceExceptionTranslator exceptionTranslator)
    {
        super(sqlSessionFactory, executorType, exceptionTranslator);
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
                new Class[] { SqlSession.class }, new SqlSessionInterceptor());
        this.defaultTargetSqlSessionFactory = sqlSessionFactory;
    }

    public void setTargetSqlSessionFactorys(Map<Object, SqlSessionFactory> targetSqlSessionFactorys)
    {
        this.targetSqlSessionFactorys = targetSqlSessionFactorys;
    }

    public void setDefaultTargetSqlSessionFactory(SqlSessionFactory defaultTargetSqlSessionFactory)
    {
        this.defaultTargetSqlSessionFactory = defaultTargetSqlSessionFactory;
    }

    @Override
    public SqlSessionFactory getSqlSessionFactory()
    {
        SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys
                .get(DynamicDataSourceContextHolder.getDataSourceType());
        if (targetSqlSessionFactory != null)
        {
            return targetSqlSessionFactory;
        }
        else if (defaultTargetSqlSessionFactory != null)
        {
            return defaultTargetSqlSessionFactory;
        }
        return this.sqlSessionFactory;
    }

    @Override
    public Configuration getConfiguration()
    {
        return this.getSqlSessionFactory().getConfiguration();
    }

    public ExecutorType getExecutorType()
    {
        return this.executorType;
    }

    public PersistenceExceptionTranslator getPersistenceExceptionTranslator()
    {
        return this.exceptionTranslator;
    }

    /**
     * {@inheritDoc}
     */
    public <T> T selectOne(String statement)
    {
        return this.sqlSessionProxy.<T> selectOne(statement);
    }

    /**
     * {@inheritDoc}
     */
    public <T> T selectOne(String statement, Object parameter)
    {
        return this.sqlSessionProxy.<T> selectOne(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, String mapKey)
    {
        return this.sqlSessionProxy.<K, V> selectMap(statement, mapKey);
    }

    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey)
    {
        return this.sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey);
    }

    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds)
    {
        return this.sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey, rowBounds);
    }

    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement)
    {
        return this.sqlSessionProxy.<E> selectList(statement);
    }

    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement, Object parameter)
    {
        return this.sqlSessionProxy.<E> selectList(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds)
    {
        return this.sqlSessionProxy.<E> selectList(statement, parameter, rowBounds);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("rawtypes")
    public void select(String statement, ResultHandler handler)
    {
        this.sqlSessionProxy.select(statement, handler);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("rawtypes")
    public void select(String statement, Object parameter, ResultHandler handler)
    {
        this.sqlSessionProxy.select(statement, parameter, handler);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("rawtypes")
    public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)
    {
        this.sqlSessionProxy.select(statement, parameter, rowBounds, handler);
    }

    /**
     * {@inheritDoc}
     */
    public int insert(String statement)
    {
        return this.sqlSessionProxy.insert(statement);
    }

    /**
     * {@inheritDoc}
     */
    public int insert(String statement, Object parameter)
    {
        return this.sqlSessionProxy.insert(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public int update(String statement)
    {
        return this.sqlSessionProxy.update(statement);
    }

    /**
     * {@inheritDoc}
     */
    public int update(String statement, Object parameter)
    {
        return this.sqlSessionProxy.update(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public int delete(String statement)
    {
        return this.sqlSessionProxy.delete(statement);
    }

    /**
     * {@inheritDoc}
     */
    public int delete(String statement, Object parameter)
    {
        return this.sqlSessionProxy.delete(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public <T> T getMapper(Class<T> type)
    {
        return getConfiguration().getMapper(type, this);
    }

    /**
     * {@inheritDoc}
     */
    public void commit()
    {
        throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void commit(boolean force)
    {
        throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void rollback()
    {
        throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void rollback(boolean force)
    {
        throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void close()
    {
        throw new UnsupportedOperationException("Manual close is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void clearCache()
    {
        this.sqlSessionProxy.clearCache();
    }

    /**
     * {@inheritDoc}
     */
    public Connection getConnection()
    {
        return this.sqlSessionProxy.getConnection();
    }

    /**
     * {@inheritDoc}
     *
     * @since 1.0.2
     */
    public List<BatchResult> flushStatements()
    {
        return this.sqlSessionProxy.flushStatements();
    }

    /**
     * Proxy needed to route MyBatis method calls to the proper SqlSession got from Spring's Transaction Manager It also
     * unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to pass a {@code PersistenceException} to
     * the {@code PersistenceExceptionTranslator}.
     */
    private class SqlSessionInterceptor implements InvocationHandler
    {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
        {
            final SqlSession sqlSession = getSqlSession(DynamicSqlSessionTemplate.this.getSqlSessionFactory(),
                    DynamicSqlSessionTemplate.this.executorType, DynamicSqlSessionTemplate.this.exceptionTranslator);
            try
            {
                Object result = method.invoke(sqlSession, args);
                if (!isSqlSessionTransactional(sqlSession, DynamicSqlSessionTemplate.this.getSqlSessionFactory()))
                {
                    sqlSession.commit(true);
                }
                return result;
            }
            catch (Throwable t)
            {
                Throwable unwrapped = unwrapThrowable(t);
                if (DynamicSqlSessionTemplate.this.exceptionTranslator != null
                        && unwrapped instanceof PersistenceException)
                {
                    Throwable translated = DynamicSqlSessionTemplate.this.exceptionTranslator
                            .translateExceptionIfPossible((PersistenceException) unwrapped);
                    if (translated != null)
                    {
                        unwrapped = translated;
                    }
                }
                throw unwrapped;
            }
            finally
            {
                closeSqlSession(sqlSession, DynamicSqlSessionTemplate.this.getSqlSessionFactory());
            }
        }
    }
}

7.3、Druid配置属性

都是从yml文件当中读取一些参数,具体的注释代码上都标注了,这边就不多作讲解了。

完整代码:

package com.example.multiple.config.properties;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
 * druid 配置属性
 *
 */
@Configuration
public class DruidProperties
{
    @Value("${spring.datasource.druid.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.druid.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.druid.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.druid.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.druid.connectTimeout}")
    private int connectTimeout;

    @Value("${spring.datasource.druid.socketTimeout}")
    private int socketTimeout;

    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.druid.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.druid.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.druid.testOnReturn}")
    private boolean testOnReturn;

    public DruidDataSource dataSource(DruidDataSource datasource)
    {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);

        /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */
        datasource.setConnectTimeout(connectTimeout);

        /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */
        datasource.setSocketTimeout(socketTimeout);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

        /**
         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
         */
        datasource.setValidationQuery(validationQuery);
        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
        datasource.setTestWhileIdle(testWhileIdle);
        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnBorrow(testOnBorrow);
        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }

    public int getInitialSize()
    {
        return initialSize;
    }

    public void setInitialSize(int initialSize)
    {
        this.initialSize = initialSize;
    }

    public int getMinIdle()
    {
        return minIdle;
    }

    public void setMinIdle(int minIdle)
    {
        this.minIdle = minIdle;
    }

    public int getMaxActive()
    {
        return maxActive;
    }

    public void setMaxActive(int maxActive)
    {
        this.maxActive = maxActive;
    }

    public int getMaxWait()
    {
        return maxWait;
    }

    public void setMaxWait(int maxWait)
    {
        this.maxWait = maxWait;
    }

    public int getConnectTimeout()
    {
        return connectTimeout;
    }

    public void setConnectTimeout(int connectTimeout)
    {
        this.connectTimeout = connectTimeout;
    }

    public int getSocketTimeout()
    {
        return socketTimeout;
    }

    public void setSocketTimeout(int socketTimeout)
    {
        this.socketTimeout = socketTimeout;
    }

    public int getTimeBetweenEvictionRunsMillis()
    {
        return timeBetweenEvictionRunsMillis;
    }

    public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis)
    {
        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
    }

    public int getMinEvictableIdleTimeMillis()
    {
        return minEvictableIdleTimeMillis;
    }

    public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis)
    {
        this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
    }

    public int getMaxEvictableIdleTimeMillis()
    {
        return maxEvictableIdleTimeMillis;
    }

    public void setMaxEvictableIdleTimeMillis(int maxEvictableIdleTimeMillis)
    {
        this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
    }

    public String getValidationQuery()
    {
        return validationQuery;
    }

    public void setValidationQuery(String validationQuery)
    {
        this.validationQuery = validationQuery;
    }

    public boolean isTestWhileIdle()
    {
        return testWhileIdle;
    }

    public void setTestWhileIdle(boolean testWhileIdle)
    {
        this.testWhileIdle = testWhileIdle;
    }

    public boolean isTestOnBorrow()
    {
        return testOnBorrow;
    }

    public void setTestOnBorrow(boolean testOnBorrow)
    {
        this.testOnBorrow = testOnBorrow;
    }

    public boolean isTestOnReturn()
    {
        return testOnReturn;
    }

    public void setTestOnReturn(boolean testOnReturn)
    {
        this.testOnReturn = testOnReturn;
    }
}

7.4、DruidConfig多数据源核心配置类

对比之前搭建好的多数据源项目,主要变更如下:

1、构建Druid数据源属性。

    protected Properties build(Environment env, String prefix)
    {
        Properties prop = new Properties();
        prop.put("url", env.getProperty(prefix + "url"));
        prop.put("username", env.getProperty(prefix + "username"));
        prop.put("password", env.getProperty(prefix + "password"));
        prop.put("initialSize", druidProperties.getInitialSize());
        prop.put("minIdle", druidProperties.getMinIdle());
        prop.put("maxActive", druidProperties.getMaxActive());
        prop.put("maxWait", druidProperties.getMaxWait());
        prop.put("timeBetweenEvictionRunsMillis", druidProperties.getTimeBetweenEvictionRunsMillis());
        prop.put("minEvictableIdleTimeMillis", druidProperties.getMinEvictableIdleTimeMillis());
        prop.put("maxEvictableIdleTimeMillis", druidProperties.getMaxEvictableIdleTimeMillis());
        prop.put("validationQuery", druidProperties.getValidationQuery());
        prop.put("testWhileIdle", druidProperties.isTestWhileIdle());
        prop.put("testOnBorrow", druidProperties.isTestOnBorrow());
        prop.put("testOnReturn", druidProperties.isTestOnReturn());
        return prop;
    }

 2、创建AtomikosDataSourceBean的数据源实例。

具体逻辑:

1调用build()方法构建Druid数据源的属性配置。

2创建AtomikosDataSourceBean对象ds

3设置XA数据源全类名为DruidXADataSource,即使用Druid作为连接池。

4添加连接池限制配置,如最大连接数、最小连接数等。

5设置uniqueResourceName,即数据源名称。

6Druid属性设置为XA属性。

7返回构建好的AtomikosDataSourceBean实例。

AtomikosDataSourceBeanAtomikos提供的XA数据源实现,它封装了一个普通连接池的数据源,具备分布式事务的功能。这里使用Druid作为底层连接池,通过Atomikos进行XA事务管理。

    protected DataSource getDataSource(Environment env, String prefix, String dataSourceName)
    {
        Properties prop = build(env, prefix);
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        // 添加连接池限制
        ds.setMaxPoolSize(50);
        ds.setMinPoolSize(5);
        ds.setBorrowConnectionTimeout(60);
        ds.setUniqueResourceName(dataSourceName);
        ds.setXaProperties(prop);
        return ds;
    }

3、创建主从数据源Bean。

@DependsOn注解表示该Bean依赖transactionManager Bean。

@ConfigurationProperties注解加载配置前缀为"spring.datasource.druid.master"的配置。

 getDataSource方法使用DruidDataSourceFactory创建Druid数据源,并配置参数.

    @Bean
    @DependsOn({"transactionManager"})
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(Environment env)
    {
        String prefix = "spring.datasource.druid.master.";
        return getDataSource(env, prefix, MASTER);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @DependsOn({"transactionManager"})
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(Environment env)
    {
        String prefix = "spring.datasource.druid.slave.";
        return getDataSource(env, prefix, SLAVE);
    }

完整代码:

package com.example.multiple.config;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.*;
import javax.sql.DataSource;

import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.example.multiple.config.datasource.DynamicDataSource;
import com.example.multiple.enums.DataSourceType;
import com.example.multiple.config.properties.DruidProperties;
import com.example.multiple.utils.SpringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.core.env.Environment;

@Configuration
public class DruidConfig
{
    public static final String MASTER = DataSourceType.MASTER.name();

    public static final String SLAVE = DataSourceType.SLAVE.name();

    @Autowired
    private DruidProperties druidProperties;

    @Bean
    @DependsOn({"transactionManager"})
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(Environment env)
    {
        String prefix = "spring.datasource.druid.master.";
        return getDataSource(env, prefix, MASTER);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @DependsOn({"transactionManager"})
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(Environment env)
    {
        String prefix = "spring.datasource.druid.slave.";
        return getDataSource(env, prefix, SLAVE);
    }

    protected DataSource getDataSource(Environment env, String prefix, String dataSourceName)
    {
        Properties prop = build(env, prefix);
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        // 添加连接池限制
        ds.setMaxPoolSize(50);
        ds.setMinPoolSize(5);
        ds.setBorrowConnectionTimeout(60);
        ds.setUniqueResourceName(dataSourceName);
        ds.setXaProperties(prop);
        return ds;
    }

    protected Properties build(Environment env, String prefix)
    {
        Properties prop = new Properties();
        prop.put("url", env.getProperty(prefix + "url"));
        prop.put("username", env.getProperty(prefix + "username"));
        prop.put("password", env.getProperty(prefix + "password"));
        prop.put("initialSize", druidProperties.getInitialSize());
        prop.put("minIdle", druidProperties.getMinIdle());
        prop.put("maxActive", druidProperties.getMaxActive());
        prop.put("maxWait", druidProperties.getMaxWait());
        prop.put("timeBetweenEvictionRunsMillis", druidProperties.getTimeBetweenEvictionRunsMillis());
        prop.put("minEvictableIdleTimeMillis", druidProperties.getMinEvictableIdleTimeMillis());
        prop.put("maxEvictableIdleTimeMillis", druidProperties.getMaxEvictableIdleTimeMillis());
        prop.put("validationQuery", druidProperties.getValidationQuery());
        prop.put("testWhileIdle", druidProperties.isTestWhileIdle());
        prop.put("testOnBorrow", druidProperties.isTestOnBorrow());
        prop.put("testOnReturn", druidProperties.isTestOnReturn());
        return prop;
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource)
    {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(MASTER, masterDataSource);
        setDataSource(targetDataSources, SLAVE, "slaveDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }

    /**
     * 设置数据源
     *
     * @param targetDataSources 备选数据源集合
     * @param sourceName 数据源名称
     * @param beanName bean名称
     */
    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
    {
        try
        {
            DataSource dataSource = SpringUtils.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        }
        catch (Exception e)
        {
        }
    }

}


7.5、MyBatis配置类

1、常量-设置Mapper接口的扫描路径。

static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

2、扫描指定的包路径,获取包下面所有的类,从而自动设置MyBatis的类型别名。

具体逻辑:

1通过ResourcePatternResolver扫描指定的包路径下的所有class文件资源。

2对每个class资源,使用MetadataReader读取类信息,拿到完整类名。

3通过Class.forName获取类,再调用getPackage()getName()获取包名。

4将扫描到的所有包名存入List中。

5、使用Set对包名进行去重,避免重复。

6将包名数组JOIN成字符串,赋值给typeAliasesPackage

7 如果扫描不到任何包,则抛出异常。

这样就可以扫描指定包路径下的所有类,自动收集它们的包名,并设置到MyBatistypeAliasesPackage中。

MyBatis就会自动将这些包名下面的类注册为类型别名,我们在Mapper映射文件中可以直接使用类名,而不需要写完整的全限定类名。这种方式可以大大简化配置,也更灵活,当新增子包或者类时,不需要手动维护typeAliases配置。

    public static String setTypeAliasesPackage(String typeAliasesPackage)
    {
        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
        List<String> allResult = new ArrayList<String>();
        try
        {
            for (String aliasesPackage : typeAliasesPackage.split(","))
            {
                List<String> result = new ArrayList<String>();
                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
                Resource[] resources = resolver.getResources(aliasesPackage);
                if (resources != null && resources.length > 0)
                {
                    MetadataReader metadataReader = null;
                    for (Resource resource : resources)
                    {
                        if (resource.isReadable())
                        {
                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
                            try
                            {
                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
                            }
                            catch (ClassNotFoundException e)
                            {
                                e.printStackTrace();
                            }
                        }
                    }
                }
                if (result.size() > 0)
                {
                    HashSet<String> hashResult = new HashSet<String>(result);
                    allResult.addAll(hashResult);
                }
            }
            if (allResult.size() > 0)
            {
                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
            }
            else
            {
                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return typeAliasesPackage;
    }

2、用来解析MyBatis的Mapper接口位置,将字符串位置转换为Resource资源对象。

具体逻辑:

1、创建PathMatchingResourcePatternResolver对象,它可以匹配ANT样式的资源路径。

2、按照给定的mapperLocations数组,逐个解析字符串路径。

3、使用getResources()获取路径对应的Resource资源数组。

4、将所有解析到的Resource资源添加到List中。

5、最后转换为Resource数组返回。

这样做的好处是,我们可以在MyBatis配置文件中,使用类似“classpath*:com/my/mappers/**/*.xml”这样的通配符路径。

public Resource[] resolveMapperLocations(String[] mapperLocations)
    {
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        List<Resource> resources = new ArrayList<Resource>();
        if (mapperLocations != null)
        {
            for (String mapperLocation : mapperLocations)
            {
                try
                {
                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
                    resources.addAll(Arrays.asList(mappers));
                }
                catch (IOException e)
                {
                    // ignore
                }
            }
        }
        return resources.toArray(new Resource[resources.size()]);
    }

3、创建SqlSessionFactory。

具体逻辑:

1、从Environment环境变量中获取相关配置:  

typeAliasesPackage:类型别名包    

mapperLocations:Mapper接口位置    

configLocation:MyBatis全局配置文件

2、对typeAliasesPackage进行扫描解析,转换为包名数组,用于自动设置别名。

3、添加SpringBootVFS,整合MyBatis的VFS扩展接口。

4、创建SqlSessionFactoryBean实例。

5、设置数据源DataSource。

6、设置typeAliasesPackage。

7、解析mapperLocations为Resource数组。

8、设置MyBatis全局配置文件location。

9、调用getObject()方法获取SqlSessionFactory实例。

    public SqlSessionFactory createSqlSessionFactory(Environment env, DataSource dataSource) throws Exception
    {
        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
        String mapperLocations = env.getProperty("mybatis.mapperLocations");
        String configLocation = env.getProperty("mybatis.configLocation");
        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);

        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
        return sessionFactory.getObject();
    }

4、通过@Qualifier注入DruidConfig配置中的AtomikosDataSourceBean的主从数据源实例,从而创建主从数据源对应的SqlSessionFactory实例。

    @Bean(name = "sqlSessionFactoryMaster")
    public SqlSessionFactory sqlSessionFactoryMaster(Environment env, @Qualifier("masterDataSource") DataSource dataSource) throws Exception
    {
        return createSqlSessionFactory(env, dataSource);
    }

    @Bean(name = "sqlSessionFactorySlave")
    public SqlSessionFactory sqlSessionFactorySlave(Environment env, @Qualifier("slaveDataSource") DataSource dataSource) throws Exception
    {
        return createSqlSessionFactory(env, dataSource);
    }

5、创建DynamicSqlSessionTemplate的Bean,它可以支持动态切换数据源。

具体逻辑:

1、接收主从两个SqlSessionFactory。

2、创建一个HashMap,key是数据源名称,value是SqlSessionFactory。

3、新建DynamicSqlSessionTemplate实例,传入主库的SqlSessionFactory。

4、通过setTargetSqlSessionFactorys设置多个SqlSessionFactory。

这样就创建了一个动态的SqlSessionTemplate,它包含一个主SqlSessionFactory和一个从SqlSessionFactory。

    @Bean(name = "sqlSessionTemplate")
    public DynamicSqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactoryMaster") SqlSessionFactory factoryMaster,
                                                        @Qualifier("sqlSessionFactorySlave") SqlSessionFactory factorySlave) throws Exception
    {
        Map<Object, SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();
        sqlSessionFactoryMap.put(DruidConfig.MASTER, factoryMaster);
        sqlSessionFactoryMap.put(DruidConfig.SLAVE, factorySlave);

        DynamicSqlSessionTemplate customSqlSessionTemplate = new DynamicSqlSessionTemplate(factoryMaster);
        customSqlSessionTemplate.setTargetSqlSessionFactorys(sqlSessionFactoryMap);
        return customSqlSessionTemplate;
    }

完整代码: 

package com.example.multiple.config;

import com.example.multiple.config.datasource.DynamicSqlSessionTemplate;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.*;

/**
 * Mybatis支持*匹配扫描包
 *
 * @author ruoyi
 */
@Configuration
public class MyBatisConfig
{
    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    public static String setTypeAliasesPackage(String typeAliasesPackage)
    {
        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
        List<String> allResult = new ArrayList<String>();
        try
        {
            for (String aliasesPackage : typeAliasesPackage.split(","))
            {
                List<String> result = new ArrayList<String>();
                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
                Resource[] resources = resolver.getResources(aliasesPackage);
                if (resources != null && resources.length > 0)
                {
                    MetadataReader metadataReader = null;
                    for (Resource resource : resources)
                    {
                        if (resource.isReadable())
                        {
                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
                            try
                            {
                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
                            }
                            catch (ClassNotFoundException e)
                            {
                                e.printStackTrace();
                            }
                        }
                    }
                }
                if (result.size() > 0)
                {
                    HashSet<String> hashResult = new HashSet<String>(result);
                    allResult.addAll(hashResult);
                }
            }
            if (allResult.size() > 0)
            {
                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
            }
            else
            {
                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return typeAliasesPackage;
    }

    public Resource[] resolveMapperLocations(String[] mapperLocations)
    {
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        List<Resource> resources = new ArrayList<Resource>();
        if (mapperLocations != null)
        {
            for (String mapperLocation : mapperLocations)
            {
                try
                {
                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
                    resources.addAll(Arrays.asList(mappers));
                }
                catch (IOException e)
                {
                    // ignore
                }
            }
        }
        return resources.toArray(new Resource[resources.size()]);
    }

    public SqlSessionFactory createSqlSessionFactory(Environment env, DataSource dataSource) throws Exception
    {
        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
        String mapperLocations = env.getProperty("mybatis.mapperLocations");
        String configLocation = env.getProperty("mybatis.configLocation");
        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);

        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
        return sessionFactory.getObject();
    }

    @Bean(name = "sqlSessionFactoryMaster")
    public SqlSessionFactory sqlSessionFactoryMaster(Environment env, @Qualifier("masterDataSource") DataSource dataSource) throws Exception
    {
        return createSqlSessionFactory(env, dataSource);
    }

    @Bean(name = "sqlSessionFactorySlave")
    public SqlSessionFactory sqlSessionFactorySlave(Environment env, @Qualifier("slaveDataSource") DataSource dataSource) throws Exception
    {
        return createSqlSessionFactory(env, dataSource);
    }

    @Bean(name = "sqlSessionTemplate")
    public DynamicSqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactoryMaster") SqlSessionFactory factoryMaster,
                                                        @Qualifier("sqlSessionFactorySlave") SqlSessionFactory factorySlave) throws Exception
    {
        Map<Object, SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();
        sqlSessionFactoryMap.put(DruidConfig.MASTER, factoryMaster);
        sqlSessionFactoryMap.put(DruidConfig.SLAVE, factorySlave);

        DynamicSqlSessionTemplate customSqlSessionTemplate = new DynamicSqlSessionTemplate(factoryMaster);
        customSqlSessionTemplate.setTargetSqlSessionFactorys(sqlSessionFactoryMap);
        return customSqlSessionTemplate;
    }
}

八、运行测试

我简单写了个服务类,在插入的最后手动抛出一个zero的异常。

    @Transactional(rollbackFor = Exception.class)
    public void handle(){
        Log log = new Log();
        log.setContent("主数据源");
        masterMapper.insert(log);
        Logger logger = new Logger();
        logger.setContent("从数据库源");
        slaveMapper.insert(logger);
        int a = 1/0;
    }

运行结果如下:

数据库情况:

都进行了回滚,没有问题。

九、Gitee源码 

源码地址:SpringBoot整合Atomikos实现多数据源分布式事务

十、总结

以上就是我目前对于SpringBoot整合Atomikos实现多数据源分布式事务的操作过程和个人理解,如有问题,欢迎评论区讨论!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/60396.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

睿讯微带你深度了解汽车交流充电桩

这几年随着新能源汽车的普及&#xff0c;充电桩也越来越多的出现在我们的视野中。新能源纯电汽车就好比一种大号的电子产品&#xff0c;而充电桩则是它不可缺少的子系统&#xff0c;是新能源车主们的必要选择。 汽车充电桩分为直流和交流两种&#xff0c;2022年底全国公共充电桩…

Java课题笔记~ Spring 概述

Spring 框架 一、Spring 概述 1、Spring 框架是什么 Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架&#xff0c;它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转&#xff08;IoC&#xff09;和面向切面编程&#xff08;AOP&#xff09;。 Spring…

创建vue-cli(脚手架搭建)

目录 功能 需要的环境 使用HbuilderX快速搭建一个vue-cli项目 组件路由 element-ui vue-cli 官方提供的一个脚手架&#xff0c;用于快速生成一个 vue 的项目模板&#xff1b;预先定义 好的目录结构及基础代码&#xff0c;就好比咱们在创建 Maven 项目时可以选择创建一个 骨…

VR内容研发公司 | VR流感病毒实验虚拟现实课件

由广州华锐互动开发的《VR流感病毒实验虚拟现实课件》是一种新型的教学模式&#xff0c;可以为学生提供更加真实和直观的流感病毒分离鉴定实验操作体验&#xff0c;从而提高学生的实验技能和工作效率。 《VR流感病毒实验虚拟现实课件》涉及了生物安全二级实验室(BSL-2)和流感病…

分布式应用:Zookeeper 集群与kafka 集群部署

目录 一、理论 1.Zookeeper 2.部署 Zookeeper 集群 3.消息队列 4.Kafka 5.部署 kafka 集群 6.FilebeatKafkaELK 二、实验 1.Zookeeper 集群部署 2.kafka集群部署 3.FilebeatKafkaELK 三、问题 1.解压文件异常 2.kafka集群建立失败 3.启动 filebeat报错 4.VIM报错…

基于DiscordMidjourney API接口实现文生图

https://discord.com/api/v9/interactions 请求头&#xff1a; authorization:取自 浏览器中discord 文生图请求头中的 authorization 的值 Content-Type:application/json 请求体&#xff1a; {“type”:2,“application_id”:“93692956130267xxxx”,“guild_id”:“1135900…

python-MySQL数据库建表语句(需要连接数据库)转存为Excel文档-工作小记

将create table XXXXXX 转为指定Excel文档。该脚本适用于数据库表结构本地文档记录 呈现效果 代码 # -*- coding:utf-8 -*- # Time : 2023/8/2 15:14 # Author: 水兵没月 # File : MySQL建表_2_excel.py import reimport mysql.connector import pandas as pd db 库名 mydb …

TS协议之PMT(节目映射表)

TS协议之PAT&#xff08;节目关联表&#xff09; 1.概要 PMT&#xff1a;节目映射表&#xff0c;与PAT成对出现&#xff0c;包含了该节目下所有的节目元素。 PMT数据结构如下&#xff1a; 字段分析&#xff1a; 字段字段描述表id标识一个TS PSI分段的内容是节目关联分段&am…

性能分析记录

4实例压测TPS浮动在200-300 1.TPS浮动200-300&#xff0c;ART浮动的可能性是10-20ms&#xff0c;链路复杂是可接受的&#xff0c;链路简单则需要分析原因。 1&#xff09;缓存没命中&#xff0c;对某些账号缓存没命中&#xff0c;或缓存失效后导致隔段时间耗时升高。 2&…

MySQL正则表达式检索数据

目录 一、使用正则表达式进行基本字符匹配 1.使用regexp关键字 2.使用正则表达式 . 二、进行OR匹配 1.为搜索两个串之一&#xff0c;使用 | 2.匹配几个字符之一[] 3.匹配范围 4.匹配特殊字符 过滤数据允许使用匹配、比较、通配符操作来寻找数据&#xff0c;但是随…

mac前端代码编辑 Sublime Text 4 Dev 中文v4.0(4151)

Sublime Text 4 for Mac是一款功能强大的代码编辑器&#xff0c;适合所有需要高效编写代码和进行代码管理的程序员使用。 快速响应&#xff1a;Sublime Text 4在加载文件和执行命令时非常快速&#xff0c;能够让用户在高效的开发过程中体验到无缝的交互。 多种语言支持&#…

第一章-JavaScript基础进阶part3:BOM

文章目录 一、BOM概述1.1 什么是BOM 二、window对象的常见事件2.1 页面加载事件2.2 调整窗口大小事件onresize 三、定时器3.1 案例 四、JS执行机制4.1 this指向4.2 JS执行机制1、JS是单线程2、JS的同步和异步3、JS的执行机制 五、location对象5.1 locationc对象常用属性5.2 loc…

Win11大小写切换图标关闭方法

大家使用Win11操作系统的时候经常会切换大小写键盘&#xff0c;有些游戏本在游戏过程中需要切换大小写&#xff0c;这个时候电脑的屏幕就会出现大小写切换的图标而影响游戏体验&#xff1b; 那么想要关闭Win11电脑上大小写切换图标&#xff0c;又不知道具体怎么操作&#xff0c…

java学习路程之篇四、进阶知识、石头迷阵游戏、绘制界面、打乱石头方块、移动业务、游戏判定胜利、统计步数、重新游戏

文章目录 1、绘制界面2、打乱石头方块3、移动业务4、游戏判定胜利5、统计步数6、重新游戏7、完整代码 1、绘制界面 2、打乱石头方块 3、移动业务 4、游戏判定胜利 5、统计步数 6、重新游戏 7、完整代码 java之石头迷阵单击游戏、继承、接口、窗体、事件、组件、按钮、图片

一零六七、JVM梳理

JVM&#xff1f; Java虚拟机&#xff0c;可以理解为Java程序的运行环境&#xff0c;可以执行Java字节码&#xff08;Java bytecode&#xff09;并提供了内存管理、垃圾回收、线程管理等功能 java内存区域划分?每块内存中都对应什么? 方法区&#xff1a;类的结构信息、常量池、…

优化|当机器学习上运筹学:PyEPO与端对端预测后优化

分享者&#xff1a;唐博 编者按&#xff1a;​ 这篇文章我想要写已经很久了&#xff0c;毕竟“端对端预测后优化”&#xff08;End-to-End Predict-then-Optimize&#xff09;正是我读博期间的主要研究方向&#xff0c;但我又一直迟迟没能下笔。想说自己杂事缠身&#xff08;实…

高温环境下光模块光功率降低的原因与解决方案

光模块是光纤通信系统中的关键组件&#xff0c;其稳定的光功率输出对于确保通信质量至关重要。然而&#xff0c;高温环境下光模块的光功率往往会出现下降&#xff0c;本期文章我们将探讨高温环境下光模块光功率降低的原因和解决方案。 一、高温环境下光功率降低的原因 &#…

算法练习--leetcode 数组

文章目录 爬楼梯问题裴波那契数列两数之和 [数组]合并两个有序数组移动零找到所有数组中消失的数字 爬楼梯问题 输入n阶楼梯&#xff0c;每次爬1或者2个台阶&#xff0c;有多少种方法可以爬到楼顶&#xff1f; 示例1&#xff1a;输入2&#xff0c; 输出2 一次爬2阶&#xff1…

金鸣识别将无表格线的图片转为excel的几个常用方案

我们知道&#xff0c;金鸣识别要将横竖线齐全的表格图片转为excel非常简单&#xff0c;但要是表格线不齐全甚至没有表格线的图片呢&#xff1f;这就没那么容易了&#xff0c;在识别这类图片时&#xff0c;我们一般会使用以下的一种或多种方法进行处理&#xff1a; 1. 基于布局…

Devart dbForge Studio for MySQL Crack

Devart dbForge Studio for MySQL Crack dbForge Studio for MySQL是一个用于MySQL和MariaDB数据库开发、管理和管理的通用GUI工具。IDE允许您通过直观的界面创建和执行查询、开发和调试存储例程、自动化数据库对象管理、分析表数据。MySQL客户端提供了数据和模式比较和同步工具…