项目中我们经常会遇到多数据源的问题,尤其是数据同步或定时任务等项目更是如此。多数据源让人最头痛的,不是配置多个数据源,而是如何能灵活动态的切换数据源。例如在一个spring和hibernate的框架的项目中,我们在spring配置中往往是配置一个dataSource来连接数据库,然后绑定给sessionFactory,在dao层代码中再指定sessionFactory来进行数据库操作。
二、代码实现
1. 首先在配置文件中配置多个dataSource
<bean id="dataSourceMySql" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${mysql.jdbc.driverClass}" />
<property name="jdbcUrl" value="${mysql.jdbc.url}" />
<property name="user" value="${mysql.jdbc.user}" />
<property name="password" value="${mysql.jdbc.password}" />
<property name="initialPoolSize" value="${mysql.jdbc.initialPoolSize}" />
<property name="minPoolSize" value="${mysql.jdbc.minPoolSize}" />
<property name="maxPoolSize" value="${mysql.jdbc.maxPoolSize}" />
<property name="checkoutTimeout" value="${mysql.jdbc.checkoutTimeout}" />
<property name="idleConnectionTestPeriod" value="${mysql.jdbc.idleConnectionTestPeriod}" />
<property name="maxIdleTime" value="${mysql.jdbc.maxIdleTime}" />
<property name="maxStatements" value="${mysql.jdbc.maxStatements}" />
<property name="testConnectionOnCheckout" value="${mysql.jdbc.testConnectionOnCheckout}" />
</bean>
<bean id="dataSourceOracle" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${oracle.jdbc.driverClass}" />
<property name="jdbcUrl" value="${oracle.jdbc.url}" />
<property name="user" value="${oracle.jdbc.user}" />
<property name="password" value="${oracle.jdbc.password}" />
<property name="initialPoolSize" value="${oracle.jdbc.initialPoolSize}" />
<property name="minPoolSize" value="${oracle.jdbc.minPoolSize}" />
<property name="maxPoolSize" value="${oracle.jdbc.maxPoolSize}" />
<property name="checkoutTimeout" value="${oracle.jdbc.checkoutTimeout}" />
<property name="idleConnectionTestPeriod" value="${oracle.jdbc.idleConnectionTestPeriod}" />
<property name="maxIdleTime" value="${oracle.jdbc.maxIdleTime}" />
<property name="maxStatements" value="${oracle.jdbc.maxStatements}" />
<property name="testConnectionOnCheckout" value="${oracle.jdbc.testConnectionOnCheckout}" />
</bean>
2. 扩展Spring的AbstractRoutingDataSource抽象类,实现动态数据源。
AbstractRoutingDataSource中的抽象方法determineCurrentLookupKey是实现数据源的route的核心.这里对该方法进行Override。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getCustomerType();
}
}
上下文DatabaseContextHolder为一线程安全的ThreadLocal,具体代码如下:
public class DatabaseContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setCustomerType(String customerType) {
contextHolder.set(customerType);
}
public static String getCustomerType() {
return contextHolder.get();
}
public static void clearCustomerType() {
contextHolder.remove();
}
}
3. 配置动态数据源
将DynamicDataSource Bean加入到Spring的上下文xml配置文件中去,同时配置DynamicDataSource的targetDataSources(多数据源目标)属性的Map映射。
<bean id="dataSource" class="com.core.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSourceMySql" value-ref="dataSourceMySql" />
<entry key="dataSourceOracle" value-ref="dataSourceOracle" />
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSourceMySql" />
</bean>
4. 使用动态数据源
DynamicDataSource是继承与AbstractRoutingDataSource,而AbstractRoutingDataSource又是继承于org.springframework.jdbc.datasource.AbstractDataSource,AbstractDataSource实现了统一的DataSource接口,所以DynamicDataSource同样可以当一个DataSource使用。
<!-- JdbcTemplate使用动态数据源的配置 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource">
<ref bean="dataSource" />
</property>
</bean>
<!-- 对JdbcTemplate的应用封装类 -->
<bean id="sqlBaseDAO" class="com.whty.dao.BaseDAOImpl">
<property name="jdbcTemplate">
<ref bean="jdbcTemplate" />
</property>
</bean>
5. 动态数据源的管理
如何选择控制每个业务中需要的具体数据源,可是使用手动控制:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
BaseDAO dao = (BaseDAO) context.getBean("baseDAO", BaseDAOImpl.class);
try {
DatabaseContextHolder.setCustomerType("dataSourceMySql");
System.err.println(dao.select("select count(*) sum from TEST t ").get(0).get("SUM"));
DatabaseContextHolder.setCustomerType("dataSourceOracle");
System.err.println(dao.select("select count(*) sum from TEST t ").get(0).get("SUM"));
} catch (Exception e) {
e.printStackTrace();
}
如果在service层有比较统一的规则的话,也可以使用aop设置数据源使用。
<bean id="dataSourceInterceptor" class="com.core.DataSourceInterceptor" />
<aop:config>
<aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor">
<aop:pointcut id="dsMysql" expression="execution(* com.controller.mysql.*.*(..))" />
<aop:pointcut id="dsOracle" expression="execution(* com.controller.oracle.*.*(..))" />
<aop:before method="setdataSourceMysql" pointcut-ref="dsMysql"/>
<aop:before method="setdataSourceOracle" pointcut-ref="dsOracle"/>
</aop:aspect>
</aop:config>
import org.aspectj.lang.JoinPoint;
public class DataSourceInterceptor {
public void setdataSourceMysql(JoinPoint jp) {
DatabaseContextHolder.setCustomerType("dataSourceMySql");
}
public void setdataSourceOracle(JoinPoint jp) {
DatabaseContextHolder.setCustomerType("dataSourceOracle");
}
}
这里一般都是一个service一个数据源,所以最好使用aop在service层执行完之后统一调用
DBContextHolder.clearDBType();
清空数据源信息。
三、注意事项
1. 注意事务拦截器的配置
这是首要的一条。首先你要明白,Spring的事务管理是与数据源绑定的,一旦程序执行到事务管理的那一层(如service)的话,由于在进入该层之前事务已经通过拦截器开启,因此在该层切换数据源是不行的,明白事务的原理是尤为重要的,我之前的文章中,将切换数据源的拦截器配置在了Dao层是有问题的(因为是示例,所以粗心了,对误导了大家我表示道歉),但提供的思路是没有问题的。
Demo中将切换数据源的拦截器(dataSourceInterceptor)配置在了事务拦截器(txadvice)的上一层,也就是Controller层。
2. 注意数据库表的创建
一些人喜欢用Hibernate的自动创建表的功能,但需要注意,在多数据源中,尤其是不同数据库的多数据源,想都自动建表是不行的。因为Hibernate自动建表是在项目启动时触发的,因此只会建立项目配置的默认数据源的表,而其他数据源的表则不会自动创建。大家要注意着点。
3. Hibernate的数据库方言(dialect)可以忽略
在多数据源时,方言的设置可以忽略,Hibernate在使用时会自动识别不同的数据库,因此不必纠结这个配置,甚至不配置也可以。
4. 报No current session错误
这个是因为使用了sessionFactory.getCurrentSession()导致的,current session是与线程绑定的,一个线程只会开启一个Session(除非使用openSession()就不会报错),因此需要设置session与线程的绑定关系。
Demo中使用了Spring管理Hibernate的session,因此在web.xml中配置了OpenSessionInViewFilter,并在hibernate.cfg.xml中配置了current_session_context_class。【PS:使用Spring管理Hibernate时,可以去掉hibernate.cfg.xml,而全部配置的Spring的配置文件里,即hibernateProperties。看个人喜好吧】