前言:
由于項目中用到了多個資料源,是以需要配置多資料源。這時候就不能使用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都反應了。希望有合理的解決方式。如果各位看到這裡的同行,有好的解決方式的話,指教一下。