天天看点

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都反应了。希望有合理的解决方式。如果各位看到这里的同行,有好的解决方式的话,指教一下。

继续阅读