1. 程式人生 > >使用Spring AOP切面解決資料庫讀寫分離

使用Spring AOP切面解決資料庫讀寫分離

為了減輕資料庫的壓力,一般會使用資料庫主從(master/slave)的方式,但是這種方式會給應用程式帶來一定的麻煩,比如說,應用程式如何做到把資料寫到master庫,而讀取資料的時候,從slave庫讀取。如果應用程式判斷失誤,把資料寫入到slave庫,會給系統造成致命的打擊。

解決讀寫分離的方案很多,常用的有SQL解析、動態設定資料來源。SQL解析主要是通過分析sql語句是insert/select/update/delete中的哪一種,從而對應選擇主從。而動態設定資料來源,則是通過攔截方法名稱的方式來決定主從的,例如:save*(),insert*() 形式的方法使用master庫,select()開頭的,使用slave庫。蠻多公司會使用在方法上標上自定義的@Master、@Slave之類的標籤來選擇主從,也有公司直接就呼叫setxxMaster,setxxSlave之類的程式碼進行主從選擇。

下面我主要介紹一下基於Spring AOP動態設定資料來源這種方式。注意這篇文章是基於自己專案的實際情況的,不是通用的方案,請知曉。

原理圖


Spring AOP的切面主要的職責是攔截Mybatis的Mapper介面,通過判斷Mapper介面中的方法名稱來決定主從。

Spring AOP 切面配置

<aop:config expose-proxy="true">

<aop:pointcut id="txPointcut" expression="execution(* com.test..persistence..*.*(..))" />

<aop:aspect ref="readWriteInterceptor" order="1">

<aop:around pointcut-ref="txPointcut" method="readOrWriteDB"/>

</aop:aspect>

</aop:config>

 

<bean id="readWriteInterceptor" class="com.test.ReadWriteInterceptor">

   <property name="readMethodList">

     <list>

       <value>query*</value>

       <value>use*</value>

       <value>get*</value>

       <value>count*</value>

       <value>find*</value>

       <value>list*</value>

       <value>search*</value>

    </list>

  </property>

<property name="writeMethodList">

    <list>

        <value>save*</value>

        <value>add*</value>

        <value>create*</value>

        <value>insert*</value>

        <value>update*</value>

        <value>merge*</value>

        <value>del*</value>

        <value>remove*</value>

        <value>put*</value>

        <value>write*</value>

   </list>

</property>

</bean>


把所有Mybatis介面類都放置在persistence下。配置的切面類是ReadWriteInterceptor。這樣當Mapper介面的方法被呼叫時,會先呼叫這個切面類的readOrWriteDB方法。在這裡需要注意<aop:aspect>中的order="1" 配置,主要是為了解決切面於切面之間的優先順序問題,因為整個系統中不太可能只有一個切面類。

Spring AOP 切面類實現

public class ReadWriteInterceptor {
   private static final String DB_SERVICE = "dbService";
   private List<String> readMethodList = new ArrayList<String>();
   private List<String> writeMethodList = new ArrayList<String>();
   public Object readOrWriteDB(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        if (isChooseReadDB(methodName)) {
            //選擇slave資料來源
        } else if (isChooseWriteDB(methodName)) {
           //選擇master資料來源
        } else {
          //選擇master資料來源
        }
       return pjp.proceed();
}

 private boolean isChooseWriteDB(String methodName) {
     for (String mappedName : this.writeMethodList) {
         if (isMatch(methodName, mappedName)) {
             return true;
         }
     }
    return false;
}

 private boolean isChooseReadDB(String methodName) {
    for (String mappedName : this.readMethodList) {
       if (isMatch(methodName, mappedName)) {
           return true;
       }
    }
    return false;
}

 private boolean isMatch(String methodName, String mappedName) {
    return PatternMatchUtils.simpleMatch(mappedName, methodName);
}

 public List<String> getReadMethodList() {
    return readMethodList;
 }

 public void setReadMethodList(List<String> readMethodList) {
   this.readMethodList = readMethodList;
}

 public List<String> getWriteMethodList() {
    return writeMethodList;
 }

 public void setWriteMethodList(List<String> writeMethodList) {
    this.writeMethodList = writeMethodList;
}

覆蓋DynamicDataSource類中的getConnection方法

ReadWriteInterceptor中的readOrWriteDB方法只是決定選擇主還是從,我們還必須覆蓋資料來源的getConnection方法,以便獲取正確的connection。一般來說,是一主多從,即一個master庫,多個slave庫的,所以還得解決多個slave庫之間負載均衡、故障轉移以及失敗重連線等問題。

1、負載均衡問題,slave不多,系統併發讀不高的話,直接使用隨機數訪問也是可以的。就是根據slave的臺數,然後產生隨機數,隨機的訪問slave。

2、故障轉移,如果發現connection獲取不到了,則把它從slave列表中移除,等其回覆後,再加入到slave列表中

3、失敗重連,第一次連線失敗後,可以多嘗試幾次,如嘗試10次。

處理業務方法中的@Transactional註解

我參與的這個專案,大部分業務程式碼是不需要事務的,只有極個別情況需要。那麼按照上面提到的方案,如果不對業務方法中@Transactional註解進行特殊處理的話,主從的選擇會出現問題。大家都知道,如果使用了Spring的事務,那麼在同一個業務方法內,只會呼叫一次資料來源的getConnection方法,如果該業務方法內,呼叫的mapper介面剛好以select開頭的,就會選擇slave庫,那麼接下來呼叫以insert開頭的mapper介面方法時,會把資料寫入到slave庫。如何解決這個問題呢?必須在進入標有@Transactional註解的業務方法前,指定選擇master主庫。可以通過覆蓋DataSourceTransactionManager中的doBegin方法,如下:

public class MyTransactionManager extendsDataSourceTransactionManager{

@Override

protected void doBegin(Object transaction, TransactionDefinitiondefinition) {

//選擇master資料庫

super.doBegin(transaction, definition);

}

}

這樣既可以避免,把資料寫入到從庫的問題。

總結

本人的解決方案是基於專案實際的,不一定合適你,我只是展示瞭解決方案而已。當然你可以選擇開源的框架,像阿里的Cobar360Atlas