1. 程式人生 > >springboot 2.0+mybatis+hikari/druid+atlas+mysql 配置多資料來源及遇到的坑

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都反應了。希望有合理的解決方式。如果各位看到這裡的同行,有好的解決方式的話,指教一下。