spring boot mybatis 多数据源

作者: staconfree | 来源:发表于2017-12-01 10:08 被阅读65次

    在实际开发中,我们一个项目可能会用到多个数据库,通常一个数据库对应一个数据源。

    在spring boot项目中,系统默认会自动在applicationContext中注册一个dataSource的bean,如果我们自己定义一个DataSource.class的实例,则会覆盖这个bean。但是如果我们定义多个DataSource.class的实例,则启动会提示实例化mapper的时候发现了多个datasource,导致启动失败。

    我们先来看看单数据源的配置案例:

    1、单数据源情况

    1.1、MyBatisConfiguration

    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.context.annotation.Bean;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    
    @Configuration
    @ConditionalOnClass({EnableTransactionManagement.class})
    @MapperScan(basePackages={"com.roy.**.mapper"})
    public class MyBatisConfiguration {
    
        @Autowired
        private DataSource dataSource;
    
        public DataSource dataSource() {
            return dataSource;
        }
    
        @Bean(name = "sqlSessionFactory")
        public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
    
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSource());
    
            PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    
            sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mybatis/**/*.xml"));
            org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
            configuration.setMapUnderscoreToCamelCase(true);
            sqlSessionFactoryBean.setConfiguration(configuration);
            return sqlSessionFactoryBean.getObject();
        }
    
        @Bean
        public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }
    
        @Bean(name = "transactionManager")
        public PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }
    
    
    }
    

    1.2、application.properties

    # 数据库访问配置
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver
    spring.datasource.url=jdbc:oracle:thin:@//127.0.0.1:1521/testdb
    spring.datasource.username=test
    spring.datasource.password=test
    # 下面为连接池的补充设置,应用到上面所有数据源中
    # 初始化大小,最小,最大
    spring.datasource.initialSize=5
    spring.datasource.minIdle=5
    spring.datasource.maxActive=20
    # 配置获取连接等待超时的时间
    spring.datasource.maxWait=60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    spring.datasource.timeBetweenEvictionRunsMillis=60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    spring.datasource.minEvictableIdleTimeMillis=300000
    spring.datasource.validationQuery=SELECT 1 FROM DUAL
    spring.datasource.testWhileIdle=true
    spring.datasource.testOnBorrow=false
    spring.datasource.testOnReturn=false
    # 打开PSCache,并且指定每个连接上PSCache的大小
    spring.datasource.poolPreparedStatements=true
    spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
    # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    spring.datasource.filters=stat,wall,log4j
    # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
    spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    # 合并多个DruidDataSource的监控数据
    #spring.datasource.useGlobalDataSourceStat=true
    

    下面我们再看看如何改造成多数据源
    多数据源的主要实现原理是重写DataSource接口的实现,重写getConnection()和unwrap()方法,在这里实现对多数据源datasource的选择切换,并注册给SqlSessionFactory和PlatformTransactionManager
    我们参考AbstractRoutingDataSource类,发现里面已经支持了路由多个datasource的功能,我们只需要实现protected abstract Object determineCurrentLookupKey();方法来切换datasource就可以。
    为此我们参考网上例子,对上面的单数据源做如下调整,以支持多数据源。

    2、多数据源情况

    2.1、新建DynamicDataSource类继承AbstractRoutingDataSource

    public class DynamicDataSource extends AbstractRoutingDataSource {
        protected Object determineCurrentLookupKey() {
            return DatabaseContextHolder.getDatabaseName();
        }
    }
    

    2.2、新建DatabaseContextHolder,利用线程变量保存当前数据源的key值(此处我们使用dataSource实例的beanName作为key值)

    public class DatabaseContextHolder {
    
        private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
        public static void setDatabaseName(String type){
            contextHolder.set(type);
        }
    
        public static String getDatabaseName(){
             return contextHolder.get();
        }
    
        public static void clear() {
            contextHolder.remove();
        }
    
    }
    

    2.3、改造上面的MyBatisConfiguration类,重写 dataSource() 方法

        @Autowired
        private DataSource dataSource;
    
        public Map<String, DataSource> otherDataSources() {
            return null;
        }
    
        public DataSource dataSource() {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put("dataSource", dataSource);
            if (otherDataSources()!=null) {
                for (String key : otherDataSources().keySet()) {
                    targetDataSources.put(key, otherDataSources().get(key));
                }
            }
            dynamicDataSource.setTargetDataSources(targetDataSources);
            dynamicDataSource.setDefaultTargetDataSource(dataSource);
            dynamicDataSource.afterPropertiesSet();
            return dynamicDataSource;
        }
    

    注意:这里我没有采用网上的实例化多个DataSource.class的bean到applicationContext的方式,因为经我实际验证发现,但凡applicationContext里面有多个DataSource.class的bean,生产mapper的bean的时候都会报错(也许是我的架构上不知道哪里有些限制)。所以我这里采用了特殊的方式,增加了一个
    public Map<String, DataSource> otherDataSources() {
    return null;
    }
    的方法,如果有多个DataSource,在这里面自己new 出来,并且不注入到applicationContext里面。

    2.4、如何自己实例化DataSource,参考下面这个,这里我们采用DruidDataSource

        @Value("${second.datasource.url}")
        private String dbUrl;
        @Value("${second.datasource.username}")
        private String username;
        @Value("${second.datasource.password}")
        private String password;
        @Autowired
        protected DataSourceProperties dataSourceProperties;
    
        public static final String DATASOURCE_SECOND_KEY="secondDataSource";
    
        public Map<String, DataSource> otherDataSources() {
            Map<String, DataSource> map = new HashMap<>();
            map.put(DATASOURCE_SECOND_KEY, secondDataSource());
            return map;
        }
    
        public DataSource secondDataSource() {
            DruidDataSource datasource = new DruidDataSource();
            datasource.setUrl(dbUrl);
            datasource.setUsername(username);
            datasource.setPassword(password);
            datasource.setDriverClassName(dataSourceProperties.getDriverClassName());
            datasource.setInitialSize(dataSourceProperties.getInitialSize());
            datasource.setMinIdle(dataSourceProperties.getMinIdle());
            datasource.setMaxActive(dataSourceProperties.getMaxActive());
            datasource.setMaxWait(dataSourceProperties.getMaxWait());
            datasource.setTimeBetweenEvictionRunsMillis(dataSourceProperties.getTimeBetweenEvictionRunsMillis());
            datasource.setMinEvictableIdleTimeMillis(dataSourceProperties.getMinEvictableIdleTimeMillis());
            datasource.setValidationQuery(dataSourceProperties.getValidationQuery());
            if (dataSourceProperties.getTestWhileIdle()!=null) {
                datasource.setTestWhileIdle(dataSourceProperties.getTestWhileIdle());
            }
            if (dataSourceProperties.getTestOnBorrow()!=null){
                datasource.setTestOnBorrow(dataSourceProperties.getTestOnBorrow());
            }
            if (dataSourceProperties.getTestOnReturn()!=null) {
                datasource.setTestOnReturn(dataSourceProperties.getTestOnReturn());
            }
            if (dataSourceProperties.getPoolPreparedStatements()!=null) {
                datasource.setPoolPreparedStatements(dataSourceProperties.getPoolPreparedStatements());
            }
            if (dataSourceProperties.getMaxPoolPreparedStatementPerConnectionSize()!=null) {
                datasource.setMaxPoolPreparedStatementPerConnectionSize(dataSourceProperties.getMaxPoolPreparedStatementPerConnectionSize());
            }
            if (dataSourceProperties.getConnectionProperties()!=null) {
                datasource.setConnectionProperties(dataSourceProperties.getConnectionProperties());
            }
            if (dataSourceProperties.getUseGlobalDataSourceStat()!=null) {
                datasource.setUseGlobalDataSourceStat(dataSourceProperties.getUseGlobalDataSourceStat());
            }
            try {
                datasource.setFilters(dataSourceProperties.getFilters());
            } catch (SQLException e) {
                logger.error("dataSource configuration initialization filter", e);
            }
            return datasource;
        }
    

    其中DataSourceProperties 类是注入了application.properties的spring.datasource. 的参数

    2.5、application.properties里面加入第二个datasource的配置

    second.datasource.url=jdbc:oracle:thin:@//127.0.0.1:1521/testdb2
    second.datasource.username=test2
    second.datasource.password=test2
    

    2.6、使用方法

    // 访问默认数据源
            City city = cityService.getCityById(id,null);
    // 以下是访问第二个数据源
            DatabaseContextHolder.setDatabaseName(MyBatisConfiguration.DATASOURCE_SECOND_KEY);
            city = cityService.getCityById(id,null);
            DatabaseContextHolder.clear();
    

    如上,我们在两个库都建立一张city表,都配置一条cityId=1的记录,第一个库,cityName=深圳,第二个库,cityName=洛杉矶。
    经过上面的两次请求,返回的cityName结果如我们预料,说明数据源已经做了正常切换。
    注意:每次切换DataSource之后记得用DatabaseContextHolder.clear();方法把线程变量清空。

    后续

    1、上面写的,如果applicationContext里面有多个DataSource.class的bean会导致启动时生成mapper时报错。后面发现如果在其中一个DataSource的bean上加上@Primary注解就可以了
    2、以上覆盖了dataSource()方法,返回的DataSource的实例是DynamicDataSource的实例,这样会导致整个项目的事务失效,所以如果系统有需要事务的地方,要慎重使用多数据源配置,多数据源比较适合的场景是数据分析,大部分都是查询逻辑,整合不同库的数据。
    3、以上方式只支持SqlSessionTemplate的查询,但是这种查询一定要对应有mapper的sqlId。如果有需求需要使用自定义的sql进行查询,大多数时候我们会使用jdbcTemplate来查询,但是此时的jdbcTemplate使用的dataSource并不是动态数据源,所以使用jdbcTemplate不能起到切换数据源的效果。为此,可以参考mybatis 最简单的执行自定义SQL语句,原理就是新建一个mapper:

    List<map> select(String sql);
    
    <select id="select" resultType="java.util.Map" parameterType="java.lang.String" >
        ${_parameter}
      </select>
    

    parameterType为String的话 参数名就必须写_parameter,不能用#{sqlStr}这种方式,否则会有sql注入报错。

    参考资料
    第八章 springboot + mybatis + 多数据源
    Spring Boot Druid数据源配置

    相关文章

      网友评论

        本文标题:spring boot mybatis 多数据源

        本文链接:https://www.haomeiwen.com/subject/xqsibxtx.html