Springboot动态多数据源+jta分布式事务
前言:
公司最近要做一个项目,要求从数据库里读取配置信息,动态切换数据源,这样数据源多的情况下就不用每次修改配置文件了。然后就参考了博客上很多文章,发现很多都是数据源可以动态切换,添加事务后就切换不了,或者是多数据源配置信息写在配置文件里,这些都不是我想要的,经过两天努力,借鉴了很多文章终于解决问题。现把过程记录下来以方便他人和自己。
项目运行从默认主数据库读取所有数据源配置信息,然后交给JTA事务管理器统一管理事务。
正文开始
为了方便观看和了解项目,可以先看下项目目录结构

首先 pom.xml 文件
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.z</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- jta --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
然后 application.yml (这里配置的是主数据源)
server: port: 8090 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: # 默认主数据源 system: username: root password: 123 url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC driverClassName: com.mysql.jdbc.Driver
准备完毕,接下来就是代码
DataSourceContextHolder.java
package com.z.demo.config.utils; /* * 数据源上下文切换 * */ public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); /* * 设置当前数据源 * */ public static void setDatasourceType(String dbType) { contextHolder.set(dbType); } /* * 取得当前数据源 * */ public static String getDatasourceType() { return contextHolder.get(); } /* * 清除上下文数据源 * */ public static void clearDatasourceType() { contextHolder.remove(); } }
CustomSqlSessionTemplate.java
package com.z.demo.config.utils; import static java.lang.reflect.Proxy.newProxyInstance; import static org.apache.ibatis.reflection.ExceptionUtil.unwrapThrowable; import static org.mybatis.spring.SqlSessionUtils.closeSqlSession; import static org.mybatis.spring.SqlSessionUtils.getSqlSession; import static org.mybatis.spring.SqlSessionUtils.isSqlSessionTransactional; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.sql.Connection; import java.util.List; import java.util.Map; import org.apache.ibatis.exceptions.PersistenceException; import org.apache.ibatis.executor.BatchResult; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.MyBatisExceptionTranslator; import org.mybatis.spring.SqlSessionTemplate; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.util.Assert; public class CustomSqlSessionTemplate 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 void setTargetSqlSessionFactorys(Map<Object, SqlSessionFactory> targetSqlSessionFactorys) { this.targetSqlSessionFactorys = targetSqlSessionFactorys; } public Map<Object, SqlSessionFactory> getTargetSqlSessionFactorys() { return targetSqlSessionFactorys; } public void setDefaultTargetSqlSessionFactory(SqlSessionFactory defaultTargetSqlSessionFactory) { this.defaultTargetSqlSessionFactory = defaultTargetSqlSessionFactory; } public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType()); } public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { this(sqlSessionFactory, executorType , new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration() .getEnvironment().getDataSource(), true)); } public CustomSqlSessionTemplate(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; } @Override public SqlSessionFactory getSqlSessionFactory() { SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys.get(DataSourceContextHolder.getDatasourceType()); if (targetSqlSessionFactory != null) { return targetSqlSessionFactory; } else if (defaultTargetSqlSessionFactory != null) { return defaultTargetSqlSessionFactory; } else { Assert.notNull(targetSqlSessionFactorys, "Property 'targetSqlSessionFactorys' or 'defaultTargetSqlSessionFactory' are required"); Assert.notNull(defaultTargetSqlSessionFactory, "Property 'defaultTargetSqlSessionFactory' or 'targetSqlSessionFactorys' are required"); } 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} */ public void select(String statement, ResultHandler handler) { this.sqlSessionProxy.select(statement, handler); } /** * {@inheritDoc} */ public void select(String statement, Object parameter, ResultHandler handler) { this.sqlSessionProxy.select(statement, parameter, handler); } /** * {@inheritDoc} */ 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( CustomSqlSessionTemplate.this.getSqlSessionFactory(), CustomSqlSessionTemplate.this.executorType, CustomSqlSessionTemplate.this.exceptionTranslator); try { Object result = method.invoke(sqlSession, args); if (!isSqlSessionTransactional(sqlSession, CustomSqlSessionTemplate.this.getSqlSessionFactory())) { sqlSession.commit(true); } return result; } catch (Throwable t) { Throwable unwrapped = unwrapThrowable(t); if (CustomSqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { Throwable translated = CustomSqlSessionTemplate.this.exceptionTranslator .translateExceptionIfPossible((PersistenceException) unwrapped); if (translated != null) { unwrapped = translated; } } throw unwrapped; } finally { closeSqlSession(sqlSession, CustomSqlSessionTemplate.this.getSqlSessionFactory()); } } } }
AbstractDataSourceConfig.java
package com.z.demo.config; import com.atomikos.jdbc.AtomikosDataSourceBean; import org.springframework.core.env.Environment; import javax.sql.DataSource; import java.util.Properties; public abstract class AbstractDataSourceConfig { 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.setUniqueResourceName(dataSourceName); ds.setXaProperties(prop); return ds; } /** * 主要针对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("driverClassName", env.getProperty(prefix + "driverClassName", "")); return prop; } }
MybatisConfig.java
package com.z.demo.config; import com.atomikos.jdbc.AtomikosDataSourceBean; import com.z.demo.config.utils.CustomSqlSessionTemplate; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; 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.context.annotation.Primary; import org.springframework.core.env.Environment; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; import java.util.Properties; @Configuration @Component @MapperScan(basePackages = MybatisConfig.BASE_PACKAGE, sqlSessionTemplateRef = "sqlSessionTemplate") public class MybatisConfig extends AbstractDataSourceConfig { //mapper模式下的接口层 static final String BASE_PACKAGE = "com.z.demo.mapper"; //对接数据库的实体层 private static final String ALIASES_PACKAGE = "com.z.demo.entity"; private static final String MAPPER_LOCATION = "classpath:mapper/*.xml"; @Primary @Bean(name = "dataSourceSystem") public DataSource dataSourceOne(Environment env) { String prefix = "spring.datasource.druid.system."; return getDataSource(env, prefix, "system"); } @Bean(name = "sqlSessionFactorySystem") public SqlSessionFactory sqlSessionFactoryOne(@Qualifier("dataSourceSystem") DataSource dataSource) throws Exception { return createSqlSessionFactory(dataSource); } @Bean(name = "sqlSessionTemplate") public CustomSqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactorySystem") SqlSessionFactory factorySystem) throws Exception { Map<Object, SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>(); sqlSessionFactoryMap.put("system", factorySystem); CustomSqlSessionTemplate customSqlSessionTemplate = new CustomSqlSessionTemplate(factorySystem); customSqlSessionTemplate.setTargetSqlSessionFactorys(sqlSessionFactoryMap); return customSqlSessionTemplate; } /** * 创建数据源 */ public SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setVfs(SpringBootVFS.class); bean.setTypeAliasesPackage(ALIASES_PACKAGE); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION)); bean.setConfiguration(configuration()); return bean.getObject(); } public org.apache.ibatis.session.Configuration configuration() { org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); return configuration; } //创建新数据源 public DataSource getDataSource(com.z.demo.entity.DataSource dataSource) { Properties prop = new Properties(); prop.put("url", dataSource.getUrl()); prop.put("username", dataSource.getUserName()); prop.put("password", dataSource.getPassWord()); prop.put("driverClassName", "com.mysql.jdbc.Driver"); AtomikosDataSourceBean ds = new AtomikosDataSourceBean(); ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource"); ds.setUniqueResourceName(dataSource.getId()); ds.setXaProperties(prop); return ds; } }
DataSourceManager.java
package com.z.demo.config.utils; import com.z.demo.config.MybatisConfig; import com.z.demo.entity.DataSource; import com.z.demo.mapper.DataSourceMapper; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; import java.util.Map; @Component public class DataSourceManager { @Resource private DataSourceMapper dataSourceMapper; @Resource private CustomSqlSessionTemplate sqlSessionTemplate; @Resource private MybatisConfig mybatisConfig; /** * 初始或者重载入数据源 */ @PostConstruct public void loadDataSource() throws Exception { //从主数据源获取数据源配置 List<DataSource> dataSourceList = dataSourceMapper.getDataSourceList(); Map<Object, SqlSessionFactory> newSqlSessionFactoryMap = new HashMap<>(); Map<Object, SqlSessionFactory> sqlSessionFactoryMap = sqlSessionTemplate.getTargetSqlSessionFactorys(); for (DataSource dataSource : dataSourceList) { SqlSessionFactory sqlSessionFactory = mybatisConfig.createSqlSessionFactory(mybatisConfig.getDataSource(dataSource)); newSqlSessionFactoryMap.put(dataSource.getId(), sqlSessionFactory); } newSqlSessionFactoryMap.putAll(sqlSessionFactoryMap); this.sqlSessionTemplate.setTargetSqlSessionFactorys(newSqlSessionFactoryMap); } }
以上就是核心代码,还有一些无关紧要的代码就不贴出来了
接下来就是测试 多数据源的插入和事务回滚
首先是插入
package com.z.demo; import com.z.demo.config.utils.DataSourceContextHolder; import com.z.demo.entity.User; import com.z.demo.mapper.UserMapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.annotation.Resource; @SpringBootTest class DemoApplicationTests { @Resource UserMapper userMapper; @Test void contextLoads() { User user = new User(); user.setUserName("张三"); user.setAge(18); System.err.println("===当前数据源:"+ DataSourceContextHolder.getDatasourceType()); DataSourceContextHolder.setDatasourceType("demo1"); System.err.println("===当前数据源:"+ DataSourceContextHolder.getDatasourceType()); int i = userMapper.insertSelective(user); DataSourceContextHolder.setDatasourceType("demo2"); System.err.println("===当前数据源:"+ DataSourceContextHolder.getDatasourceType()); int i1 = userMapper.insertSelective(user); }
测试结果:


这边可以看到两个数据库都插入进去了数据
然后是测试事务回滚
package com.z.demo; import com.z.demo.config.utils.DataSourceContextHolder; import com.z.demo.entity.User; import com.z.demo.mapper.UserMapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @SpringBootTest class DemoApplicationTests { @Resource UserMapper userMapper; @Test @Transactional void contextLoads() { User user = new User(); user.setUserName("李四"); user.setAge(20); System.err.println("===当前数据源:"+ DataSourceContextHolder.getDatasourceType()); DataSourceContextHolder.setDatasourceType("demo1"); System.err.println("===当前数据源:"+ DataSourceContextHolder.getDatasourceType()); int i = userMapper.insertSelective(user); DataSourceContextHolder.setDatasourceType("demo2"); System.err.println("===当前数据源:"+ DataSourceContextHolder.getDatasourceType()); int i1 = userMapper.insertSelective(user); int num = 1/0; } }
测试结果:

可以看到这里发生了异常

数据库里也没有数据插入,事务统一回滚成功
以上就是动态多数据源事务解决方案,有什么不足之处或者有什么问题可以在评论区提出,拜了个拜

浙公网安备 33010602011771号