基于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函数。