springboot 动态数据源(Mybatis+Druid)

栏目: Java · 发布时间: 6年前

内容简介:Spring多数据源实现的方式大概有2中,一种是新建多个​ 第1种方式虽然实现比较加单,劣势就是不同数据源的mapper文件不能在同一包名,就显得不太灵活了。所以为了更加灵活的作为一个组件的存在,作者采用的第二种方式实现。

Spring多数据源实现的方式大概有2中,一种是新建多个 MapperScan 扫描不同包,另外一种则是通过继承 AbstractRoutingDataSource 实现动态路由。今天作者主要基于后者做的实现,且方式1的实现比较简单这里不做过多探讨。

实现方式

方式1的实现(核心代码):

@Configuration
@MapperScan(basePackages = "com.goofly.test1", sqlSessionTemplateRef  = "test1SqlSessionTemplate")
public class DataSource1Config1 {

    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.test1")
    @Primary
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }
    // .....略

}

@Configuration
@MapperScan(basePackages = "com.goofly.test2", sqlSessionTemplateRef  = "test1SqlSessionTemplate")
public class DataSourceConfig2 {

    @Bean(name = "dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.test2")
    @Primary
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }
    // .....略

}

方式2的实现(核心代码):

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    private static final Logger log = Logger.getLogger(DynamicRoutingDataSource.class);
    
    @Override
    protected Object determineCurrentLookupKey() {
        //从ThreadLocal中取值
        return DynamicDataSourceContextHolder.get();
    }
}

​ 第1种方式虽然实现比较加单,劣势就是不同数据源的mapper文件不能在同一包名,就显得不太灵活了。所以为了更加灵活的作为一个组件的存在,作者采用的第二种方式实现。

设计思路

springboot 动态数据源(Mybatis+Druid)

  1. 当请求经过被注解修饰的类后,此时会进入到切面逻辑中。
  2. 切面逻辑会获取注解中设置的key值,然后将该值存入到 ThreadLocal
  3. 执行完切面逻辑后,会执行 AbstractRoutingDataSource.determineCurrentLookupKey() 方法,然后从 ThreadLocal 中获取之前设置的key值,然后将该值返回。
  4. 由于 AbstractRoutingDataSourcetargetDataSources 是一个map,保存了数据源key和数据源的对应关系,所以能够顺利的找到该对应的数据源。

源码解读

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource ,如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    
    private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    private Map<Object, DataSource> resolvedDataSources;
    private DataSource resolvedDefaultDataSource;
    
        protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * <p>Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    protected abstract Object determineCurrentLookupKey();
    
    //........略

targetDataSources 是一个map结构,保存了key与数据源的对应关系;

dataSourceLookup 是一个 DataSourceLookup 类型,默认实现是 JndiDataSourceLookup 。点开该类源码会发现,它实现了通过key获取DataSource的逻辑。当然,这里可以通过 setDataSourceLookup() 来改变其属性,因为关于此处有一个坑,后面会讲到。

public class JndiDataSourceLookup extends JndiLocatorSupport implements DataSourceLookup {

    public JndiDataSourceLookup() {
        setResourceRef(true);
    }

    @Override
    public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException {
        try {
            return lookup(dataSourceName, DataSource.class);
        }
        catch (NamingException ex) {
            throw new DataSourceLookupFailureException(
                    "Failed to look up JNDI DataSource with name '" + dataSourceName + "'", ex);
        }
    }

}

配置示例

多数据源

# db1
spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.master.username = root
spring.datasource.master.password = 123456
spring.datasource.master.driverClassName = com.mysql.jdbc.Driver
spring.datasource.master.validationQuery = true
spring.datasource.master.testOnBorrow = true
## db2
spring.datasource.slave.url = jdbc:mysql://127.0.0.1:3306/test1?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.slave.username = root
spring.datasource.slave.password = 123456
spring.datasource.slave.driverClassName = com.mysql.jdbc.Driver
spring.datasource.slave.validationQuery = true
spring.datasource.slave.testOnBorrow = true

#主数据源名称
spring.maindb=master
#mapperper包路径
mapper.basePackages =com.btps.xli.multidb.demo.mapper
### 单数据源

为了让使用者能够用最小的改动实现最好的效果,作者对单数据源的多种配置做了兼容。

配置1:

spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false

spring.datasource.master.username = root

spring.datasource.master.password = 123456

spring.datasource.master.driverClassName = com.mysql.jdbc.Driver

spring.datasource.master.validationQuery = true

spring.datasource.master.testOnBorrow = true

mapper包路径

mapper.basePackages = com.goofly.xli.multidb.demo.mapper

主数据源名称

spring.maindb=master

配置2

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false

spring.datasource.username = root

spring.datasource.password = 123456

spring.datasource.driverClassName = com.mysql.jdbc.Driver

spring.datasource.validationQuery = true

spring.datasource.testOnBorrow = true

mapper包路径

mapper.basePackages = com.goofly.xli.multidb.demo.mapper

## 踩坑之路

### 多数据源的循环依赖

Description:

The dependencies of some of the beans in the application context form a cycle:

happinessController (field private com.db.service.HappinessService com.db.controller.HappinessController.happinessService)

happinessServiceImpl (field private com.db.mapper.MasterDao com.db.service.HappinessServiceImpl.masterDao)

masterDao defined in file [E:GitRepositoryframework-graytest-dbtargetclassescomdbmapperMasterDao.class]

sqlSessionFactory defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]

┌─────┐

| dynamicDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]

↑ ↓

| firstDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]

↑ ↓

| dataSourceInitializer

**解决方案:**

在Spring boot启动的时候排除`DataSourceAutoConfiguration`即可。如下:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

public class DBMain {

public static void main(String[] args) {
    SpringApplication.run(DBMain.class, args);
}

}

> ​     但是作者在创建多数据源的时候由于并未创建多个`DataSource`的Bean,而是只创建了一个即需要做动态数据源的那个Bean。 其他的`DataSource`则直接创建实例然后存放在Map里面,然后再设置到`DynamicRoutingDataSource#setTargetDataSources`即可。
>
>    因此这种方式也不会出现循环依赖的问题!



### 动态刷新数据源

> ​     笔者在设计之初是想构建一个动态刷新数据源的方案,所以利用了`SpringCloud`的`@RefreshScope`去标注数据源,然后利用`RefreshScope#refresh`实现刷新。但是在实验的时候发现由Druid创建的数据源会因此而关闭,由Spring的`DataSourceBuilder`创建的数据源则不会发生任何变化。
>
> ​    最后关于此也没能找到解决方案。同时思考,如果只能的可以实现动态刷新的话,那么数据源的原有连接会因为刷新而中断吗还是会有其他处理?



### 多数据源事务

> ​    有这么一种特殊情况,一个事务中调用了两个不同数据源,这个时候动态切换数据源会因此而失效。
>
> 翻阅了很多文章,大概找了2中解决方案,一种是Atomikos进行事务管理,但是貌似性能并不是很理想。
>
>   另外一种则是通过优先级控制,切面的的优先级必须要大于数据源的优先级,用注解`@Order`控制。
>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Ajax Design Patterns

Ajax Design Patterns

Michael Mahemoff / O'Reilly Media / 2006-06-29 / USD 44.99

Ajax, or Asynchronous JavaScript and XML, exploded onto the scene in the spring of 2005 and remains the hottest story among web developers. With its rich combination of technologies, Ajax provides a s......一起来看看 《Ajax Design Patterns》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具