使用目的
出于在審計(jì)廳項(xiàng)目建設(shè)的需求摘昌,我們?cè)陧?xiàng)目建設(shè)工程先是使用了單一的數(shù)據(jù)庫(kù)哥攘,經(jīng)過(guò)三個(gè)月的代碼編寫,完成了單機(jī)的項(xiàng)目部署晶伦,在經(jīng)過(guò)兩臺(tái)loadRunner進(jìn)行2k的并發(fā)訪問(wèn)時(shí),發(fā)現(xiàn)數(shù)據(jù)庫(kù)的寫日志緩沖區(qū)已經(jīng)爆滿啄枕,導(dǎo)致系統(tǒng)宕機(jī)婚陪。后來(lái)在老師的決策
下將數(shù)據(jù)庫(kù)分庫(kù)存儲(chǔ),不同地區(qū)的數(shù)據(jù)利用切分工具進(jìn)行數(shù)據(jù)的切分频祝,然后使用ETL泌参、dts配合自己寫的腳本完成數(shù)據(jù)的遷移和各種角色、存儲(chǔ)過(guò)程常空、權(quán)限的設(shè)置沽一。
數(shù)據(jù)是分開存放了,那如何在保證單機(jī)系統(tǒng)的可運(yùn)行的情況下漓糙,使用多數(shù)據(jù)源呢铣缠。經(jīng)過(guò)調(diào)研,我們發(fā)現(xiàn)hibernate+spring
可以使用多數(shù)據(jù)源,可以在使用中動(dòng)態(tài)切換數(shù)據(jù)源蝗蛙。
數(shù)據(jù)源切換的步驟
我們的項(xiàng)目中用到了17個(gè)私有數(shù)據(jù)庫(kù)實(shí)例蝇庭,117個(gè)模式。1個(gè)公共數(shù)據(jù)庫(kù)實(shí)例捡硅。
如上圖所示哮内,我們系統(tǒng)的數(shù)據(jù)源的切換過(guò)程大概是這種模型。剛開始所有用戶的登錄數(shù)據(jù)都是由公共數(shù)據(jù)源進(jìn)行統(tǒng)一管理的壮韭,后面的審計(jì)數(shù)據(jù)的查詢北发,自由上傳的輔助數(shù)據(jù)都是存放在用戶的私有數(shù)據(jù)源中的。
動(dòng)態(tài)數(shù)據(jù)源切換的實(shí)現(xiàn)
編寫動(dòng)態(tài)數(shù)據(jù)源類
利用spring
的AbstractRoutingDataSource
來(lái)實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源的獲取泰涂。AbstractRoutingDataSource鲫竞,它的一個(gè)作用就是可以根據(jù)用戶發(fā)起的不同請(qǐng)求去轉(zhuǎn)換不同的數(shù)據(jù)源,比如根據(jù)用戶的不同地區(qū)語(yǔ)言選擇不同的數(shù)據(jù)庫(kù)
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;
}
}
補(bǔ)充:dataSource.properties文件內(nèi)容
DRIVER_NAME = xx.xx.xxx.xxxx
DATABASE_PASSWORD = xxxxx
DATABASE_USER = xxxx
#私有數(shù)據(jù)源
DATABASE_URL_DEFAULT=jdbc:xx://localhost
#共有數(shù)據(jù)源
DATABASE_URL_COMMON=jdbc:xx://localhost
利用Druid實(shí)現(xiàn)私有數(shù)據(jù)源和公共數(shù)據(jù)源
<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>
聲明動(dòng)態(tài)數(shù)據(jù)源springBean
map
中可以存放多個(gè)數(shù)據(jù)源逼蒙,在使用的時(shí)候指定key
就可以獲取對(duì)應(yīng)的數(shù)據(jù)源了.
<bean id="dynamicDataSource" class="dao.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!--私有數(shù)據(jù)源-->
<entry key="default" value-ref="dataSource_default" />
<!--公共數(shù)據(jù)源-->
<entry key="common" value-ref="dataSource_common" />
</map>
</property>
<!--默認(rèn)的數(shù)據(jù)源-->
<property name="defaultTargetDataSource" ref="dataSource_default" />
</bean>
配置Hibernate的SessionFactory
這里是在spring的配置文件中配置Hibernate的SessionFactorty,這里使用的dataSource已經(jīng)是動(dòng)態(tài)的數(shù)據(jù)源了寄疏。
<!-- 配置會(huì)話工廠 -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<!-- 設(shè)置數(shù)據(jù)源 -->
<property name="dataSource" ref="dynamicDataSource" />
<!-- 接管了hibernate對(duì)象映射文件 -->
<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>
測(cè)試數(shù)據(jù)源是否可以連接成功
這里使用的是基于注解的測(cè)試類是牢,測(cè)試的運(yùn)行結(jié)果如下圖所示。
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());
}
}
創(chuàng)建線程私有的數(shù)據(jù)源上下文
創(chuàng)建一個(gè)數(shù)據(jù)源上下文持有者,該類使用了ThreadLocal
能夠保證線程私有陕截,使得不同地區(qū)的用戶訪問(wèn)時(shí)驳棱,不會(huì)出現(xiàn)數(shù)據(jù)源沖突。
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();
}
}
在程序運(yùn)行時(shí)农曲,如何切換數(shù)據(jù)源
在service
類中調(diào)用Hibernate
進(jìn)行數(shù)據(jù)操作之前社搅,如果需要切換數(shù)據(jù)源到私有的數(shù)據(jù)源,可以調(diào)用下面的方法.然后spring
的DynamicDataSource
實(shí)例則會(huì)調(diào)用determineCurrentLookupKey
方法進(jìn)行數(shù)據(jù)源的切換乳规。
DbContextHolder.setDbType(DbContextHolder.DATA_SOURCE_COMMON);
一個(gè)更為實(shí)際的例子如下形葬,如果我們想要獲取某條用戶輸入的sql語(yǔ)句可以得到多少條數(shù)據(jù),那么我們需將數(shù)據(jù)源切換到用戶私有的數(shù)據(jù)源暮的。
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語(yǔ)句:" + hql);
long result = basicDao.executeQueryCountSQL_throwEX(hql.toString());
logger.info("后臺(tái)得到的數(shù)據(jù)count:" + result);
return result;
}
從上面可以看出笙以,我們使用的是DbContextHolder.setDbType()
方法來(lái)動(dòng)態(tài)的進(jìn)行數(shù)據(jù)源的切換。
總結(jié)
數(shù)據(jù)源的動(dòng)態(tài)切換就整理到這里冻辩,項(xiàng)目已經(jīng)做了很久了猖腕,今天在整理項(xiàng)目中使用到的知識(shí)時(shí),想到這塊自己可以整理一下恨闪,才寫此文作為記錄吧倘感。