本文源代码主要来源于简书作者“险远的奇伟诡怪”的文章Spring Boot 集成Mybatis实现主从(多数据源)分离方案,感谢作者的奉献,但是由于作者写的代码pull下来后未能实现主从分离,而且有诸多bug存在,遂写该文记录一下。
如下图原作者评论区,大部分读者都未实现主从分离:
通过debug发现,启动后程序并没有进入MybatisConfiguration中的sqlSessionFactory的方法,所有主从配置未初始化到spring容器中。究其原因,是因为作者定义的方法和父类的方法名返回值都一模一样,程序走了父类MybatisAutoConfiguration中sqlSessionFactory的方法默认的配置,所有从库未生效。
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
factory.setConfiguration(properties.getConfiguration());
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory.getObject();
}
找到问题后,修改如下
@Configuration
@AutoConfigureAfter({ DataSourceConfiguration.class })
public class MybatisConfiguration extends MybatisAutoConfiguration {
private static Log logger = LogFactory.getLog(MybatisConfiguration.class);
@Resource(name = "masterDataSource")
private DataSource masterDataSource;
@Resource(name = "slaveDataSource")
private DataSource slaveDataSource;
@Bean
public SqlSessionFactory sqlSessionFactoryTest() throws Exception {
return super.sqlSessionFactory(roundRobinDataSouceProxy());
}
public ReadWriteSplitRoutingDataSource roundRobinDataSouceProxy() {
ReadWriteSplitRoutingDataSource proxy = new ReadWriteSplitRoutingDataSource();
Map<Object, Object> targetDataResources = new ClassLoaderRepository.SoftHashMap();
// Map<Object, Object> targetDataResources = new HashMap<Object, Object>();
targetDataResources.put(ReadWriteSplitRoutingDataSource.DbType.MASTER, masterDataSource);
targetDataResources.put(ReadWriteSplitRoutingDataSource.DbType.SLAVE, slaveDataSource);
proxy.setDefaultTargetDataSource(masterDataSource);
proxy.setTargetDataSources(targetDataResources);
return proxy;
}
}
启动不报错了,但是访问http://localhost:8080/dictype/all,报下面异常
java.lang.IllegalArgumentException: DataSource router not initialized
dataSource未初始化,debug发现,源码类AbstractRoutingDataSource中初始化DataSource发现,传递给父类的datasource需要是DataSource类型或者String类型,而我们在自定义类MybatisConfiguration中封装DataSource的时候使用的是SoftHashMap
public static class SoftHashMap extends AbstractMap {
private Map<Object, SpecialValue> map;
boolean recordMiss = true; // only interested in recording miss stats sometimes
private ReferenceQueue rq = new ReferenceQueue();
public SoftHashMap(Map<Object, SpecialValue> map) {
this.map = map;
}
public SoftHashMap() {
this(new HashMap());
}
观察SoftHashMap源码发现:SoftHashMap的value值类型为SpecialValue类型,不是上面源码需要的DataSource类型或者String类型,所以修改如下:
@Configuration
@AutoConfigureAfter({ DataSourceConfiguration.class })
public class MybatisConfiguration extends MybatisAutoConfiguration {
private static Log logger = LogFactory.getLog(MybatisConfiguration.class);
@Resource(name = "masterDataSource")
private DataSource masterDataSource;
@Resource(name = "slaveDataSource")
private DataSource slaveDataSource;
@Bean
public SqlSessionFactory sqlSessionFactoryTest(ReadWriteSplitRoutingDataSource dataSource) throws Exception {
logger.info("-------------------- 重载父类 sqlSessionFactory init---------------------");
return super.sqlSessionFactory(dataSource);
}
@Bean
public ReadWriteSplitRoutingDataSource roundRobinDataSouceProxy() {
ReadWriteSplitRoutingDataSource proxy = new ReadWriteSplitRoutingDataSource();
Map<Object, Object> targetDataResources = new HashMap<Object, Object>();
targetDataResources.put(ReadWriteSplitRoutingDataSource.DbType.MASTER, masterDataSource);
targetDataResources.put(ReadWriteSplitRoutingDataSource.DbType.SLAVE, slaveDataSource);
proxy.setDefaultTargetDataSource(masterDataSource);
proxy.setTargetDataSources(targetDataResources);
return proxy;
}
}
重启后再次访问http://localhost:8080/dictype/all,发现完美的调用了从库的地址:
init datasource error, url: jdbc:mysql://192.168.249.128:3381/db-test?characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up.
至此,完美实现多数据源主从分离。
综上,遇到问题要多debug,多方位考虑问题的症结所在。