使用目的
出于在审计厅项目建设的需求,我们在项目建设工程先是使用了单一的数据库,经过三个月的代码编写,完成了单机的项目部署,在经过两台loadRunner进行2k的并发访问时,发现数据库的写日志缓冲区已经爆满,导致系统宕机。后来在老师的决策
下将数据库分库存储,不同地区的数据利用切分工具进行数据的切分,然后使用ETL、dts配合自己写的脚本完成数据的迁移和各种角色、存储过程、权限的设置。
数据是分开存放了,那如何在保证单机系统的可运行的情况下,使用多数据源呢。经过调研,我们发现hibernate+spring
可以使用多数据源,可以在使用中动态切换数据源。
数据源切换的步骤
我们的项目中用到了17个私有数据库实例,117个模式。1个公共数据库实例。
如上图所示,我们系统的数据源的切换过程大概是这种模型。刚开始所有用户的登录数据都是由公共数据源进行统一管理的,后面的审计数据的查询,自由上传的辅助数据都是存放在用户的私有数据源中的。
动态数据源切换的实现
编写动态数据源类
利用spring
的AbstractRoutingDataSource
来实现动态数据源的获取。AbstractRoutingDataSource,它的一个作用就是可以根据用户发起的不同请求去转换不同的数据源,比如根据用户的不同地区语言选择不同的数据库
import java.sql.SQLException;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
// static Logger log = Logger.getLogger("DynamicDataSource");
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
}
补充:dataSource.properties文件内容
DRIVER_NAME = xx.xx.xxx.xxxx
DATABASE_PASSWORD = xxxxx
DATABASE_USER = xxxx
#私有数据源
DATABASE_URL_DEFAULT=jdbc:xx://localhost
#共有数据源
DATABASE_URL_COMMON=jdbc:xx://localhost
利用Druid实现私有数据源和公共数据源
<bean id="dataSource_common" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${DRIVER_NAME}" />
<property name="url" value="${DATABASE_URL_DEFAULT}" />
<property name="username" value="${DATABASE_USER}" />
<property name="password" value="${DATABASE_PASSWORD}" />
<property name="filters" value="stat" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="3000" />
<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" />
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
</bean>
<bean id="dataSource_default" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${DRIVER_NAME}" />
<property name="url" value="${DATABASE_URL_COMMON}" />
<property name="username" value="${DATABASE_USER}" />
<property name="password" value="${DATABASE_PASSWORD}" />
<property name="filters" value="stat" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="connectionProperties" value="druid.stat.slowSqlMillis=5000" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="3000" />
<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" />
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
</bean>
声明动态数据源springBean
map
中可以存放多个数据源,在使用的时候指定key
就可以获取对应的数据源了.
<bean id="dynamicDataSource" class="dao.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!--私有数据源-->
<entry key="default" value-ref="dataSource_default" />
<!--公共数据源-->
<entry key="common" value-ref="dataSource_common" />
</map>
</property>
<!--默认的数据源-->
<property name="defaultTargetDataSource" ref="dataSource_default" />
</bean>
配置Hibernate的SessionFactory
这里是在spring的配置文件中配置Hibernate的SessionFactorty,这里使用的dataSource已经是动态的数据源了。
<!-- 配置会话工厂 -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<!-- 设置数据源 -->
<property name="dataSource" ref="dynamicDataSource" />
<!-- 接管了hibernate对象映射文件 -->
<property name="mappingResources">
<list>
<value>xxx/xxx.hbm.xml</value>
</list>
</property>
<!--指定hibernate的属性值 -->
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.xxx</prop>
<!-- <prop key="hibernate.hbm2ddl.auto">update</prop> -->
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.format_sql">true</prop>
<!-- -->
<prop key="javax.persistence.validation.mode">none</prop>
<prop key="connection.driver_class">xx.xxx.xx.xx</prop>
</props>
</property>
</bean>
测试数据源是否可以连接成功
这里使用的是基于注解的测试类,测试的运行结果如下图所示。
import dao.DynamicDataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.sql.SQLException;
/**
* Created by frank on 16-5-30.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class NIuTest {
@Autowired
ApplicationContext context;
@Test
public void testContext() throws SQLException {
DynamicDataSource ds = (DynamicDataSource) context.getBean("dynamicDataSource");
System.out.println("loginTimeOutValue:"+ds.getLoginTimeout());
}
}
创建线程私有的数据源上下文
创建一个数据源上下文持有者,该类使用了ThreadLocal
能够保证线程私有,使得不同地区的用户访问时,不会出现数据源冲突。
public class DbContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal();
public static final String DATA_SOURCE_DEFAULT = "default";
public static final String DATA_SOURCE_COMMON = "common";
public static void setDbType(String dbType) {
contextHolder.set(dbType);
}
public static String getDbType() {
return (String) contextHolder.get();
}
public static void clearDbType() {
contextHolder.remove();
}
}
在程序运行时,如何切换数据源
在service
类中调用Hibernate
进行数据操作之前,如果需要切换数据源到私有的数据源,可以调用下面的方法.然后spring
的DynamicDataSource
实例则会调用determineCurrentLookupKey
方法进行数据源的切换。
DbContextHolder.setDbType(DbContextHolder.DATA_SOURCE_COMMON);
一个更为实际的例子如下,如果我们想要获取某条用户输入的sql语句可以得到多少条数据,那么我们需将数据源切换到用户私有的数据源。
public long getAllMessageCount_Common(String sql)
throws HibernateException, NullPointerException, SQLException {
DbContextHolder.setDbType(DbContextHolder.DATA_SOURCE_COMMON);
StringBuilder hql = null;
hql = new StringBuilder("SELECT count(*) from (");
hql.append(sql);
hql.append(")");
// logger.info("得到的sql语句:" + hql);
long result = basicDao.executeQueryCountSQL_throwEX(hql.toString());
logger.info("后台得到的数据count:" + result);
return result;
}
从上面可以看出,我们使用的是DbContextHolder.setDbType()
方法来动态的进行数据源的切换。
总结
数据源的动态切换就整理到这里,项目已经做了很久了,今天在整理项目中使用到的知识时,想到这块自己可以整理一下,才写此文作为记录吧。