动态切换数据库
‘壹’ 如何在Spring代码中动态切换数据源
最近项目中遇到一个场景,需要能够在一个方法中操作多个具有相同表结构数据库(你可以理解为一个表中的数据被水平拆分到多个库中,查询时需要遍历这多个库)。经过笔者几天的研究,最终解决了问题,并且写了一个demo共享到我的github。
关注笔者博客的小伙伴一定知道之前的这篇文章点击打开链接,这篇博客中的解决方案仅仅适用读写分离的场景。就是说,当你在开发的时候已经确定了使用写库一读库的形式。笔者今天要写的这篇文章具有普适性,适合所有需要在Spring工程中动态切换数据源的场景,而且本文中的解决方案对工程的代码基本没有侵入性。下面就来说下该方案的实现原理:
在Spring-Mybatis中,有这样一个类AbstractRoutingDataSource,根据名字可以猜到,这是一个框架提供的用于动态选择数据源的类。这个类有两个重要的参数,分别叫
defaultTargetDataSource和targetDataSources。一般的工程都是一个数据源,所以不太接触到这个类。在作者之前的博客自动切换多个数据源中,可以看到这个类的xml配置如下:
[html] view plain
<bean id="myoneDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.myone.driver}"/>
<property name="url" value="${jdbc.myone.url}"/>
<property name="username" value="${jdbc.myone.username}"/>
<property name="password" value="${jdbc.myone.password}"/>
</bean>
<bean id="mytwoDataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.mytwo.driver}"/>
<property name="url" value="${jdbc.mytwo.url}"/>
<property name="username" value="${jdbc.mytwo.username}"/>
<property name="password" value="${jdbc.mytwo.password}"/>
</bean>
<bean id="multipleDataSource" class="dal.datasourceswitch.MultipleDataSource">
<property name="defaultTargetDataSource" ref="myoneDataSource"/> <!--默认主库-->
<property name="targetDataSources">
<map>
<entry key="myone" value-ref="myoneDataSource"/> <!--辅助aop完成自动数据库切换-->
<entry key="mytwo" value-ref="mytwoDataSource"/>
</map>
</property>
</bean>
上面的配置文件对这两个参数的描述已经很清楚了,但这是多个数据源已经确定的场景。我们这篇博客中的场景是多个数据源的信息存在于数据库中,可能数据库中的数据源信息会动态的增加或者减少。这样的话,就不能像上面这样配置了。那怎么办呢?
我们仅仅需要设定默认的数据源,即defaultDataSource参数,至于targetDataSources参数我们需要在代码中动态的设定。来看下具体的xml配置:
[html] view plain
<bean id="defaultDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
p:driverClassName="${db_driver}"
p:url="${db_url}"
p:username="${db_user}"
p:password="${db_pass}"
p:validationQuery="select 1"
p:testOnBorrow="true"/>
<!--动态数据源相关-->
<bean id="dynamicDataSource" class="org.xyz.test.service.datasourceswitch.impl.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="defaultDataSource" value-ref="defaultDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="defaultDataSource"/>
</bean>
从上面的配置文件中可以看到,我们仅仅配置了默认的数据源defaultDataSource。至于其他的数据源targetDataSources,我们没有配置,需要在代码中动态的创建。关于配置就讲清楚啦!但我们注意到,支持动态数据源的不应该是AbstractRoutingDataSource类吗?怎么上面的配置中是DynamicDataSource类。没错,这个是我们自定义的继承自AbstractRoutingDataSource类的类,也只最重要的类,来看下:(理解这个类,你需要熟练掌握JAVA反射,以及ThreadLocal变量,和Spring的注入机制。别退缩,大家都是这样一步步学过来的!)(下面仅仅是看下全貌,代码的下面会有详细的说明)
[java] view plain
final class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware{
private static final String DATA_SOURCES_NAME = "targetDataSources";
private ApplicationContext applicationContext;
@Override
protected Object determineCurrentLookupKey() {
DataSourceBeanBuilder dataSourceBeanBuilder = DataSourceHolder.getDataSource();
System.out.println("----determineCurrentLookupKey---"+dataSourceBeanBuilder);
if (dataSourceBeanBuilder == null) {
return null;
}
DataSourceBean dataSourceBean = new DataSourceBean(dataSourceBeanBuilder);
//查看当前容器中是否存在
try {
Map<Object,Object> map=getTargetDataSources();
synchronized (this) {
if (!map.keySet().contains(dataSourceBean.getBeanName())) {
map.put(dataSourceBean.getBeanName(), createDataSource(dataSourceBean));
super.afterPropertiesSet();//通知spring有bean更新
}
}
return dataSourceBean.getBeanName();
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new SystemException(ErrorEnum.MULTI_DATASOURCE_SWITCH_EXCEPTION);
}
}
private Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {
//在spring容器中创建并且声明bean
context = () applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);
//将dataSourceBean中的属性值赋给目标bean
Map<String, Object> properties = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);
for (Map.Entry<String, Object> entry : properties.entrySet()) {
beanDefinitionBuilder.addPropertyValue((String) entry.getKey(), entry.getValue());
}
beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());
return applicationContext.getBean(dataSourceBean.getBeanName());
}
private Map<Object, Object> getTargetDataSources() throws NoSuchFieldException, IllegalAccessException {
Field field = AbstractRoutingDataSource.class.getDeclaredField(DATA_SOURCES_NAME);
field.setAccessible(true);
return (Map<Object, Object>) field.get(this);
}
private <T> Map<String, Object> getPropertyKeyValues(Class<T> clazz, Object object) throws IllegalAccessException {
Field[] fields = clazz.getDeclaredFields();
Map<String, Object> result = new HashMap<>();
for (Field field : fields) {
field.setAccessible(true);
result.put(field.getName(), field.get(object));
}
result.remove("beanName");
return result;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
}
首先来看覆盖方法determineCurrentLookupKey(),框架在每次调用数据源时会先调用这个方法,以便知道使用哪个数据源。在本文的场景中,数据源是由程序员在即将切换数据源之前,将要使用的那个数据源的名称放到当前线程的ThreadLocal中,这样在determineCurrentLookupKey()方法中就可以从ThreadLocal中拿到当前请求钥匙用的数据源,从而进行初始化数据源并返回该数据源的操作。在ThreadLocal变量中,我们保存了一个DataSourceBuilder,这是一个建造者模式,不是本文的关键。我们在后面说。读者直接把他理解为是一个数据源的描述就好。因此,determineCurrentLookupKey()方法的流程就是:先从ThreadLocal中拿出要使用的数据源信息,然后看当前的targetDataSources中是否有了这个数据源。如果有直接返回。如果没有,创建一个这样的数据源,放到targetDataSources中然后返回。这个过程需要加锁,为何?这是典型的判断后插入场景,在多线程中会有线程安全问题,所以要加锁!至嘱!
由于targetDataSources是父类AbstractRoutingDataSource中的一个私有域,因此想要获得他的实例只能通过Java反射机制。这也是下面的方法存在的意义!
[java] view plain
private Map<Object, Object> getTargetDataSources() throws NoSuchFieldException, IllegalAccessException {
Field field = AbstractRoutingDataSource.class.getDeclaredField(DATA_SOURCES_NAME);
field.setAccessible(true);
return (Map<Object, Object>) field.get(this);
}
然后,我们来看具体是怎么创建数据源的。
[java] view plain
private Object createDataSource(DataSourceBean dataSourceBean) throws IllegalAccessException {
//在spring容器中创建并且声明bean
context = () applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BasicDataSource.class);
//将dataSourceBean中的属性值赋给目标bean
Map<String, Object> properties = getPropertyKeyValues(DataSourceBean.class, dataSourceBean);
for (Map.Entry<String, Object> entry : properties.entrySet()) {
beanDefinitionBuilder.addPropertyValue((String) entry.getKey(), entry.getValue());
}
beanFactory.registerBeanDefinition(dataSourceBean.getBeanName(), beanDefinitionBuilder.getBeanDefinition());
return applicationContext.getBean(dataSourceBean.getBeanName());
}
大家知道,Spring最主要的功能是作为bean容器,即他负责bean生命周期的管理。因此,我们自定义的datasource也不能“逍遥法外”,必须交给Spring容器来管理。这也正是DynamicDataSource类需要实现ApplicationContextAware并且注入ApplicationContext的原因。上面的代码就是根据指定的信息创建一个数据源。这种创建是Spring容器级别的创建。创建完毕之后,需要把刚刚创建的这个数据源放到targetDataSources中,并且还要通知Spring容器,targetDataSources对象变了。下面的方法就是在做这样的事情:
[java] view plain
private void (DataSourceBean dataSourceBean) throws NoSuchFieldException, IllegalAccessException {
getTargetDataSources().put(dataSourceBean.getBeanName(), createDataSource(dataSourceBean));
super.afterPropertiesSet();//通知spring有bean更新
}
上面的这一步很重要。没有这一步的话,Spring压根就不会知道targetDataSources中多了一个数据源。至此DynamicDataSource类就讲完了。其实仔细想想,思路还是很清晰的。啃掉了DynamicDataSource类这块硬骨头,下面就是一些辅助类了。比如说DataSourceHolder,业务代码通过使用这个类来通知DynamicDataSource中的determineCurrentLookupKey()方法到底使用那个数据源:
[java] view plain
public final class DataSourceHolder {
private static ThreadLocal<DataSourceBeanBuilder> threadLocal=new ThreadLocal<DataSourceBeanBuilder>(){
@Override
protected DataSourceBeanBuilder initialValue() {
return null;
}
};
static DataSourceBeanBuilder getDataSource(){
return threadLocal.get();
}
public static void setDataSource(DataSourceBeanBuilder dataSourceBeanBuilder){
threadLocal.set(dataSourceBeanBuilder);
}
public static void clearDataSource(){
threadLocal.remove();
}
}
‘贰’ java代码出错,spring动态切换数据库
配置文件是否正确 。。。。。。。。动态切换,得重新初始化 ~
‘叁’ spring配置多数据源进行动态切换,数据库方言设置那个,因为数据库是oracle和mysql。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"
>
<!-- dataSource -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property>
<property name="url" value="jdbc:oracle:thin:@localhost:1521:oracle"></property>
<property name="username" value="scott"></property>
<property name="password" value="tiger"></property>
</bean>
<!-- jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 注入dataSource -->
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
‘肆’ 多个mongoDB数据源,怎么配置动态切换
1:配置多个数据连接,如下
dbUrl = 'mongodb://'+configs.dbconfig.dbmongodbhealth.host+':'
+ configs.dbconfig.dbmongodbhealth.port.toString() +'/'+configs.dbconfig.dbmongodbhealth.dbname ;
var healthdbtoptions = {db: {},
server: { poolSize: 5 , auto_reconnect:true } ,
replSet: {},
mongos: {}
} ;
MongoClient.connect(dbUrl,healthdbtoptions ,function(err, database) {
if(err) throw err;
dbs.healthdb = database ;
console.info(dbUrl + " open ok healthdb");
//
});
2:调用数据访问的时候动态传递要使用的链接
var publicdb = opts.dbs.publicdb ;
--------------
exports.getAreaByPcodeAsync = function(opts){
var results = {error_code:-1,error_msg:"error"} ;
var bbPromise = opts.dbs.bbpromise ;
var collectionName = "D10002";
var publicdb = opts.dbs.publicdb ;
var filter = {pcode:opts.area.pcode}
return publicdb.collection(collectionName).find(filter).toArrayAsync().
then(function(areas){
var selectArray = [];
});
};
‘伍’ ssh框架中,如何动态切换数据库
每个项目用不同的数据库连接再加一个登陆的项目(只是做登陆然后根据用户的登陆的选择把请求的用户名和密码请求跳转到到不同的项目路径下的登陆验证,如果验证成功则把匹配项目的路径地址返回,记得在那个匹配好了的项目中的session中保存好个人信息),开始的登陆界面收到返回的跳转地址好跳转到相应的项目ps:我没有实现过,只是有这个想法,技术能实现不就你试一下
‘陆’ 完全不依靠xml文件,怎么配置mybatis 说是动态切换数据库,还是什么……
完全不依靠xml文件,怎么配置m依赖包的引入。创建好maven项目之后,打开maven项目下的pom.xml文件,在配置文件中增加对mybatis包和mysql驱动包的依赖。在dependencies中增加对这两个包的配置即可自动将这两个包添加到项目中。
mybatis数据库配置。对于mybatis框架来说,首先需要配置的就是数据库的数据源配置以及采用何种开发模式的配置,对于mavne项目来说这些资源信息都需要配置在src/main/resources下面,对于普通的java项目则只需要配置在src下即可。ybatis 说是动态切换数据库,还是什么
‘柒’ 多个mongoDB数据源,怎么配置动态切换
Spring动态配置多数据源,即在大型应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效提高系统的水平伸缩性。而这样的方案就会不同于常见的单一数据实例的方案,这就要程序在运行时根据当时的请求及系统状态来动态的决定将数据存储在哪个数据库实例中,以及从哪个数据库提取数据。
‘捌’ mybatis plus自定义的mapper如何动态切换数据源
在配置文件更换数据库路径即可
‘玖’ 怎么使用spring或者hibernate实现对数据库的动态切换,最好只需要编程不要使用配置文件
你就是用jdbc来实现吧,写一个 .properties文件进行配置,手动改之间的链接,每次获取一个新的connection对象即可了,记得把之前获取到对象关闭即可。方便你的操作。