springboot 2.0+mybatis+hikari/druid+atlas+mysql 配置多資料來源及遇到的坑
前言:
由於專案中用到了多個數據源,所以需要配置多資料來源。這時候就不能使用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都反應了。希望有合理的解決方式。如果各位看到這裡的同行,有好的解決方式的話,指教一下。