在現在互聯網系統中,隨着用戶量的增長,單數據源通常無法滿足系統的負載要求。因此為了解決用戶量增長帶來的壓力,在數據庫層面會采用讀寫分離技術和數據庫拆分等技術。讀寫分離就是就是一個Master數據庫,多個Slave數據庫,Master數據庫負責數據的寫操作,slave庫負責數據讀操作,通過slave庫來降低Master庫的負載。因為在實際的應用中,數據庫都是讀多寫少(讀取數據的頻率高,更新數據的頻率相對較少),而讀取數據通常耗時比較長,占用數據庫服務器的CPU較多,從而影響用戶體驗。我們通常的做法就是把查詢從主庫中抽取出來,采用多個從庫,使用負載均衡,減輕每個從庫的查詢壓力。同時隨着業務的增長,會對數據庫進行拆分,根據業務將業務相關的數據庫表拆分到不同的數據庫中。不管是讀寫分離還是數據庫拆分都是解決數據庫壓力的主要方式之一。本篇文章主要講解Spring如何配置讀寫分離和多數據源手段。
1.讀寫分離
具體到開發中,如何方便的實現讀寫分離呢?目前常用的有兩種方式:
- 第一種方式是最常用的方式,就是定義2個數據庫連接,一個是MasterDataSource,另一個是SlaveDataSource。對數據庫進行操作時,先根據需求獲取dataSource,然后通過dataSource對數據庫進行操作。這種方式配置簡單,但是缺乏靈活新。
- 第二種方式動態數據源切換,就是在程序運行時,把數據源動態織入到程序中,從而選擇讀取主庫還是從庫。主要使用的技術是:annotation,Spring AOP ,反射。下面會詳細的介紹實現方式。
在介紹實現方式之前,先准備一些必要的知識,spring的AbstractRoutingDataSource類。AbstractRoutingDataSource這個類是spring2.0以后增加的,我們先來看下AbstractRoutingDataSource的定義:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {}
AbstractRoutingDataSource繼承了AbstractDataSource並實現了InitializingBean,因此AbstractRoutingDataSource會在系統啟動時自動初始化實例。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;
...
}
AbstractRoutingDataSource繼承了AbstractDataSource ,而AbstractDataSource 又是DataSource 的子類。DataSource 是javax.sql 的數據源接口,定義如下:
public interface DataSource extends CommonDataSource,Wrapper {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password)
throws SQLException;
}
DataSource接口定義了2個方法,都是獲取數據庫連接。我們在看下AbstractRoutingDataSource如何實現了DataSource接口:
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
很顯然就是調用自己的determineTargetDataSource() 方法獲取到connection。determineTargetDataSource方法定義如下:
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;
}
我們最關心的還是下面2句話:
Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey);
determineCurrentLookupKey方法返回lookupKey,resolvedDataSources方法就是根據lookupKey從Map中獲得數據源。resolvedDataSources 和determineCurrentLookupKey定義如下:
private Map<Object, DataSource> resolvedDataSources; protected abstract Object determineCurrentLookupKey()
看到以上定義,我們是不是有點思路了,resolvedDataSources是Map類型,我們可以把MasterDataSource和SlaveDataSource存到Map中。通過寫一個類DynamicDataSource繼承AbstractRoutingDataSource,實現其determineCurrentLookupKey() 方法,該方法返回Map的key,master或slave。
public class DynamicDataSource extends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getCustomerType();
}
}
定義DatabaseContextHolder
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();
}
}
從DynamicDataSource 的定義看出,他返回的是DynamicDataSourceHolder.getDataSouce()值,我們需要在程序運行時調用DynamicDataSourceHolder.putDataSource()方法,對其賦值。下面是我們實現的核心部分,也就是AOP部分,DataSourceAspect定義如下:
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
@Before(value = "execution(* com.netease.nsip.DynamicDataSource.dao..*.insert*(..))"
+ "||execution(* com.netease.nsip.DynamicDataSource.dao..*.add*(..))"
+ "||@org.springframework.transaction.annotation.Transactional * *(..)")
public Object before(ProceedingJoinPoint joinPoint) throws Throwable {
DatabaseContextHolder.setCustomerType("master");
Object object = joinPoint.proceed();
DatabaseContextHolder.setCustomerType("slave");
return object;
}
}
為了方便測試,我定義了2個數據庫,Master庫和Slave庫,兩個庫中person表結構一致,但數據不同,properties文件配置如下:
#common db-driver=com.mysql.jdbc.Driver #master master-url=jdbc:mysql://127.0.0.1:3306/master?serverTimezone=UTC master-user=root master-password=root #salve slave-url=jdbc:mysql://127.0.0.1:3306/slave?serverTimezone=UTC slave-user=root slave-password=root
Spring中的xml定義如下:
<!-- 配置數據源公共參數 -->
<bean name="baseDataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>${db-driver}</value>
</property>
</bean>
<!-- 配置主數據源 -->
<bean name="masterDataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url">
<value>${master-url}</value>
</property>
<property name="username">
<value>${master-user}</value>
</property>
<property name="password">
<value>${master-password}</value>
</property>
</bean>
<!--配置從數據源 -->
<bean name="slavueDataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url">
<value>${slave-url}</value>
</property>
<property name="username">
<value>${slave-user}</value>
</property>
<property name="password">
<value>${slave-password}</value>
</property>
</bean>
<bean id="dataSource"
class="com.netease.nsip.DynamicDataSource.commom.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="master" value-ref="masterDataSource" />
<entry key="slave" value-ref="slavueDataSource" />
</map>
</property>
<property name="defaultTargetDataSource" ref="slavueDataSource" />
</bean>
<!-- 配置SqlSessionFactoryBean -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:SqlMapConfig.xml" />
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 持久層訪問模板化的工具,線程安全,構建sqlSessionFactory -->
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<!-- 事務管理器 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="txManager"
proxy-target-class="true" order="200" />
<!-- 回滾方式 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" />
</tx:attributes>
</tx:advice>
<!-- 定義@Transactional的注解走事務管理器 -->
<aop:config>
<aop:pointcut id="transactionPointcutType"
expression="@within(org.springframework.transaction.annotation.Transactional)" />
<aop:pointcut id="transactionPointcutMethod"
expression="@annotation(org.springframework.transaction.annotation.Transactional)" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcutType" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcutMethod" />
</aop:config>
到目前讀寫分離已經配置好了,所有的以insert和add開頭的dao層,以及帶有Transaction注解的會走主庫,其他的數據庫操作走從庫。當然也可以修改切入點表達式讓update和delete方法走主庫。上述方法是基於AOP的讀寫分離配置,下面使用實例結合注解講述多數據源的配置。
2.多數據源配置
上面的實例使用AOP來配置讀寫分離,接下來將結合Spring注解配置多數據源,該方法也可以用於配置讀寫分離。先看下annotation的定義:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Profile {
String value();
}
定義MultiDataSourceAspect ,在MultiDataSourceAspect根據注解獲取數據源.
public class MultiDataSourceAspect {
public void before(JoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
String method = joinPoint.getSignature().getName();
Class<?>[] classz = target.getClass().getInterfaces();
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).
getMethod().getParameterTypes();
try {
Method m = classz[0].getMethod(method, parameterTypes);
if (m != null&&m.isAnnotationPresent(Profile.class)) {
Profile data = m .getAnnotation(Profile.class);
DatabaseContextHolder.setCustomerType(data.value());
}
} catch (Exception e) {
}
}
}
同樣為了測試,數據源properties文件如下:
#common db-driver=com.mysql.jdbc.Driver #master account-url=jdbc:mysql://127.0.0.1:3306/master?serverTimezone=UTC account-user=root account-password=root #salve goods-url=jdbc:mysql://127.0.0.1:3306/slave?serverTimezone=UTC goods-user=root goods-password=root
Spring的XML文件定義如下:
<!-- 配置數據源公共參數 -->
<bean name="baseDataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>${db-driver}</value>
</property>
</bean>
<!-- 配置主數據源 -->
<bean name="accountDataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url">
<value>${account-url}</value>
</property>
<property name="username">
<value>${account-user}</value>
</property>
<property name="password">
<value>${account-password}</value>
</property>
</bean>
<!--配置從數據源 -->
<bean name="goodsDataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url">
<value>${goods-url}</value>
</property>
<property name="username">
<value>${goods-user}</value>
</property>
<property name="password">
<value>${goods-password}</value>
</property>
</bean>
<bean id="dataSource"
class="com.netease.nsip.DynamicDataSource.commom.MultiDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="goods" value-ref="goodsDataSource" />
<entry key="account" value-ref="accountDataSource" />
</map>
</property>
</bean>
<!-- 配置SqlSessionFactoryBean -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:multiSqlMapConfig.xml" />
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 持久層訪問模板化的工具,線程安全,構建sqlSessionFactory -->
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<!-- 配置AOP -->
<bean id="multiAspect"
class="com.netease.nsip.DynamicDataSource.commom.MultiDataSourceAspect" />
<aop:config>
<aop:aspect id="datasourceAspect" ref="multiAspect">
<aop:pointcut
expression="execution(* com.netease.nsip.DynamicDataSource.dao..*.insert*(..))"
id="tx" />
<aop:before pointcut-ref="tx" method="before" />
</aop:aspect>
</aop:config>
dao層接口定義如下:
public interface IAccountDao {
@Profile("account")
public boolean insert(Accounts accounts);
}
public interface IGoodsDao {
@Profile("goods")
public boolean insert(Goods goods);
}
Spring配置多數據源的主要方式如上所示,在實例中為了方便數據源的選擇都在dao進行。而在日常開發的過程中事務通常在Service層,而事務又和數據源綁定,所以為了在Service層使用事務可以將數據源的選擇在service層進行。
