前言
最近做了一个数据源平台,主要功能是它可以存在多个数据源在一个工程下,并且在执行sql时能找到正确的数据源切换并执行。也就是动态数据源切换执行。
原工程使用的是springJdbc + druid进行的数据源管理,因此数据源获取结构发生了如下改变:
核心代码就是实现 AbstractRoutingDataSource类determineCurrentLookupKey()方法。
原来工程使用的数据源也要被AbstractRoutingDataSource管理,在这里暂且称为主数据源用于区分。
依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>12.2.0.1</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>9.4.1.jre8</version>
</dependency>
源码
AbstractRoutingDataSource类中实现的getConnection()方法
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
调用了determineTargetDataSource(),其内部又调用了唯一抽象方determineCurrentLookupKey(),因此得知这个方法需要返回具体key值,然后才能从resolvedDataSources这个map中取到DataSource,从而获取数据库连接。
private Map<Object, DataSource> resolvedDataSources;
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource 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 + "]");
} else {
return dataSource;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
代码
- 创建本地线程维护类,用于切换数据源
public class RoutingDataSourceContext {
private static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();
/**
* 获取主数据库的key
*
* @return
*/
public static String getMainKey() {
return "mainDB";
}
/**
* 获取数据库key
*
* @return
*/
public static String getDataSourceRoutingKey() {
String key = threadLocalDataSourceKey.get();
return key == null ? getMainKey() : key;
}
/**
* 设置数据库的key
*
* @param key
*/
public static void setThreadLocalDataSourceKey(String key) {
threadLocalDataSourceKey.set(key);
}
}
- 自定义类RoutingDataSource ,继承AbstractRoutingDataSource
@Configuration
@MapperScan("com.your.package.mapper")
public class RoutingDataSource extends AbstractRoutingDataSource
- 创建sqlSession工厂。这里一定要把this传入dataSource
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.apache.ibatis.session.SqlSessionFactory;
@Configuration
@MapperScan("com.your.package.mapper")
public class RoutingDataSource extends AbstractRoutingDataSource {
private Map<Object, Object> dataSourcesMap = new ConcurrentHashMap<>();
public RoutingDataSource() {
log.info("初始化动态数据源");
//这里是指定用什么容器去存所有路由的数据源,当前使用ConcurrentHashMap
super.setTargetDataSources(dataSourcesMap);
}
@Resource
private GlobalConfig globalConfig;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(this);
factoryBean.setConfigLocation(new ClassPathResource("mybatis/mybatis-config.xml"));
//读取mapper.xml
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("mapper/*.xml"));
factoryBean.setTypeAliasesPackage("com.your.package.po");
factoryBean.setGlobalConfig(globalConfig);
return factoryBean.getObject();
}
}
- 实现determineCurrentLookupKey()方法
@Override
//本地线程没有则使用主数据源
protected Object determineCurrentLookupKey() {
String dbSourceKey = RoutingDataSourceContext.getDataSourceRoutingKey();
if (StringUtils.isEmpty(dbSourceKey)) {
dbSourceKey = RoutingDataSourceContext.getMainKey();
}
log.info("当前操作数据源ID:{}", dbSourceKey);
if (!dataSourcesMap.containsKey(dbSourceKey)) {
log.info("{}数据源不存在, 创建对应的数据源", dbSourceKey);
createAndSaveDataSource(dbSourceKey);
} else {
log.info("{}数据源已存在不需要创建", dbSourceKey);
}
log.info("切换到{}数据源", dbSourceKey);
return dbSourceKey;
}
private synchronized void createAndSaveDataSource(String dbSourceKey) {
DruidDataSource dataSource;
if (dbSourceKey.equals(RoutingDataSourceContext.getMainKey())) {
//创建主数据源,主数据源的durid配置要区别于其他
dataSource = getMainDBSource();
//设置默认的数据源
super.setDefaultTargetDataSource(dataSource);
//加入本地容器中
dataSourcesMap.put(dbSourceKey, dataSource);
} else {
dataSource = createDruidDataSource(dbSourceKey);
//在执行过程中不必调用druid.init(),在获取链接时自动调用
log.info("{}数据源创建成功", dbSourceKey);
//加入本地容器中
dataSourcesMap.put(dbSourceKey, dataSource);
//设置路由容器
super.setTargetDataSources(dataSourcesMap);
}
//对创建的数据源初始化
afterPropertiesSet();
}
private static DruidDataSource createDruidDataSource(DynamicDataSourcePO dataSourceBean) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(dataSourceBean.getUrl());
dataSource.setUsername(dataSourceBean.getUsername());
dataSource.setPassword(dataSourceBean.getPassword());
dataSource.setInitialSize(1);
dataSource.setMaxActive(2);
dataSource.setMinIdle(1);
dataSource.setTestOnBorrow(false);
dataSource.setValidationQuery("select 1");
// 如果连接空闲超过1小时就断开
dataSource.setMinEvictableIdleTimeMillis(60000 * 60);
// 每十分钟验证一下连接
dataSource.setTimeBetweenEvictionRunsMillis(600000);
// 运行ilde链接测试线程,剔除不可用的链接
dataSource.setTestWhileIdle(true);
dataSource.setMaxWait(5000);
return dataSource;
}
- 使用时先设置当前需要用到的数据源的key值,然后执行sql就可以了
public List<Map<String, Object>> doQuery(String dbid, String sql) {
//切换
RoutingDataSourceContext.setThreadLocalDataSourceKey(dbid);
//执行
return dynamicDataSourceMapper.query(sql);
}
问题
ConcurrentHashMap随着使用增多而导致内存、线程等开销增大,有没有固定容量的map作为容器?
答案是有的,LinkedHashMap就可以控制
public class DynamicDataSourceContainer extends LinkedHashMap<Object, Object> {
//返回true就是移除第1个插入的元素,FIFO策略
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
//eldest 为首个元素
final int maxSize = 50;
boolean b = size() > maxSize;
if (b) {
//要移除之前,释放资源
DruidDataSource dataSource = (DruidDataSource) eldest.getValue();
dataSource.close();
}
return b;
}
}
//private Map<Object, Object> dataSourcesMap = new ConcurrentHashMap<>();
//替换为
private Map<Object, Object> dataSourcesMap = new DynamicDataSourceContainer();
这样又会产生新的问题,就是主数据源会被移除,那么如何保证主数据源不被移除呢?
public class DynamicDataSourceContainer extends LinkedHashMap<Object, Object> {
//主数据源的临时变量
private static volatile Object tempDs;
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
//eldest 为首个元素
final int maxSize = 50;
boolean b = size() > maxSize;
if (b) {
if (DataSourceTypeConstant.mainDB.equals(eldest.getKey())) {
//先缓存下来
tempDs = super.get(DataSourceTypeConstant.mainDB);
//移除,让出容器大小
super.remove(DataSourceTypeConstant.mainDB);
//重新添加到末尾
super.put(DataSourceTypeConstant.mainDB, tempDs);
//不移除首个元素
return false;
} else {
DruidDataSource dataSource = (DruidDataSource) eldest.getValue();
dataSource.close();
}
}
return b;
}
}