Spring动态切换多数据源解决方案

时间:2023-03-09 00:45:40
Spring动态切换多数据源解决方案

Spring动态配置多数据源,即在大型应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效提高系统的水平伸缩性。而这样的方案就会不同于常见的单一数据实例的方案,这就要程序在运行时根据当时的请求及系统状态来动态的决定将数据存储在哪个数据库实例中,以及从哪个数据库提取数据。

Spring配置多数据源的方式和具体使用过程。 
Spring对于多数据源,以数据库表为参照,大体上可以分成两大类情况: 
一是,表级上的跨数据库。即,对于不同的数据库却有相同的表(表名和表结构完全相同)。 
二是,非表级上的跨数据库。即,多个数据源不存在相同的表。

1、根据用户的选择,使用不同的数据源。

2、解决思路锁定:将sessionFactory的属性dataSource设置成不同的数据源,以达到切换数据源的目的。

3、问题产生:因为整个项目用的几乎都是单例模式,当多个用户并发访问数据库的时候,会产生资源争夺的问题。即项目启动时候,所有的bean都被装载到内存,并且每个bean都只有一个对象。正因为只有一个对象,所有的对象属性就如同静态变量(静态变量跟单例很相似,常用静态来实现单例)。整个项目系统的dataSource只有一个,如果很多用户不断的去改变dataSource的值,那必然会出现资源的掠夺问题,造成系统隐患。

4、多资源共享解决思路:同一资源被抢夺的时候,通常有两种做法,a、以时间换空间 b、以空间换时间。

5、线程同步机制就是典型的“以时间换空间”,采用排队稍等的方法,一个个等待,直到前面一个用完,后面的才跟上,多人共用一个变量,用synchronized锁定排队。   

6、“ThreadLocal”就是典型的“以空间换时间”,她可以为每一个人提供一份变量,因此可以同时访问并互不干扰。

7、言归正传:sessionFactory的属性dataSource设置成不用的数据源,首先不能在配置文件中写死,我们必须为她单独写一个类,让她来引用这个类,在这个类中再来判断我们到底要选择哪个数据源。

spring + mybatis 多数据源切换

DbContextHolder.java

 package com.easyway.stage.commons;  

 public class DbContextHolder
{ // ThreadLocal是线程安全的,并且不能在多线程之间共享。
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static void setDbType(String dbType)
{
contextHolder.set(dbType);
} public static String getDbType()
{
return ((String) contextHolder.get());
} public static void clearDbType()
{
contextHolder.remove();
} }

MultiDataSource.java

 package com.easyway.stage.commons;  

 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;  

 public class MultiDataSource extends AbstractRoutingDataSource
{ @Override
protected Object determineCurrentLookupKey()
{
return DbContextHolder.getDbType();
} }

Xml代码:

applicationContext.xml

 <?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:annotation-config/> <!-- 数据源 -->
<bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="1" />
<property name="maxActive" value="20" />
<property name="minIdle" value="1" /> <!-- 配置获取连接等待超时的时间60s -->
<property name="maxWait" value="60000" /> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" /> <property name="validationQuery" value="SELECT 'x'" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" /> <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> <!-- 配置监控统计拦截的filters -->
<property name="filters" value="wall,stat,slf4j" /> <!-- 对于长时间不使用的连接强制关闭 -->
<property name="removeAbandoned" value="true" />
<!-- 超过30分钟开始关闭空闲连接 -->
<property name="removeAbandonedTimeout" value="1800" />
<!-- 关闭abanded连接时输出错误日志 -->
<property name="logAbandoned" value="true" />
</bean>
<bean id="local" parent="parentDataSource" init-method="init" destroy-method="close">
<property name="url" value="${local.jdbc.url}" />
<property name="username" value="${local.jdbc.username}" />
<property name="password" value="${local.jdbc.password}" />
</bean>
<bean id="server" parent="parentDataSource" init-method="init" destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean> <bean id="dataSource" class="com.autrade.stage.commons.MultiDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry value-ref="local" key="local"></entry>
<entry value-ref="server" key="server"></entry>
</map>
</property>
<!-- 默认使用server的数据源 -->
<property name="defaultTargetDataSource" ref="server"></property>
</bean> <!-- MyBatis -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:resources/mybatis/myBatisConfig.xml" />
<property name="mapperLocations" value="classpath:resources/mybatis/mapper/*.xml"/>
</bean>
<bean class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory"/>
</bean>
<!-- MyBatis --> <!-- 配置事务管理对象-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 将所有具有@Transactional注解的Bean自动配置为声明式事务支持 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/> <!-- 自定义的拦截器 -->
<bean id="methodAdvisor" class="com.easyway.app.interceptor.InjectorManager" /> <aop:config proxy-target-class="true">
<aop:pointcut id="baseMethods"
expression="execution(* com.easyway.app.service..*.*(..))" />
<aop:advisor advice-ref="methodAdvisor" pointcut-ref="baseMethods" />
</aop:config> </beans>

Test.java测试类

 package com.easyway.stage.test;  

 import javax.sql.DataSource;  

 import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource; import com.easyway.stage.commons.DbContextHolder; public class Test
{ /**
* @param args
*/
public static void main(String[] args)
{
ApplicationContext appContext = new ClassPathXmlApplicationContext("client-beans.xml"); DbContextHolder.setDbType("local");
String res = "resources/mybatis/myBatisConfig.xml";
DataSource datasource = (DataSource) appContext.getBean("dataSource"); SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
Resource resource = new FileSystemResource(res);
bean.setConfigLocation(resource);
try
{
SqlSessionFactory sessionfactory = bean.getObject();
SqlSession session = sessionfactory.openSession();
User user = session.selectOne("com.easyway.mybatis.mapper.findOne");
System.out.println(user.getName());
}
catch (Exception e)
{
e.printStackTrace();
} DbContextHolder.setDbType("server");
String res1 = "resources/mybatis/myBatisConfig.xml";
DataSource datasource1 = (DataSource) appContext.getBean("dataSource"); SqlSessionFactoryBean bean1 = new SqlSessionFactoryBean();
bean1.setDataSource(datasource1);
Resource resource1 = new FileSystemResource(res1);
bean1.setConfigLocation(resource1); try
{
SqlSessionFactory sessionfactory = bean.getObject();
SqlSession session = sessionfactory.openSession();
User user = session.selectOne("com.easyway.mybatis.mapper.findOne");
System.out.println(user.getName());
}
catch (Exception e)
{
e.printStackTrace();
} } }
注意:当切换数据源时,需要在service层之外,如果需要在service层中切换非默认数据源,则不能开启事务,而且下次使用时,线程仍然绑定,此时若需要使用默认数据源,则需要显示的手动切换数据源,否则会出现xxx.table doesnt exist的问题。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Spring动态配置多数据源,即在大型应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效提高系统的水平伸缩性。而这样的方案就会不同于常见的单一数据实例的方案,这就要程序在运行时根据当时的请求及系统状态来动态的决定将数据存储在哪个数据库实例中,以及从哪个数据库提取数据。

Spring2.x以后的版本中采用Proxy模式,就是我们在方案中实现一个虚拟的数据源,并且用它来封装数据源选择逻辑,这样就可以有效地将数据源选择逻辑从Client中分离出来。Client提供选择所需的上下文(因为这是Client所知道的),由虚拟的DataSource根据Client提供的上下文来实现数据源的选择。

实现

具体的实现就是,虚拟的DataSource仅需继承AbstractRoutingDataSource实现determineCurrentLookupKey()在其中封装数据源的选择逻辑。
一、动态配置多数据源
1. 数据源的名称常量类:
  1. /**
  2. * 动态配置多数据源
  3. * 数据源的名称常量类
  4. * @author LONGHUI_LUO
  5. *
  6. */
  7. public class DataSourceConst {
  8. public static final String TEST="test";
  9. public static final String USER="User";
  10. }

2. 建立一个获得和设置上下文环境的类,主要负责改变上下文数据源的名称:

  1. /**
  2. * 获得和设置上下文环境 主要负责改变上下文数据源的名称
  3. *
  4. * @author LONGHUI_LUO
  5. *
  6. */
  7. public class DataSourceContextHolder {
  8. private static final ThreadLocal contextHolder = new ThreadLocal(); // 线程本地环境
  9. // 设置数据源类型
  10. public static void setDataSourceType(String dataSourceType) {
  11. contextHolder.set(dataSourceType);
  12. }
  13. // 获取数据源类型
  14. public static String getDataSourceType() {
  15. return (String) contextHolder.get();
  16. }
  17. // 清除数据源类型
  18. public static void clearDataSourceType() {
  19. contextHolder.remove();
  20. }
  21. }

3. 建立动态数据源类,注意,这个类必须继承AbstractRoutingDataSource,且实现方法determineCurrentLookupKey,该方法返回一个Object,一般是返回字符串:

  1. /**
  2. * 建立动态数据源
  3. *
  4. * @author LONGHUI_LUO
  5. *
  6. */
  7. public class DynamicDataSource extends AbstractRoutingDataSource {
  8. protected Object determineCurrentLookupKey() {
  9. // 在进行DAO操作前,通过上下文环境变量,获得数据源的类型
  10. return DataSourceContextHolder.getDataSourceType();
  11. }
  12. }

4. 编写spring的配置文件配置多个数据源

  1. <!-- 数据源相同的内容 -->
  2. <bean
  3. id="parentDataSource"
  4. class="org.apache.commons.dbcp.BasicDataSource"
  5. destroy-method="close">
  6. <property
  7. name="driverClassName"
  8. value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
  9. <property name="username" value="sa" />
  10. <property name="password" value="net2com" />
  11. </bean>
  1. <!-- start以下配置各个数据源的特性 -->
  2. <bean parent="parentDataSource" id="testDataSource">
  3. <propertynamepropertyname="url" value="jdbc:sqlserver://localhost:1433;databaseName=test" />
  4. </bean>
  5. <bean parent="parentDataSource" id="UserDataSource">
  6. <property
  7. name="url"
  8. value="jdbc:sqlserver://localhost:1433;databaseName=User" />
  9. </bean>
  1. <!-- end 配置各个数据源的特性 -->

5. 编写spring配置文件配置多数据源映射关系

  1. <bean class="com.xxxx.datasouce.DynamicDataSource" id="dataSource">
  2. <property name="targetDataSources">
  3. <map key-type="java.lang.String">
  4. <entry value-ref="testDataSource" key="test"></entry>
  5. <entry value-ref="UserDataSource" key="User"></entry>
  6. </map>
  7. </property>
  8. <property name="defaultTargetDataSource" ref="testDataSource" ></property>
  9. </bean>

在这个配置中第一个property属性配置目标数据源,<map key-type="Java.lang.String">中的key-type必须要和静态键值对照类DataSourceConst中的值的类型相 同;<entry key="User" value-ref="userDataSource"/>中key的值必须要和静态键值对照类中的值相同,如果有多个值,可以配置多个< entry>标签。第二个property属性配置默认的数据源。

动态切换是数据源

  1. DataSourceContextHolder.setDataSourceType(DataSourceConst.TEST);

该方案的优势

       首先,这个方案完全是在spring的框架下解决的,数据源依然配置在spring的配置文件中,sessionFactory依然去配置它的dataSource属性,它甚至都不知道dataSource的改变。唯一不同的是在真正的dataSource与sessionFactory之间增加了一个MultiDataSource。
其次,实现简单,易于维护。这个方案虽然我说了这么多东西,其实都是分析,真正需要我们写的代码就只有MultiDataSource、SpObserver两个类。MultiDataSource类真正要写的只有getDataSource()和getDataSource(sp)两个方法,而SpObserver类更简单了。实现越简单,出错的可能就越小,维护性就越高。
最后,这个方案可以使单数据源与多数据源兼容。这个方案完全不影响BUS和DAO的编写。如果我们的项目在开始之初是单数据源的情况下开发,随着项目的进行,需要变更为多数据源,则只需要修改spring配置,并少量修改MVC层以便在请求中写入需要的数据源名,变更就完成了。如果我们的项目希望改回单数据源,则只需要简单修改配置文件。这样,为我们的项目将增加更多的弹性。

该方案的缺点

       没有能够解决多用户访问单例“sessionFactory”时共享“dataSource”变量,导致产生争抢“dataSource”的结果,本质类似于操作系统中的“生产者消费者”问题。因此当多用户访问时,多数据源可能会导致系统性能下降的后果。