本文描述spring boot基于Atomikos+DruidXADataSource分布式事务配置(100%纯动态),也就是增加、减少数据源只需要修改application.properties文件,无需动态增加或减少Bean。
有时候我们一个应用会有N份部署,每个需要访问多个数据源,A环境可能只需要2个数据源,B环境需要5个数据源(因为我们是行业软件,所以会有这个情况,对于纯项目的系统,通常没有这个问题),所以我们希望代码只有一份,配置按需调整就确定了具体的数据源。
MapperConfig配置:
package com.hundsun.ta.aop.config; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @Configuration public class MybatisConfig { @Order(1) @Bean public MapperScannerConfigurer mapperScannerConfigurer1() { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory" + "default"); mapperScannerConfigurer.setBasePackage("com.hundsun.ta.base.mapper;com.hundsun.ta.aop.sysinfo.mapper;com.hundsun.ta.aop.parameters.mapper;com.hundsun.ta.aop.interfile.mapper;com.hundsun.ta.aop.demo.mapper;com.hundsun.ta.aop.config.mapper;com.hundsun.ta.aop.base.mapper;com.hundsun.ta.aop.auditresult.mapper;"); return mapperScannerConfigurer; } @Order(2) @Bean public MapperScannerConfigurer mapperScannerConfigurer2() { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory" + "yoga1"); mapperScannerConfigurer.setBasePackage("com.hundsun.ta.aop.ta.mapper;com.hundsun.ta.aop.ta.*.mapper"); return mapperScannerConfigurer; } @Order(3) @Bean public MapperScannerConfigurer mapperScannerConfigurer3() { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory" + "yoga2"); mapperScannerConfigurer.setBasePackage("com.hundsun.ta.aop.yoga.*.mapper;com.hundsun.ta.aop.ta4.**.mapper"); return mapperScannerConfigurer; } }
XA配置
package com.hundsun.ta.datasource; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.PropertySource; @ConfigurationProperties(prefix="dyn.spring") @PropertySource("classpath:jrescloud.properties") public class DynamicDataSourceConfig { private List<DataSource> datasources; public static class DataSource { private String name; private String driverClassName; private String url; private String username; private String password; private int maxActive; private int maxIdle; private String mapperLocations; private String basePackage; public int getMaxActive() { return maxActive; } public void setMaxActive(int maxActive) { this.maxActive = maxActive; } public int getMaxIdle() { return maxIdle; } public void setMaxIdle(int maxIdle) { this.maxIdle = maxIdle; } public int getMaxWait() { return maxWait; } public void setMaxWait(int maxWait) { this.maxWait = maxWait; } public String getValidationQuery() { return validationQuery; } public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } public boolean isDefaultAutoCommit() { return defaultAutoCommit; } public void setDefaultAutoCommit(boolean defaultAutoCommit) { this.defaultAutoCommit = defaultAutoCommit; } public String getConnectionInitSqls() { return connectionInitSqls; } public void setConnectionInitSqls(String connectionInitSqls) { this.connectionInitSqls = connectionInitSqls; } private int maxWait; private String validationQuery; private boolean defaultAutoCommit; private String connectionInitSqls; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getMapperLocations() { return mapperLocations; } public void setMapperLocations(String mapperLocations) { this.mapperLocations = mapperLocations; } public String getBasePackage() { return basePackage; } public void setBasePackage(String basePackage) { this.basePackage = basePackage; } } public List<DataSource> getDatasources() { return datasources; } public void setDatasources(List<DataSource> datasources) { this.datasources = datasources; } }
package com.hundsun.ta.datasource; import java.util.Properties; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.stereotype.Service; import com.alibaba.druid.pool.xa.DruidXADataSource; import com.atomikos.jdbc.AtomikosDataSourceBean; import com.hundsun.ta.datasource.DynamicDataSourceConfig.DataSource; import oracle.jdbc.xa.client.OracleXADataSource; @Service public class DynamicDataSourceRegister implements InitializingBean,ApplicationContextAware,BeanPostProcessor { @Value("${dbType}") private String dbType; @Value("${mybatis.mapperLocations}") private String mapperLocations; @Value("${mybatis.configLocation}") private String configLocation; @Value("${mybatis.typeAliasesPackage}") private String typeAliasesPackage; private ApplicationContext applicationContext; @Autowired private DynamicDataSourceConfig config; @Override public void afterPropertiesSet() throws Exception { // Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext; // 获取bean工厂并转换为DefaultListableBeanFactory DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); for(DataSource datasource : config.getDatasources()) { RootBeanDefinition rbd = new RootBeanDefinition(AtomikosDataSourceBean.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, true); rbd.setInitMethodName("init"); rbd.setDestroyMethodName("close"); // MutablePropertyValues propertyValues = new MutablePropertyValues(); /*propertyValues.add("url", datasource.getUrl()); // propertyValues.add("url", "jdbc:mysql://" + app.getHostname() + ":" + app.getMapPort() + "/performance_schema?useUnicode=true&characterEncoding=gbk&autoReconnect=true&failOverReadOnly=false"); // propertyValues.add("driverClassName", datasource.getDriverClassName()); propertyValues.add("username", datasource.getUsername()); propertyValues.add("password", datasource.getPassword());*/ // propertyValues.add("password", Base64Util.getFromBase64(datasource.getPassword())); propertyValues.add("minPoolSize", 1); propertyValues.add("maxPoolSize", datasource.getMaxActive()); propertyValues.add("borrowConnectionTimeout", datasource.getMaxWait()); // propertyValues.add("maintenanceInterval", 30); propertyValues.add("xaDataSourceClassName", OracleXADataSource.class.getCanonicalName()); propertyValues.add("uniqueResourceName","xa-" + datasource.getName()); Properties xaProperties = new Properties(); xaProperties.setProperty("URL", datasource.getUrl()); xaProperties.setProperty("user", datasource.getUsername()); xaProperties.setProperty("password", datasource.getPassword()); // xaProperties.setProperty("testOnborrow", "true"); propertyValues.add("xaProperties", xaProperties); rbd.setPropertyValues(propertyValues); rbd.setDependencyCheck(AbstractBeanDefinition.DEPENDENCY_CHECK_NONE); beanFactory.registerBeanDefinition("xa-" + datasource.getName(), rbd); // targetDataSources.put(datasource.getName(), applicationContext.getBean(datasource.getName())); rbd = new RootBeanDefinition(SqlSessionFactoryBean.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, true); propertyValues = new MutablePropertyValues(); propertyValues.add("configLocation", configLocation); propertyValues.add("mapperLocations", datasource.getMapperLocations()); propertyValues.add("dataSource", applicationContext.getBean("xa-" + datasource.getName())); rbd.setPropertyValues(propertyValues); rbd.setDependencyCheck(AbstractBeanDefinition.DEPENDENCY_CHECK_NONE); beanFactory.registerBeanDefinition("sqlSessionFactory" + datasource.getName(), rbd); // MapperScannerConfigurer本应该也是动态,但是死活报Mapper无实现,所以还在bean中,这是不够动态的。 /* rbd = new RootBeanDefinition(MapperScannerConfigurer.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, true); propertyValues = new MutablePropertyValues(); propertyValues.add("sqlSessionFactoryBeanName", "sqlSessionFactory" + datasource.getName()); propertyValues.add("basePackage", datasource.getBasePackage()); rbd.setPropertyValues(propertyValues); rbd.setDependencyCheck(AbstractBeanDefinition.DEPENDENCY_CHECK_NONE); beanFactory.registerBeanDefinition("mapperScanner-" + datasource.getName(), rbd);*/ propertyValues = new MutablePropertyValues(); ConstructorArgumentValues cargs = new ConstructorArgumentValues(); cargs.addIndexedArgumentValue(0, applicationContext.getBean("sqlSessionFactory" + datasource.getName())); rbd = new RootBeanDefinition(SqlSessionTemplate.class, cargs, propertyValues); rbd.setDependencyCheck(AbstractBeanDefinition.DEPENDENCY_CHECK_NONE); beanFactory.registerBeanDefinition("sqlSessionTemplate" + datasource.getName(), rbd); } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }
配置文件:
dyn.spring.datasources[0].name=default dyn.spring.datasources[0].driverClassName=oracle.jdbc.OracleDriver dyn.spring.datasources[0].url=jdbc:oracle:thin:@10.20.39.223:1521:ora11g dyn.spring.datasources[0].username=hs_aop dyn.spring.datasources[0].password=hs_aop dyn.spring.datasources[0].maxActive=100 dyn.spring.datasources[0].maxWait=5000 dyn.spring.datasources[0].maxIdle=10 dyn.spring.datasources[0].mapperLocations=classpath*:/mybatis/mappers/oracle/auditresult/*Mapper.xml dyn.spring.datasources[0].basePackage=com.hundsun.ta.base.mapper;com.hundsun.ta.aop.sysinfo.mapper;com.hundsun.ta.aop.parameters.mapper;com.hundsun.ta.aop.interfile.mapper;com.hundsun.ta.aop.demo.mapper;com.hundsun.ta.aop.config.mapper;com.hundsun.ta.aop.base.mapper;com.hundsun.ta.aop.auditresult.mapper; # 瑜伽TA分库 dyn.spring.datasources[1].name=yoga1 dyn.spring.datasources[1].driverClassName=oracle.jdbc.OracleDriver dyn.spring.datasources[1].url=jdbc:oracle:thin:@10.20.39.223:1521:ora11g dyn.spring.datasources[1].username=hs_aop dyn.spring.datasources[1].password=hs_aop dyn.spring.datasources[1].maxActive=100 dyn.spring.datasources[1].maxWait=5000 dyn.spring.datasources[1].maxIdle=10 dyn.spring.datasources[1].mapperLocations=classpath*:/mybatis/mappers/oracle/interfile/*Mapper.xml dyn.spring.datasources[1].basePackage=com.hundsun.ta.aop.ta.mapper; dyn.spring.datasources[2].name=yoga2 dyn.spring.datasources[2].driverClassName=oracle.jdbc.OracleDriver dyn.spring.datasources[2].url=jdbc:oracle:thin:@10.20.39.223:1521:ora11g dyn.spring.datasources[2].username=yoga2 dyn.spring.datasources[2].password=yoga2 dyn.spring.datasources[2].maxActive=100 dyn.spring.datasources[2].maxWait=5000 dyn.spring.datasources[2].maxIdle=10 dyn.spring.datasources[2].mapperLocations=classpath*:/mybatis/mappers/yoga2/**/*Mapper.xml dyn.spring.datasources[2].basePackage=com.hundsun.ta.aop.yuga.mapper;
这样就支持分布式事务了,示例如下:
@Transactional @Override public ResultModel<?> insert() { AgencyInfo agencyInfo = new AgencyInfo(); agencyInfo.setAgencyName("4"); agencyInfo.setAgencyNo("4"); agencyInfo.setAgencyStatus("4"); agencyInfo.setSysType("4"); defaultDsMapper.insert(agencyInfo); SubDbInfo subDbInfo = new SubDbInfo(); subDbInfo.setSubDbDataSource("4"); subDbInfo.setSubDbNo("4"); subDbInfo.setSysType("4"); // 非独立事务 taDsMapper.insert(subDbInfo); List insertList = new ArrayList(); insertList.add("aaa");
//数据源使用SqlSessionTemplate动态切换 baseBatchMapper.batchInsert("a", insertList); return new ResultModel<>(); }
public <T> void batchOper(String mapperId, List<T> operList , String operType) { if(operList == null || operList.isEmpty()) { logger.info("无需要批量入库的记录!"); return; }
// 动态传入数据源即可 sqlSessionTemplate = SpringContextHolder.getBean("sqlSessionTemplate" + "default",SqlSessionTemplate.class); SqlSession session = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
XA存在的一个问题是事务传播级别REQUIRE_NEW不生效(还没找到怎么解决),如下:
/** * 暂时不支持自治事务 */ @Transactional @Override public ResultModel<?> insertAuto() { AgencyInfo agencyInfo = new AgencyInfo(); agencyInfo.setAgencyName("2"); agencyInfo.setAgencyNo("2"); agencyInfo.setAgencyStatus("2"); agencyInfo.setSysType("2"); defaultDsMapper.insert(agencyInfo); SubDbInfo subDbInfo = new SubDbInfo(); /* subDbInfo.setSubDbDataSource("2"); subDbInfo.setSubDbNo("2"); subDbInfo.setSysType("2");*/ //独立事务,会报错,但是整个回滚了 service.insertNew(subDbInfo); agencyInfo.setAgencyName("3"); agencyInfo.setAgencyNo("3"); agencyInfo.setAgencyStatus("3"); agencyInfo.setSysType("3"); defaultDsMapper.insert(agencyInfo); return new ResultModel<>(); }
-- 加上rollbackFor,或者抛出RuntimeException都不行,整个XA被回滚了 @Transactional public ResultModel<?> insertNew(SubDbInfo subDbInfo) { taDsMapper.insert(subDbInfo); return new ResultModel<>(); }
使用druid xa数据源有一个问题,jstack会看到在获取连接的地方一直WAITING:
"http-nio-8080-exec-54" daemon prio=10 tid=0x0000000000e61000 nid=0xcc9 waiting on condition [0x00007f4a753d4000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000007a143f230> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043) at com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:1732) at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1330) at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1198) at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4619)
换成OracleXA就没有问题,使用的druid是1.1.10,所以应该不是早期版本bug的问题。