背景
我們一般應用對資料庫而言都是“讀多寫少”,也就說對資料庫讀取資料的壓力比較大,有一個思路就是說採用資料庫叢集的方案,
其中一個是主庫,負責寫入資料,我們稱之為:寫庫;
其它都是從庫,負責讀取資料,我們稱之為:讀庫;
那麼,對我們的要求是:
1、 讀庫和寫庫的資料一致;
2、 寫資料必須寫到寫庫;
3、 讀資料必須到讀庫; 方案
解決讀寫分離的方案有兩種:應用程式層解決和中介軟體解決。 應用程式層解決
優點:
1、 多資料來源切換方便,由程式自動完成;
2、 不需要引入中介軟體;
3、 理論上支援任何資料庫;
缺點:
1、 由程式員完成,營運參與不到;
2、 不能做到動態增加資料來源; 中介軟體解決
優點:
1、 來源程式不需要做任何改動就可以實現讀寫分離;
2、 動態添加資料來源不需要重啟程式;
缺點:
1、 程式依賴於中介軟體,會導致切換資料庫變得困難;
2、 由中介軟體做了中轉代理,效能有所下降;
相關中介軟體產品使用:
mysql-proxy:http://hi.baidu.com/geshuai2008/item/0ded5389c685645f850fab07
Amoeba for MySQL:http://www.iteye.com/topic/188598和http://www.iteye.com/topic/1113437 使用Spring基於應用程式層實現 原理
在進入Service之前,使用AOP來做出判斷,是使用寫庫還是讀庫,判斷依據可以根據方法名判斷,比如說以query、find、get等開頭的就走讀庫,其他的走寫庫。 代碼實現
DynamicDataSource
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;/** * 定義動態資料源,實現通過整合Spring提供的AbstractRoutingDataSource,只需要實現determineCurrentLookupKey方法即可 * * 由於DynamicDataSource是單例的,線程不安全的,所以採用ThreadLocal保證安全執行緒,由DynamicDataSourceHolder完成。 * * @author * */public class DynamicDataSource extends AbstractRoutingDataSource{ @Override protected Object determineCurrentLookupKey() { // 使用DynamicDataSourceHolder保證安全執行緒,並且得到當前線程中的資料來源key return DynamicDataSourceHolder.getDataSourceKey(); }}
DynamicDataSourceHolder
/** * * 使用ThreadLocal技術來記錄當前線程中的資料來源的key * * @author * */public class DynamicDataSourceHolder { //寫庫對應的資料來源key private static final String MASTER = "master"; //讀庫對應的資料來源key private static final String SLAVE = "slave"; //使用ThreadLocal記錄當前線程的資料來源key private static final ThreadLocal<String> holder = new ThreadLocal<String>(); /** * 設定資料來源key * @param key */ public static void putDataSourceKey(String key) { holder.set(key); } /** * 擷取資料來源key * @return */ public static String getDataSourceKey() { return holder.get(); } /** * 標記寫庫 */ public static void markMaster(){ putDataSourceKey(MASTER); } /** * 標記讀庫 */ public static void markSlave(){ putDataSourceKey(SLAVE); }}
DataSourceAspect
import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.JoinPoint;/** * 定義資料來源的AOP切面,通過該Service的方法名判斷是應該走讀庫還是寫庫 * * @author * */public class DataSourceAspect { /** * 在進入Service方法之前執行 * * @param point 切面對象 */ public void before(JoinPoint point) { // 擷取到當前執行的方法名 String methodName = point.getSignature().getName(); if (isSlave(methodName)) { // 標記為讀庫 DynamicDataSourceHolder.markSlave(); } else { // 標記為寫庫 DynamicDataSourceHolder.markMaster(); } } /** * 判斷是否為讀庫 * * @param methodName * @return */ private Boolean isSlave(String methodName) { // 方法名以query、find、get開頭的方法名走從庫 return StringUtils.startsWithAny(methodName, "query", "find", "get"); }}配置實現
jdbc.properties配置2個資料來源
jdbc.master.driver=com.mysql.jdbc.Driverjdbc.master.url=jdbc:mysql://127.0.0.1:3306/mybatis_1128?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=truejdbc.master.username=rootjdbc.master.password=123456jdbc.slave01.driver=com.mysql.jdbc.Driverjdbc.slave01.url=jdbc:mysql://127.0.0.1:3307/mybatis_1128?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=truejdbc.slave01.username=rootjdbc.slave01.password=123456
定義串連池
<!-- 配置串連池 --> <bean id="masterDataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close"> <!-- 資料庫驅動 --> <property name="driverClass" value="${jdbc.master.driver}" /> <!-- 相應驅動的jdbcUrl --> <property name="jdbcUrl" value="${jdbc.master.url}" /> <!-- 資料庫的使用者名稱 --> <property name="username" value="${jdbc.master.username}" /> <!-- 資料庫的密碼 --> <property name="password" value="${jdbc.master.password}" /> <!-- 檢查資料庫連接池中空閑串連的間隔時間,單位是分,預設值:240,如果要取消則設定為0 --> <property name="idleConnectionTestPeriod" value="60" /> <!-- 串連池中未使用的連結最大存活時間,單位是分,預設值:60,如果要永遠存活設定為0 --> <property name="idleMaxAge" value="30" /> <!-- 每個分區最大的串連數 --> <property name="maxConnectionsPerPartition" value="150" /> <!-- 每個分區最小的串連數 --> <property name="minConnectionsPerPartition" value="5" /> </bean> <!-- 配置串連池 --> <bean id="slave01DataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close"> <!-- 資料庫驅動 --> <property name="driverClass" value="${jdbc.slave01.driver}" /> <!-- 相應驅動的jdbcUrl --> <property name="jdbcUrl" value="${jdbc.slave01.url}" /> <!-- 資料庫的使用者名稱 --> <property name="username" value="${jdbc.slave01.username}" /> <!-- 資料庫的密碼 --> <property name="password" value="${jdbc.slave01.password}" /> <!-- 檢查資料庫連接池中空閑串連的間隔時間,單位是分,預設值:240,如果要取消則設定為0 --> <property name="idleConnectionTestPeriod" value="60" /> <!-- 串連池中未使用的連結最大存活時間,單位是分,預設值:60,如果要永遠存活設定為0 --> <property name="idleMaxAge" value="30" /> <!-- 每個分區最大的串連數 --> <property name="maxConnectionsPerPartition" value="150" /> <!-- 每個分區最小的串連數 --> <property name="minConnectionsPerPartition" value="5" /> </bean>
定義DataSource
<!-- 定義資料來源,使用自己實現的資料來源 --> <bean id="dataSource" class="cn.itcast.usermanage.spring.DynamicDataSource"> <!-- 設定多個資料來源 --> <property name="targetDataSources"> <map key-type="java.lang.String"> <!-- 這個key需要和程式中的key一致 --> <entry key="master" value-ref="masterDataSource"/> <entry key="slave" value-ref="slave01DataSource"/> </map> </property> <!-- 設定預設的資料來源,這裡預設走寫庫 --> <property name="defaultTargetDataSource" ref="masterDataSource"/> </bean>
配置交易管理以及動態切換資料來源切面
1、定義交易管理員
<!-- 定義交易管理員 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean>
2、定義事務策略
<!-- 定義事務策略 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--定義查詢方法都是唯讀 --> <tx:method name="query*" read-only="true" /> <tx:method name="find*" read-only="true" /> <tx:method name="get*" read-only="true" /> <!-- 主庫執行操作,事務傳播行為定義為預設行為 --> <tx:method name="save*" propagation="REQUIRED" /> <tx:method name="update*" propagation="REQUIRED" /> <tx:method name="delete*" propagation="REQUIRED" /> <!--其他方法使用預設事務策略 --> <tx:method name="*" /> </tx:attributes> </tx:advice>
3、定義切面
<!-- 定義AOP切面處理器 --> <bean class="cn.itcast.usermanage.spring.DataSourceAspect" id="dataSourceAspect" /> <aop:config> <!-- 定義切面,所有的service的所有方法 --> <aop:pointcut id="txPointcut" expression="execution(* xx.xxx.xxxxxxx.service.*.*(..))" /> <!-- 應用事務策略到Service切面 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/> <!-- 將切面應用到自訂的切面處理器上,-9999保證該切面優先順序最高執行 --> <aop:aspect ref="dataSourceAspect" order="-9999"> <aop:before method="before" pointcut-ref="txPointcut" /> </aop:aspect> </aop:config>
提高篇1:改進切面實現,使用事務策略規則匹配
之前的實現我們是將通過方法名匹配,而不是使用事務策略中的定義,我們使用交易管理策略中的規則匹配。
改進後的配置
<!-- 定義AOP切面處理器 --> <bean class="cn.itcast.usermanage.spring.DataSourceAspect" id="dataSourceAspect"> <!-- 指定事務策略 --> <property name="txAdvice" ref="txAdvice"/> <!-- 指定slave方法的首碼(非必須) --> <property name="slaveMethodStart" value="query,find,get"/> </bean>
改進後的實現
import java.lang.reflect.Field;import java.util.ArrayList;import java.util.List;import java.util.Map;import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.JoinPoint;import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;import org.springframework.transaction.interceptor.TransactionAttribute;import org.springframework.transaction.interceptor.TransactionAttributeSource;import org.springframework.transaction.interceptor.TransactionInterceptor;import org.springframework.util.PatternMatchUtils;import org.springframework.util.ReflectionUtils;/** * 定義資料來源的AOP切面,該類控制了使用Master還是Slave。 * * 如果交易管理中配置了事務策略,則採用配置的事務策略中的標記了ReadOnly的方法是用Slave,其它使用Master。 * * 如果沒有配置交易管理的策略,則採用方法名匹配的原則,以query、find、get開頭方法用Slave,其它用Master。 * * @author * */public class DataSourceAspect { private List<String> slaveMethodPattern = new ArrayList<String>(); private static final String[] defaultSlaveMethodStart = new String[]{ "query", "find", "get" }; private String[] slaveMethodStart; /** * 讀取交易管理中的策略 * * @param txAdvice * @throws Exception */ @SuppressWarnings("unchecked") public void setTxAdvice(TransactionInterceptor txAdvice) throws Exception { if (txAdvice == null) { // 沒有配置交易管理策略 return; } //從txAdvice擷取到策略配置資訊 TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource(); if (!(transactionAttributeSource instanceof NameMatchTransactionAttributeSource)) { return; } //使用反射技術擷取到NameMatchTransactionAttributeSource對象中的nameMap屬性值 NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource; Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap"); nameMapField.setAccessible(true); //設定該欄位可訪問 //擷取nameMap的值 Map<String, TransactionAttribute> map = (Map<String, TransactionAttribute>) nameMapField.get(matchTransactionAttributeSource); //遍曆nameMap for (Map.Entry<String, TransactionAttribute> entry : map.entrySet()) { if (!entry.getValue().isReadOnly()) {//判斷之後定義了ReadOnly的策略才加入到slaveMethodPattern continue; } slaveMethodPattern.add(entry.getKey()); } } /** * 在進入Service方法之前執行 * * @param point 切面對象 */ public void before(JoinPoint point) { // 擷取到當前執行的方法名 String methodName = point.getSignature().getName(); boolean isSlave = false; if (slaveMethodPattern.isEmpty()) { // 當前Spring容器中沒有配置事務策略,採用方法名匹配方式 isSlave = isSlave(methodName); } else { // 使用原則規則匹配 for (String mappedName : slaveMethodPattern) { if (isMatch(methodName, mappedName)) { isSlave = true; break; } } } if (isSlave) { // 標記為讀庫 DynamicDataSourceHolder.markSlave(); } else { // 標記為寫庫 DynamicDataSourceHolder.markMaster(); } } /** * 判斷是否為讀庫 * * @param methodName * @return */ private Boolean isSlave(String methodName) { // 方法名以query、find、get開頭的方法名走從庫 return StringUtils.startsWithAny(methodName, getSlaveMethodStart()); } /** * 萬用字元匹配 * * Return if the given method name matches the mapped name. * <p> * The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, as well as direct