一、引言
作者最近的平台项目需要一个功能,数据库是动态的,sql也是动态的,所以需要动态注入数据源,并且能够在运行过程中进行切换数据库。作者在这里分享一下做法,以及Mybatis这样做的原理。
二、分析
接下来分析一下需求,主要是两个点:
1、动态数据源
既然是动态的,那肯定需要在配置中心放数据库。问题是把这堆数据库初始化起来,这个比较大众的做法就是放在多个路径下,每个路径一个数据库一个配置类,这样扫描的类就始终属于某个数据库,清晰解耦,但是这样加数据库就需要发布,而且每加一个库都要加一堆路径,正常情况下是适合的。
但是作者的平台大部分数据库执行的操作都是一样的,这种方式带来的后续改动和发布是没有必要的,所以要把数据源给放在集合里面,只需要改配置就可以加库。
2、运行切换
要是一个路径一个库,没有切换的必要。数据源存在集合里面,就肯定需要切换了,切换正常都是基于aop或者拦截器。这里Mybatis提供了一些工具,基于aop实现更快。
三、实现
先试试简单的,要有个保底,免得第二种踩坑太多时间来不及。
1、路径-库
配置类,这里创建数据源的时候是作者公司的框架做的,正常创建是要在yml里面指定链接、账号、密码
@EnableDalMybatis(encryptParameters = false)
@Configuration
@MapperScan(basePackages = {"com.mapper"},
sqlSessionFactoryRef = "sqlSessionFactory")
public class OneDBConfig {
@Bean
public DataSource dataSource() throws Exception {
return factory.getOrCreateDataSource(DB_KEY);
}
@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean
.setMapperLocations(resolver.getResources(
"classpath*:/com/*.xml"));
sqlSessionFactoryBean.setConfigLocation(resolver.getResource("classpath:/mybatis/mybatis-config.xml"));
return sqlSessionFactoryBean.getObject();
}
}
多少个库就有多少个配置类,这里有个坑在,每个配置类对应的SqlSessionFactory要起别名,不能一样。
SqlSessionFactory用来创建SqlSession的工厂类,SqlSession是MyBatis中用于执行SQL语句的主要对象。数据源塞在这个对象里面,所以数据源和数据源管理器不起名没关系,但是他在外层,得不同。
显然xml和mapper也要有多个对应的目录。
用的时候就是根据db使用不同路径下的mapper
DbEnum dbEnum = DbEnum.getExecuteEnumByValue(db);
switch (dbEnum) {
case ONE:
//
case TWO:
//
default:
return null;
}
2、动态多数据源
继承mybatis的AbstractRoutingDataSource,这个类里面有个方法,是mybatis每次创建链接的时候使用的,根据key找对应的数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceThreadLocal.getDB();
}
}
要使用ThreadLocal存储需要使用的数据源,这样才不会影响其他线程操作数据
public class DataSourceThreadLocal {
public static final String DEFAULT_DB = "default";
private static final ThreadLocal<String> nowDb = new ThreadLocal<>();
public static void setDB(String dbType) {
nowDb.set(dbType);
}
public static String getDB() {
// 如果当前线程没有设置切换数据库,就使用默认数据库
if (nowDb == null || StringUtilsExt.isBlank(nowDb.get())) {
return DEFAULT_DB;
}
return (nowDb.get());
}
public static void clearDB() {
nowDb.remove();
}
}
mybatis提供了一个DynamicDataSource,里面的targetDataSources是一个hashmap,可以存放db和数据初始化的datasource,配置中心放的数据库主要是放了db的key,拿到程序里面反序列化为对象,配置中心里面放json、map、list都行。
@EnableDalMybatis(encryptParameters = false)
@Configuration
@MapperScan(
basePackages = {
"com.mapper"},
sqlSessionFactoryRef = "sqlSessionFactory")
public class CommonQueryDBConfig {
private static final LoggerService LOG = LoggerServiceFactory.getLoggerService(CommonQueryDBConfig.class);
private static final String LOG_TITLE = "CommonQueryDBConfig";
@Primary
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() throws Exception {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
LOG.info(LOG_TITLE, "init data source");
// 数据源可以存放在map里面
Map<Object, Object> dbMap = new HashMap<>();
String s = ConfigurationFunc.getString(QConfigConstants.CREATE_DATA_CONFIG_PARAMETER);
List<DynamicDataSourceConfigBo> dataSourceConfigBos =
JSONUtil.parse(s, new TypeReference<List<DynamicDataSourceConfigBo>>() {});
LOG.info(LOG_TITLE, "dataSourceConfigBos:{}", JSONUtil.toJsonNoException(dataSourceConfigBos));
for (DynamicDataSourceConfigBo ds : dataSourceConfigBos) {
DataSource now = factory.createDataSource(ds.getDbCreateKey());
dbMap.put(ds.getDbCreateKey(), now);
}
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(dsMap.get("corpcodescannerdb_dalcluster"));
dynamicDataSource.setTargetDataSources(dsMap);
LOG.info(LOG_TITLE, "init data source success:{}", dsMap.size());
return dynamicDataSource;
}
@Bean(name = "dynamicTransactionManager")
public DataSourceTransactionManager dynamicTransactionManager() throws Exception {
return new DataSourceTransactionManager(dynamicDataSource());
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean
.setMapperLocations(resolver.getResources(
"classpath*:/com.mapper/*.xml"));
sqlSessionFactoryBean.setConfigLocation(resolver.getResource("classpath:/mybatis/mybatis-config.xml"));
SqlSessionFactory res = sqlSessionFactoryBean.getObject();
return res;
}
}
用起来就是设置一下threadlocal,用完记得清除
DataSourceThreadLocal.setDB(db);
LOG.info(LOG_TITLE, "change datasource:{}", db);
// mapper做什么
LOG.info(LOG_TITLE, "datasource res:{}", JSONUtil.toJsonNoException(res));
DataSourceThreadLocal.clearDB();
return res;
四、原理
1、设计
在分析mybatis的动态数据源原理之前,先思考一下实现需要怎么设计,设计都是大差不离的,有了设计理念才知道看mybatis哪里的代码。
如果自己做,就是要考虑数据库的信息放在哪里,真到了使用的时候其实就是找到数据库的链接地址、账号、密码然后建立网络链接,通过链接进行比特流传输。所以可以分为以下几步:
1、数据库初始化,把数据库的链接地址账号密码存到本地缓存里面去,放在hashmap里面是方便安全的,因为初始化之后没有改动只有取数,而且数据库名称和信息的键值对也方便get。
2、用的时候要传数据库名称,从缓存里面拿数据库信息
3、拿着数据库信息,建立链接
这样看起来主要的点在于找数据库信息和建立连接的时候,那么就可以往这里找mybatis的代码。
2、Mybatis实现
首先要进入他的查询逻辑
这里可以看到获取链接了
在这个本地缓存里面存放了各个数据库的信息以及当前默认的数据库
没有建立过链接就准备开启链接
在工具类里面建立连接
这里已经所有拿到数据库信息了,接下来就是选择哪个进行连接初始化
就是这里拿lookupKey作为键,从map里面拿数据库信息,如果没有设置的话就会拿默认数据源,由此也可以看出ThreadLocal类里面其实不需要给默认值。作者写了是因为便于理解,后面的同事不一定了解这些原理。
这里还有个坑在,之所以mybatis会判断当前链接是否为null,是因为在同一个事务当中链接复用,这时候切换数据库就不生效了。
通常每次查询Mybatis都会执行一次openConnection方法来获取新的数据库连接。这样可以确保每次查询都是在独立的事务中执行,避免数据的混乱和并发问题。
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
五、总结
做法和原理就这些了,还有一些踩的坑忘了,使用有疑问的同学可以评论区交流下。