前言:
由于项目中用到了多个数据源,所以需要配置多数据源。这时候就不能使用springboot的默认数据源加载了,需要自定义多个数据源。
一、配置准备
看了springboot的doc和一些博主的经验之谈,发现配置多数据源并不麻烦。选择了一种比较简单的方式,自定义DataSource,SqlSessionFactoryBean,SqlSessionTemplate和DataSourceTransactionManager等。
二、多数据源加载类以及配置文件
1.配置多个数据源在application.yaml中
spring:
datasource:
datasource-xxorder:
name: datasource-xxorder
url: jdbc:mysql://{$host}:{$port}/{$db}?characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull
username: xxxx
password: xxxx
driver-class-name: com.mysql.jdbc.Driver
max-pool-size: 20
max-active: 10
max-idle: 5
min-idle: 2
initial-size: 2
validation-query: select 1
test-on-borrow: true
test-on-return: false
test-while-idle: false
time-between-eviction-runs-millis: 3000
min-evictable-idle-time-millis: 3000
max-wait: 3000
jmx-enabled: true
datasource-xxauth:
name: datasource-xxauth
url: jdbc:mysql://{$host}:{$port}/{$db}?characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull
username: xxxxxx
password: xxxxxx
driver-class-name: com.mysql.jdbc.Driver
max-pool-size: 20
max-active: 10
max-idle: 5
min-idle: 2
initial-size: 2
validation-query: select 1
test-on-borrow: true
test-on-return: false
test-while-idle: false
time-between-eviction-runs-millis: 3000
min-evictable-idle-time-millis: 3000
max-wait: 3000
jmx-enabled: true
2.datasource 配置类,要让springboot能自动加载自定义的数据源
@Configuration
public class DataSourceConfig {
@Primary
@Bean(name = "xxOrderDSProperties")
@Qualifier("xxOrderDSProperties")
@ConfigurationProperties(prefix = "spring.datasource.datasource-xxorder")
public DataSourceProperties xxOrderDSProperties(){//这是是用hikariCP的时候用的
return new DataSourceProperties();
}
@Bean(name = "xxAuthDSProperties")
@Qualifier("xxAuthDSProperties")
@ConfigurationProperties(prefix = "spring.datasource.datasource-xxauth")
public DataSourceProperties xxAuthDSProperties(){
return new DataSourceProperties();
}
@Primary
@Bean(name = "xxorderDS")
@ConfigurationProperties(prefix = "spring.datasource.datasource-xxorder")
public HikariDataSource dataSourceOrder(){
//return DruidDataSourceBuilder.create().build(); //使用druidCP时打开这个注释,同时注释掉下面一行
return xxOrderDSProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean(name = "xxauthDS")
@ConfigurationProperties(prefix = "spring.datasource.datasource-xxauth")
public HikariDataSource dataSourceAuth(){
//return DruidDataSourceBuilder.create().build();//使用druidCP时打开这个注释,同时注释掉下面一行
return xxAuthDSProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}}
说明:
i.对于数据源加载配置来讲,springboot2.0 默认的数据库连接池用的是hikari2.7.9,所以不用引入依赖了。但是创建数据源的时候要相应的指定类型,也可以在配置文件中指定。
ii.这里可以看到,笔者使用了DataSourceProperties来创建数据源对象,是因为hikariCP加载配置文件对字段的key与默认jdbc配置的不一致。e.g. url,其要求为jdbc-url。但是如果要保持为url的话,就需要通过DataSourceProperties来取得相应的属性。
3.mybatis对应mapper的config加载类
这个地方就是要把原来springboot自动加载的mybatis的sqlsessionfactory和sqltemplate自定义加载处理。如果用到事务管理的话也要相应的定义出来。这里举一个例子,多个的话相似。如果不做读写分离,一个mapper也不用多个数据源的话可以不做动态数据源。
@Configuration
@MapperScan(basePackages = {"com.xx.dao.xxorder"}, sqlSessionFactoryRef = "xxorderSqlSessionFactory")
public class MybatisDBPayOrderConfig {
@Autowired
@Qualifier("xxorderDS")
private DataSource xxorderDS;
@Bean(name = "xxorderSqlSessionFactory")
public SqlSessionFactory xxorderSqlSessionFactory() throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(xxorderDS);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/xxorder/*.xml"));
return sqlSessionFactoryBean.getObject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
@Bean(name = "xxorderSqlSessionTemplate")
public SqlSessionTemplate xxorderSqlSessionTemplate() throws Exception{
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(xxorderSqlSessionFactory());
return sqlSessionTemplate;
}
@Bean(name = "xxorderDataSourceTransactionManager")
public DataSourceTransactionManager xxorderDataSourceTransactionManager() throws Exception{
DataSourceTransactionManager manager = new DataSourceTransactionManager(xxorderDS);
return manager;
}
}
这里说明一下:
i.配置用到的annotation和对应的属性:
@MapperScan(basePackages = {"com.xx.dao.xxorder"}, sqlSessionFactoryRef = "xxorderSqlSessionFactory")
@MapperScan用来detect&scan对应的mapper所在的包,这里用到的两个属性:
1)basePackages:mapper dao所在的包reference地址
2)sqlSessionFactoryRef:所要加载的sqlSessionFactory的name,要在加载的时候知道这个mapper用的是哪个sqlSessionFactory。
ii.其他的就是SqlSessionTemplate以及TransactionManager的配置,跟在spring中用配置文件配置类似。只不过这里是用configuration annotation在启动时注入的。
iii.再多加数据源以及对应mapper的话直接按照上面的方式配置就可以。不再赘述了,有问题可以给我留言。
4.mybatis对应的mapper.xml, mapper dao, entity对应的配置,通用方法不在本文重复,可以参考笔者的另外一篇springboot+mybatis配置博文springboot配置mybatis 。
5.在启动类中加上exclude,不要使用默认的DataSource加载
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
好了,到这里配置就结束了,成功启动application,没有问题。
5.如果使用druidCP的话,改动的地方很小,就把获取数据源的时候,上面代码注释掉的那行放开,然后获取hikariCP数据源的那行注释掉就可以了。
踩到并解决的坑:
下面笔者来讲一下遇到的一个大坑,如果你的项目也用到了atlas作为db中间件的话,要仔细看这个问题,敲黑板:
我们尝试调用mapper中的sql来看一下。这时候报错了:
2018-06-29 18:02:59.189 WARN 37899 --- [nio-8008-exec-3] c.z.h.p.PoolBase : HikariPool-1 - Default transaction isolation level detection failed (Proxy Warning - near ".": syntax error).
2018-06-29 18:02:59.199 ERROR 37899 --- [nio-8008-exec-3] c.z.h.p.HikariPool : HikariPool-1 - Exception during pool initialization.
java.sql.SQLException: Proxy Warning - near ".": syntax error
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3976) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3912) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2482) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2440) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1381) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.mysql.jdbc.ConnectionImpl.getTransactionIsolation(ConnectionImpl.java:3001) ~[mysql-connector-java-5.1.46.jar:5.1.46]
at com.zaxxer.hikari.pool.PoolBase.checkDriverSupport(PoolBase.java:457) ~[HikariCP-2.7.9.jar:?]
at com.zaxxer.hikari.pool.PoolBase.setupConnection(PoolBase.java:412) ~[HikariCP-2.7.9.jar:?]
at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:370) ~[HikariCP-2.7.9.jar:?]
at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:194) ~[HikariCP-2.7.9.jar:?]
at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:460) [HikariCP-2.7.9.jar:?]
at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:534) [HikariCP-2.7.9.jar:?]
at com.zaxxer.hikari.pool.HikariPool.<init>(HikariPool.java:115) [HikariCP-2.7.9.jar:?]
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112) [HikariCP-2.7.9.jar:?]
.....不截全了,不影响理解
重点是标红的那句。 解释一下就是取不到默认的数据库事物隔离级别。为啥会这样呢。配置没有任何问题啊!
开始定位问题:
1.根据调用栈去看,不难找到报错的地方在hikari的PoolBase.java中。代码是这样的:
try {
defaultTransactionIsolation = connection.getTransactionIsolation();//报错
if (transactionIsolation == -1) {
transactionIsolation = defaultTransactionIsolation;
}
}
catch (SQLException e) {
LOGGER.warn("{} - Default transaction isolation level detection failed ({}).", poolName, e.getMessage());
if (e.getSQLState() != null && !e.getSQLState().startsWith("08")) {
throw e;
}
}
从报错的那行往里看
找到调用方法在ConnectionImpl:
public int getTransactionIsolation() throws SQLException {
synchronized (getConnectionMutex()) {
if (this.hasIsolationLevels && !getUseLocalSessionState()) {
java.sql.Statement stmt = null;
java.sql.ResultSet rs = null;
try {
stmt = getMetadataSafeStatement(this.sessionMaxRows);
String query = versionMeetsMinimum(8, 0, 3) || (versionMeetsMinimum(5, 7, 20) && !versionMeetsMinimum(8, 0, 0))
? "SELECT @@session.transaction_isolation" : "SELECT @@session.tx_isolation";//重点在这
rs = stmt.executeQuery(query);
。。。。。//省略其他代码
return this.isolationLevel;
}
}
现在可以看到报错的语句了“SELECT @@session.tx_isolation”。坑找到了,就是atlas不支持这个命令。在命令行执行验证了这个问题。
找到问题了,可是怎么解呢。这个异常是原生mysql connector中抛出来的。先留个小悬念,接着看druid的使用情况:
遇到问题之后,笔者切换到druid数据源,然后发现竟然好使!完全没有问题,sql请求妥妥的。这是什么原因的,作为driver之上CP来讲,hikari和druid用到的是一样的。那只能去druid的源码中看一下为什么了,同样的找到调用getTransactionIsolation的语句:
try {
this.underlyingTransactionIsolation = conn.getTransactionIsolation();
} catch (SQLException e) {
// compartible for alibaba corba
if ("HY000".equals(e.getSQLState())
|| "com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException".equals(e.getClass().getName())) {
// skip
} else {
throw e;
}
}
一目了然。druid的老哥机智的把这个异常skip了。所以后面正常处理,不影响。
好了,到这里问题结束了。定位问题花了一些时间,但是也捋清了一些东西,暂时先使用druid了。 关于这个问题笔者已经跟atlas和hikari都反应了。希望有合理的解决方式。如果各位看到这里的同行,有好的解决方式的话,指教一下。