MybatisPlus分库源码分析

基于MybatisPlus分库源码分析

分库实现基于spring的也有,但是实现的逻辑本质是一样的。本次分析基于苞米豆提供的程序作出总结。

一、核心的jar 【dynamic-datasource-spring-boot-starter-XXX.jar】

其实分库本质就是获取自己想要的数据源,正常业务开发就是spring帮助我们注入DataSource(比如 druidDataSource,HikariDataSource等等)。所以为了解决分库肯定要封装一下DataSource,变相的在这个封装类上逻辑处理一下。基于这个思路DynamicRoutingDataSource就来了。只要是DataSource 我们就关心getConnection,看下这个DataSource获取数据库连接代码。

public Connection getConnection() throws SQLException {
    String xid = TransactionContext.getXID();
    if (StringUtils.isEmpty(xid)) {
        return determineDataSource().getConnection();
    } else {
        String ds = DynamicDataSourceContextHolder.peek();
        ds = StringUtils.isEmpty(ds) ? "default" : ds;
        ConnectionProxy connection = ConnectionFactory.getConnection(ds);
        return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
    }
}

上述两个核心代码。

1、DynamicDataSourceContextHolder.peek();

2、determineDataSource().getConnection()

我们先看下 determineDataSource()函数。

@Override
public DataSource determineDataSource() {
    // DynamicDataSourceContextHolder.peek(); 这个很重要,这个数据源key是从上下文获取的,所以业务代码必须在上下文放入数据源的key,这个key到底是啥?其实在后面创建数据源的时候会看到key是怎么来的
    String dsKey = DynamicDataSourceContextHolder.peek();
    return getDataSource(dsKey);
}
// getDataSource(dsKey)
public DataSource getDataSource(String ds) {
    if (StringUtils.isEmpty(ds)) {
        // 如果ds 为空获取主库
        return determinePrimaryDataSource(); 
    } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
        log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
        return groupDataSources.get(ds).determineDataSource();
    } else if (dataSourceMap.containsKey(ds)) {
        log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
        return dataSourceMap.get(ds);
    }
    if (strict) {
        throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
    }
    return determinePrimaryDataSource();
}
​
// 获取主库的 determinePrimaryDataSource
//private String primary = "master";
private DataSource determinePrimaryDataSource() {
    log.debug("dynamic-datasource switch to the primary datasource");
    //你会发现,ds为空时候,给了一个默认值为 master 。在这里会发现,所有的数据源都会放在这个 dataSourceMap里
    DataSource dataSource = dataSourceMap.get(primary);
    if (dataSource != null) {
        return dataSource;
    }
    GroupDataSource groupDataSource = groupDataSources.get(primary);
    if (groupDataSource != null) {
        return groupDataSource.determineDataSource();
    }
    throw new CannotFindDataSourceException("dynamic-datasource can not find primary datasource");
}

通过上述分析 自定义的数据源都放在 dataSourceMap里,之前datasource都是spring帮我们注入到容器里,多数据源的时候数据源会放在 这个dataSourceMap 里,那这个dataSourceMap 是什么时候完成数据源装载呢????而且这个map的key很重要,因为后续业务代码会通过key去获取想要的数据源。看下面这段话描述。

// DynamicDataSourceContextHolder.peek(); 这个很重要,这个数据源key是从上下文获取的,所以业务代码必须在上下文放入数据源的key,这个key到底是啥?其实在后面创建数据源的时候会看到key是怎么来的

二、多数据源装载 dataSourceMap

回到我们万物开头DynamicRoutingDataSource,由于这个类实现了InitializingBean,当你将这个bean注入时候,会执行afterPropertiesSet方法。

@Override
public void afterPropertiesSet() throws Exception {
    // 检查开启了配置但没有相关依赖
    checkEnv();
    // 添加并分组数据源
    Map<String, DataSource> dataSources = new HashMap<>(16);
    for (DynamicDataSourceProvider provider : providers) {
        dataSources.putAll(provider.loadDataSources());
    }
    for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
        addDataSource(dsItem.getKey(), dsItem.getValue());
    }
    // 检测默认数据源是否设置
    if (groupDataSources.containsKey(primary)) {
        log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
    } else if (dataSourceMap.containsKey(primary)) {
        log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
    } else {
        log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
    }
}

这个函数核心代码:

for (DynamicDataSourceProvider provider : providers) {
    dataSources.putAll(provider.loadDataSources());
}

DynamicDataSourceProvider这个接口有一个loadDataSources()函数,我们看下他的实现都有哪些?

1、com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider

2、com.baomidou.dynamic.datasource.provider.YmlDynamicDataSourceProvider

我们看 AbstractJdbcDataSourceProvider 实现,YmlDynamicDataSourceProvider这个就是YML数据源提供者,就没必要分析了。

AbstractJdbcDataSourceProvider

@Override
    public Map<String, DataSource> loadDataSources() {
        Connection conn = null;
        Statement stmt = null;
        try {
            // 由于 SPI 的支持,现在已无需显示加载驱动了
            // 但在用户显示配置的情况下,进行主动加载
            if (!StringUtils.isEmpty(driverClassName)) {
                Class.forName(driverClassName);
                log.info("成功加载数据库驱动程序");
            }
            conn = DriverManager.getConnection(url, username, password);
            log.info("成功获取数据库连接");
            stmt = conn.createStatement();
            //扩展口
            Map<String, DataSourceProperty> dataSourcePropertiesMap = executeStmt(stmt);
            return createDataSourceMap(dataSourcePropertiesMap);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JdbcUtils.closeConnection(conn);
            JdbcUtils.closeStatement(stmt);
        }
        return null;
    }
​
    /**
     * 执行语句获得数据源参数
     *
     * @param statement 语句
     * @return 数据源参数
     * @throws SQLException sql异常
     */
    protected abstract Map<String, DataSourceProperty> executeStmt(Statement statement)
            throws SQLException;

可以看出这个类里面给大家留了一个扩展口executeStmt,你返回一个Map<String, DataSourceProperty> 后,它会帮你创建 dataSource ,createDataSourceMap这个函数就是解决数据源创建过程,从这里我们可以看出来了数据源构建是在这个地方。

三、DataSource创建

protected Map<String, DataSource> createDataSourceMap(
    Map<String, DataSourceProperty> dataSourcePropertiesMap) {
    Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
    for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
        String dsName = item.getKey();
        DataSourceProperty dataSourceProperty = item.getValue();
        String poolName = dataSourceProperty.getPoolName();
        if (poolName == null || "".equals(poolName)) {
            poolName = dsName;
        }
        dataSourceProperty.setPoolName(poolName);
        dataSourceMap.put(dsName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
    }
    return dataSourceMap;
}

1、核心代码

defaultDataSourceCreator.createDataSource(dataSourceProperty)
//  defaultDataSourceCreator核心类
@Slf4j
@Setter
public class DefaultDataSourceCreator {
​
    private List<DataSourceCreator> creators;
​
    public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
        DataSourceCreator dataSourceCreator = null;
        for (DataSourceCreator creator : this.creators) {
            if (creator.support(dataSourceProperty)) {
                dataSourceCreator = creator;
                break;
            }
        }
        if (dataSourceCreator == null) {
            throw new IllegalStateException("creator must not be null,please check the DataSourceCreator");
        }
        return dataSourceCreator.createDataSource(dataSourceProperty);
    }
​
}

上面我们可以发现 List<DataSourceCreator> creators 这个数据源创建器。

DataSourceCreator 这个创建器实现会有一个
    boolean support(DataSourceProperty dataSourceProperty);这个函数很重要。那我们可以看一下有哪些创建器。

这些创建器的注入是在 com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceCreatorAutoConfiguration

@Primary
    @Bean
    @ConditionalOnMissingBean
    public DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSourceCreators) {
        DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();
        defaultDataSourceCreator.setCreators(dataSourceCreators);
        return defaultDataSourceCreator;
    }
​
    @Bean
    @Order(DEFAULT_ORDER)
    public BasicDataSourceCreator basicDataSourceCreator() {
        return new BasicDataSourceCreator();
    }
​
    @Bean
    @Order(JNDI_ORDER)
    public JndiDataSourceCreator jndiDataSourceCreator() {
        return new JndiDataSourceCreator();
    }
​
    /**
     * 存在Druid数据源时, 加入创建器
     */
    @ConditionalOnClass(DruidDataSource.class)
    @Configuration
    static class DruidDataSourceCreatorConfiguration {
​
        @Bean
        @Order(DRUID_ORDER)
        public DruidDataSourceCreator druidDataSourceCreator() {
            return new DruidDataSourceCreator();
        }
    }
​
    /**
     * 存在Hikari数据源时, 加入创建器
     */
    @ConditionalOnClass(HikariDataSource.class)
    @Configuration
    static class HikariDataSourceCreatorConfiguration {
        @Bean
        @Order(HIKARI_ORDER)
        public HikariDataSourceCreator hikariDataSourceCreator() {
            return new HikariDataSourceCreator();
        }
    }
​
    /**
     * 存在BeeCp数据源时, 加入创建器
     */
    @ConditionalOnClass(BeeDataSource.class)
    @Configuration
    static class BeeCpDataSourceCreatorConfiguration {
​
        @Bean
        @Order(BEECP_ORDER)
        public BeeCpDataSourceCreator beeCpDataSourceCreator() {
            return new BeeCpDataSourceCreator();
        }
    }
​
    /**
     * 存在Dbcp2数据源时, 加入创建器
     */
    @ConditionalOnClass(BasicDataSource.class)
    @Configuration
    static class Dbcp2DataSourceCreatorConfiguration {
​
        @Bean
        @Order(DBCP2_ORDER)
        public Dbcp2DataSourceCreator dbcp2DataSourceCreator() {
            return new Dbcp2DataSourceCreator();
        }
    }

看到这我想大家就可以明白了,我就不赘述了。

四、总结

总结分库的逻辑调用: 当你注入1、DynamicRoutingDataSource这个DataSource Bean对象的时候,2、会调用com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider 里面loadDataSources函数,3、这里给大家提供一个扩展函数com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider.executeStmt(Statement)去加载DataSource配置信息。然后通过createDataSourceMap(dataSourcePropertiesMap);数据库连接信息创建了DataSource对象,返回的这个Map会保存在DynamicRoutingDataSource dataSourceMap属性里,这样后续你发sql请求时候,获取getConnection会通过这个map获取你想要的DataSource的Connection。

五、利用扩展口实现

按照上述总结操作步骤如下:

1、向IOC注入DynamicRoutingDataSource Bean对象

@Bean
@Primary
public DataSource dataSource(ChaoticSummerTenantProperties tenantProperties){
    DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
    dataSource.setSeata(tenantProperties.isSeata());
    return dataSource;
}

2、实现 com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider 类,并重写executeStmt函数

public class TenantJdbcDataSourceProvider extends AbstractJdbcDataSourceProvider {
​
    private final String driverClassName;
    private final String url;
    private final String username;
    private final String password;
    private final DynamicDataSourceProperties dynamicDataSourceProperties;
​
    private static final String DYNAMIC_QUERY_SQL = "SELECT tenant_id as tenantId, driver_class as driverClass, url, username, password from sys_tenant tenant LEFT JOIN sys_datasource datasource ON tenant.datasource_id = datasource.id WHERE tenant.delete_flag = 0";
​
    public TenantJdbcDataSourceProvider(DynamicDataSourceProperties dynamicDataSourceProperties,String driverClassName, String url, String username, String password) {
        super(driverClassName, url, username, password);
        this.driverClassName = driverClassName;
        this.url = url;
        this.password = password;
        this.username = username;
        this.dynamicDataSourceProperties = dynamicDataSourceProperties;
​
    }
​
    @Override
    protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException {
        Map<String, DataSourceProperty> dataSourceProperties = new HashMap(16);
        DataSourceProperty masterProperty = new DataSourceProperty();
        masterProperty.setDriverClassName(this.driverClassName);
        masterProperty.setUrl(this.url);
        masterProperty.setUsername(this.username);
        masterProperty.setPassword(this.password);
        masterProperty.setDruid(this.dynamicDataSourceProperties.getDruid());
        //dynamicDataSourceProperties 这里考虑到基于yaml配置了DataSource,大家自己玩的时候可以删除这块代码,稍微改在一下这个executeStmt 函数实现,可以简单点
        dataSourceProperties.put(this.dynamicDataSourceProperties.getPrimary(), masterProperty);
        Map<String, DataSourceProperty> datasource = this.dynamicDataSourceProperties.getDatasource();
        if (datasource.size() > 0) {
            dataSourceProperties.putAll(datasource);
        }
​
        ResultSet rs = statement.executeQuery(DYNAMIC_QUERY_SQL);
​
        while(rs.next()) {
            String tenantId = rs.getString("tenantId");
            String driver = rs.getString("driverClass");
            String url = rs.getString("url");
            String username = rs.getString("username");
            String password = rs.getString("password");
            if (StringUtils.isNoneBlank(new CharSequence[]{tenantId, driver, url, username, password})) {
                DataSourceProperty jdbcProperty = new DataSourceProperty();
                jdbcProperty.setDriverClassName(driver);
                jdbcProperty.setUrl(url);
                jdbcProperty.setUsername(username);
                jdbcProperty.setPassword(password);
                jdbcProperty.setDruid(this.dynamicDataSourceProperties.getDruid());
                dataSourceProperties.put(tenantId, jdbcProperty);
            }
        }
​
        return dataSourceProperties;
    }

分析 dataSourceProperties.put(tenantId, jdbcProperty);大家会发现这个tenantId就是我用来区分后续业务用那个数据源,前面我们说过:

DynamicDataSourceContextHolder.peek(); 这个很重要,这个数据源key是从上下文获取的,所以业务代码必须在上下文放入数据源的key,这个key到底是啥?其实在后面创建数据源的时候会看到key是怎么来的 ,在这里我们就呼应到前面了。

我这个类写的有点复杂其实核心就是

DataSourceProperty masterProperty = new DataSourceProperty();
masterProperty.setDriverClassName(this.driverClassName);
masterProperty.setUrl(this.url);
masterProperty.setUsername(this.username);
masterProperty.setPassword(this.password);
masterProperty.setDruid(this.dynamicDataSourceProperties.getDruid());
// this.dynamicDataSourceProperties.getPrimary() 这个其实就是主数据源的key 【master】,主数据源的初始化其实依赖于配置文件。
dataSourceProperties.put(this.dynamicDataSourceProperties.getPrimary(), masterProperty);
​
ResultSet rs = statement.executeQuery(DYNAMIC_QUERY_SQL);
​
while(rs.next()) {
    String tenantId = rs.getString("tenantId");
    String driver = rs.getString("driverClass");
    String url = rs.getString("url");
    String username = rs.getString("username");
    String password = rs.getString("password");
    if (StringUtils.isNoneBlank(new CharSequence[]{tenantId, driver, url, username, password})) {
        DataSourceProperty jdbcProperty = new DataSourceProperty();
        jdbcProperty.setDriverClassName(driver);
        jdbcProperty.setUrl(url);
        jdbcProperty.setUsername(username);
        jdbcProperty.setPassword(password);
        jdbcProperty.setDruid(this.dynamicDataSourceProperties.getDruid());
        dataSourceProperties.put(tenantId, jdbcProperty);
    }
}

将自定义的TenantJdbcDataSourceProvider注入到IOC里

@Primary
@Bean
public DynamicDataSourceProvider jdbcDynamicDataSourceProvider(DataSourceProperties dataSourceProperties, DynamicDataSourceProperties dynamicDataSourceProperties){
    String driverClassName = dataSourceProperties.getDriverClassName();
    String url = dataSourceProperties.getUrl();
    String username = dataSourceProperties.getUsername();
    String password = dataSourceProperties.getPassword();
    DataSourceProperty master = dynamicDataSourceProperties.getDatasource().get(dynamicDataSourceProperties.getPrimary());
    if (master != null) {
        driverClassName = master.getDriverClassName();
        url = master.getUrl();
        username = master.getUsername();
        password = master.getPassword();
    }
    return new TenantJdbcDataSourceProvider(dynamicDataSourceProperties,driverClassName,url,username,password);
}

下一步就是在发sql的业务代码之前要在 DynamicDataSourceContextHolder上下文调用push函数

public static String push(String ds) {
        String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
        LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
        return dataSourceStr;
    }

如果没有传入会调用master数据源,通常这块会写入一盒拦截器,因为业务里肯定有的表不需要分库,这块拦截器代码就不说了,大家可以自行编写一个。【如果需要这块代码,可以留言。】。主要拦截器后面要释放 上下文的数据,调用poll函数。

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值