項目中要求讀寫分離,在spring中做到讀寫分離,很簡單的想到在配置文件中設置兩個數據源,一個datesource(只寫),一個datesourceread(只讀)。但是要根據上下文動態切換數據源,還需要增加兩個幫助類。
類1 ContextHolder
主要功能是幫助切換數據源,其中ThreadLocal保證線程中的一致性,不受其他線程影響。
public class ContextHolder {
public static final String DATA_SOURCE = "dataSource";
public static final String DATA_SOURCE_READ = "dataSourceRead";
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();
}
}
類2 MyDataSource
自定義數據源,繼承AbstractRoutingDataSource,實現determineCurrentLookupKey()方法。
public class MyDataSource extends AbstractRoutingDataSource {
/* (non-Javadoc)
* @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
*/
@Override
protected Object determineCurrentLookupKey() {
return ContextHolder.getCustomerType();
}
}
配置文件中這樣配置:
<bean id="abstractDataSource" abstract="true"
class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass"
value="#{configManager.getConfigValue('trade-seller-guestbook-serv','datasource.driverClassName')}" />
<property name="acquireIncrement" value="3" />
<property name="initialPoolSize" value="3" />
<property name="minPoolSize" value="2" />
<property name="maxPoolSize" value="50" />
<property name="maxIdleTime" value="600" />
<property name="idleConnectionTestPeriod" value="900" />
<property name="maxStatements" value="100" />
<property name="numHelperThreads" value="10" />
</bean>
<bean id="dataSourceRead" parent="abstractDataSource">
<property name="jdbcUrl"
value="jdbc:mysql://192.168.10.57:3306/trade?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull" />
<property name="user" value="root" />
<property name="password" value="canada" />
<!-- <property name="jdbcUrl"
value="#{configManager.getMysqlConfig('trade_public_mysql','1.2','trade').url}" />
<property name="user"
value="#{configManager.getMysqlConfig('trade_public_mysql','1.2','trade').user}" />
<property name="password"
value="#{configManager.getMysqlConfig('trade_public_mysql','1.2','trade').pwd}" /> -->
</bean>
<bean id="dataSource" parent="abstractDataSource">
<property name="jdbcUrl"
value="#{configManager.getMysqlConfig('trade_public_mysql','1.1','trade').url}" />
<property name="user"
value="#{configManager.getMysqlConfig('trade_public_mysql','1.1','trade').user}" />
<property name="password"
value="#{configManager.getMysqlConfig('trade_public_mysql','1.1','trade').pwd}" />
</bean>
<bean id="myDataSource" class="com.zhe800.guestbook.database.MyDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry value-ref="dataSource" key="dataSource"/>
<entry value-ref="dataSourceRead" key="dataSourceRead"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSource"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="myDataSource" />
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml" />
<property name="mapperLocations">
<array>
<value>classpath*:mybatis/sqlmap/guestbook/*.xml</value>
<value>classpath*:mybatis/sqlmap/manual/*.xml</value>
</array>
</property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.zhe800.guestbook.model.mapper" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="myDataSource" />
</bean>
ContextHolder.setCustomerType(ContextHolder.DATA_SOURCE_READ);
1、一直有一個疑問,既然xml配置中bean是啓動的時候就加載到內存當中,且全局只有一個對象,那麼在運行時設置ContextHolder參數可以修改數據源呢?
2、爲什麼在配置了事務的時候(@transaction),是改變不了數據源的呢?對於第一個問題,在設置了ContextHolder之後,再開始使用sqlSessionFactory進行數據庫相關操作,這時候sqlSessionFactory會找到真正的datasource
public void setDataSource(DataSource dataSource) {
if (dataSource instanceof TransactionAwareDataSourceProxy) {
// If we got a TransactionAwareDataSourceProxy, we need to perform
// transactions for its underlying target DataSource, else data
// access code won't see properly exposed transactions (i.e.
// transactions for the target DataSource).
this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource();
} else {
this.dataSource = dataSource;
}
}
而mydatesource繼承自AbstractRoutingDataSource,類中精華部分在這裏
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
他會調用determineCurrentLookupKey()方法選擇合適的數據源的key,而這個方法在我們的MyDataSource類中已經實現,且將向下文需要的ContextHolder設置了進去。
這樣就實現了運行時的動態數據庫的選擇。