Spring MVC+jdbcTemplate+MySql實現讀寫分離
現如今,資料庫讀寫分離已經不在是新鮮的話題,很多資料統計網站 、訪問量高的網站都會使用這項技術。解決網站響應慢、伺服器承載壓力過大的方案其實有很多,而讀寫分離只是其中一種實現方案。有的時候讀寫分離並不能完美解決。此文章記錄了筆者使用@Aspect註解方式實現Spring MVC+jdbcTemplate+MySql實現讀寫分離的過程。
既然要讀寫分離,那麼資料庫至少就要又兩個,一個主資料庫,負責讀寫,多個重資料庫,負責讀取。
既然是讀寫分離,為什麼主資料庫還要負責讀和寫呢?
在實際的應用場景種,
一、我寫入資料的同時往往要讀取一些基礎資料。
二、資料在寫入之後要讀取出相關資訊,讀寫分離再及時,也會產生延遲,這樣程式上就會異常了。
所以,主資料庫不僅需要寫入的能力,還要有讀取的能力。
筆者實用MySql資料庫,jdbc.properties配置如下
mysqlM.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8
mysqlM.username=root
mysqlM.password=root
mysqlC.url=jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8
mysqlC.username=root
mysqlC.password=root
mysql.driverClassName =com.mysql.jdbc.Driver
mysql.filters=stat
mysql.maxActive=20
mysql.initialSize=1
mysql.maxWait=60000
mysql.minIdle=10
mysql.maxIdle=15
mysql.timeBetweenEvictionRunsMillis=60000
mysql.minEvictableIdleTimeMillis=300000
mysql.validationQuery=SELECT 'x'
mysql.testWhileIdle=true
mysql.testOnBorrow=false
mysql.testOnReturn =false
mysql.maxOpenPreparedStatements=20
mysql.removeAbandoned=true
mysql.removeAbandonedTimeout=1800
mysql.logAbandoned=true
其中,mysqlM是主資料庫,mysqlC是重資料庫。
spring-dao.xml配置如下
<!-- 資料庫基本資訊配置 -->
<bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource" abstract="true">
<property name="driverClassName" value="${mysql.driverClassName}" />
<property name="filters" value="${mysql.filters}" />
<property name="maxActive" value="${mysql.maxActive}" />
<property name="initialSize" value="${mysql.initialSize}" />
<property name="maxWait" value="${mysql.maxWait}" />
<property name="minIdle" value="${mysql.minIdle}" />
<property name="timeBetweenEvictionRunsMillis" value="${mysql.timeBetweenEvictionRunsMillis}" />
<property name="minEvictableIdleTimeMillis" value="${mysql.minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${mysql.validationQuery}" />
<property name="testWhileIdle" value="${mysql.testWhileIdle}" />
<property name="testOnBorrow" value="${mysql.testOnBorrow}" />
<property name="testOnReturn" value="${mysql.testOnReturn}" />
<property name="maxOpenPreparedStatements" value="${mysql.maxOpenPreparedStatements}" />
<property name="removeAbandoned" value="${mysql.removeAbandoned}" />
<property name="removeAbandonedTimeout" value="${mysql.removeAbandonedTimeout}" />
<property name="logAbandoned" value="${mysql.logAbandoned}" />
<property name="poolPreparedStatements" value="false" />
</bean>
<!-- 主資料庫 -->
<bean id="dataSourceM" parent="parentDataSource">
<property name="url" value="${mysqlM.url}" />
<property name="username" value="${mysqlM.username}" />
<property name="password" value="${mysqlM.password}" />
</bean>
<!-- 從資料庫 -->
<bean id="dataSourceC" parent="parentDataSource">
<property name="url" value="${mysqlC.url}" />
<property name="username" value="${mysqlC.username}" />
<property name="password" value="${mysqlC.password}" />
</bean>
<!-- 啟用CGliB -->
<aop:aspectj-autoproxy/>
<!--切面注入spring自定義標籤-->
<bean id="dsChangeAspect" class="com.org.util.DataSourceAspect"/>
<!--動態資料來源、主從庫選擇-->
<bean id="dynamicDataSource" class="com.org.util.DynamicDataSource">
<property name="master" ref="dataSourceM"/>
<property name="slaves">
<list>
<ref bean="dataSourceC"/>
</list>
</property>
</bean>
<!-- 配置Jdbc模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dynamicDataSource"></property>
</bean>
<!-- 配置事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSourceM" />
<!-- 啟用基於註解的事務管理 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
<tx:method name="get*" propagation="REQUIRED" read-only="true" />
<tx:method name="add*" propagation="NESTED" isolation="REPEATABLE_READ" />
<tx:method name="init*" propagation="NESTED" isolation="REPEATABLE_READ" />
....
</tx:attributes>
</tx:advice>
註解:
1、<aop:aspectj-autoproxy/>
spring預設是不開啟Aspect(自定義標籤注入)模式的,需要手動開啟。
2、<bean id="dsChangeAspect" class="com.org.util.DataSourceAspect"/>
自定義類:DataSourceAspect,目的是建立自定義標籤、繫結標籤實現方法。程式碼如下:
package com.org.util;
import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
/**
* 有{@DataSourceChange}註解的方法,呼叫時會切換到指定的資料來源
* @author zbr
* @date 2016年12月28日
*/
@Aspect
public class DataSourceAspect {
@Pointcut(value = "@annotation(com.org.util.DataSourceChange)")
private void changeDS() {
}
@Around(value = "changeDS() ", argNames = "pjp")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
Object retVal = null;
MethodSignature ms = (MethodSignature) pjp.getSignature();
Method method = ms.getMethod();
DataSourceChange annotation = method.getAnnotation(DataSourceChange.class);
boolean selectedDataSource = false;
try {
if (null != annotation) {
selectedDataSource = true;
if (annotation.slave()) {
DynamicDataSource.useSlave();
} else {
DynamicDataSource.useMaster();
}
}
retVal = pjp.proceed();
} catch (Throwable e) {
throw e;
} finally {
if (selectedDataSource) {
DynamicDataSource.reset();
}
}
return retVal;
}
}
DataSourceChange 類
package com.org.util;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceChange {
boolean slave() default false;
}
3、com.org.util.DynamicDataSource
自定義類DynamicDataSource,動態資料來源、主從庫選擇。這一切都要靠spring的AbstractRoutingDataSource,所以我們要繼承AbstractRoutingDataSource。其實可以想一下,如果我們自己要實現讀寫分離,我們自己的思路是什麼。
問題一、什麼時候使用主庫,什麼時候使用重庫。
問題二、什麼時候去切換資料庫。
第一個問題其實很好解釋,主重的選擇完全靠人為來決定,我們給方法、類或者介面加上自定義標籤後,我們的程式就能判斷出你期望使用的資料來源。
第二個問題、很多人都會想到是在dao層查詢的時候去選擇dataSource,沒錯,spring也是這麼想的,所以它提供了我們一個類:AbstractRoutingDataSource。
方法1:afterPropertiesSet,資料來源初始化
方法2:determineCurrentLookupKey,翻譯過來的意思是:根據key來選擇資料來源,就是說我們要訪問資料庫前,AbstractRoutingDataSource中的determineCurrentLookupKey方法,幫我們去選擇dataSource,我們需要告訴spring我們要使用哪個資料來源,也就是Key。
package com.org.util;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 動態資料來源
* 配置主從資料來源後,根據選擇,返回對應的資料來源。多個從庫的情況下,會平均的分配從庫,用於負載均衡。
* @author zbr
* @date 2016年12月28日
*/
public class DynamicDataSource extends AbstractRoutingDataSource{
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);
private DataSource master; // 主庫,只允許有一個
private List<DataSource> slaves; // 從庫,允許有多個
private AtomicLong slaveCount = new AtomicLong();
private int slaveSize = 0;
private Map<Object, Object> dataSources = new HashMap<Object, Object>();
private static final String DEFAULT = "master";//主資料庫的Key
private static final String SLAVE = "slave";//重資料庫的key
private static final ThreadLocal<LinkedList<String>> datasourceHolder = new ThreadLocal<LinkedList<String>>() {
@Override
protected LinkedList<String> initialValue() {
return new LinkedList<String>();
}
};
/**
* 初始化
*/
@Override
public void afterPropertiesSet() {
if (null == master) {
throw new IllegalArgumentException("主資料庫載入失敗!");
}
dataSources.put(DEFAULT, master);
if (null != slaves && slaves.size() > 0) {
for (int i = 0; i < slaves.size(); i++) {
dataSources.put(SLAVE + (i + 1), slaves.get(i));
}
slaveSize = slaves.size();
}
this.setDefaultTargetDataSource(master);
this.setTargetDataSources(dataSources);
super.afterPropertiesSet();
}
/**
* 選擇使用主庫,並把選擇放到當前ThreadLocal的棧頂
*/
public static void useMaster() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("資料來源使用:" + datasourceHolder.get());
}
LinkedList<String> m = datasourceHolder.get();
m.offerFirst(DEFAULT);
}
/**
* 選擇使用從庫,並把選擇放到當前ThreadLocal的棧頂
*/
public static void useSlave() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("資料來源使用:" + datasourceHolder.get());
}
LinkedList<String> m = datasourceHolder.get();
m.offerFirst(SLAVE);
}
/**
* 重置當前棧
*/
public static void reset() {
LinkedList<String> m = datasourceHolder.get();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("重置資料來源 {}", m);
}
if (m.size() > 0) {
m.poll();
}
}
/**
* 如果是選擇使用從庫,且從庫的數量大於1,則通過取模來控制從庫的負載,
* 計算結果返回AbstractRoutingDataSource
*/
@Override
protected Object determineCurrentLookupKey() {
LinkedList<String> m = datasourceHolder.get();
String key = m.peekFirst() == null ? "" : m.peekFirst();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("當前資料來源:" + key);
}
if (null != key) {
if (DEFAULT.equals(key)) {
return key;
} else if (SLAVE.equals(key)) {
if (slaveSize > 1) {
long c = slaveCount.incrementAndGet();
c = c % slaveSize;
return SLAVE + (c + 1);
} else {
return SLAVE + "1";
}
}
return null;
} else {
return null;
}
}
public DataSource getMaster() {
return master;
}
public List<DataSource> getSlaves() {
return slaves;
}
public void setMaster(DataSource master) {
this.master = master;
}
public void setSlaves(List<DataSource> slaves) {
this.slaves = slaves;
}
}
以上就是讀寫分離的基本配置。
在我們需要讀取重資料庫的方法上加入註解:@DataSourceChange(slave = true)
在程式中斷點一下,可以看到每個類的先後執行順序。
多個數據庫之間的建立關係,後續會補上。