Spring MVC+Mybatis 多數據源配置

隨著業務的不斷擴張,應用壓力逐漸增大,特別是數據庫。不論從讀寫分離還是分庫的方法來提高應用的性能,都需要涉及到多數據源問題。本文主要介紹在Spring MVC+Mybatis下的多數據源配置。主要通過Spring提供的AbstractRoutingDataSource來實現多數據源。

1. 繼承AbstractRoutingDataSource

AbstractRoutingDataSource 是spring提供的一個多數據源抽象類。spring會在使用事務的地方來調用此類的determineCurrentLookupKey()方法來獲取數據源的key值。我們繼承此抽象類并實現此方法:

package com.ctitc.collect.manage.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 
 * @author zongbo
 * 實現spring多路由配置,由spring調用
 */
public class DataSourceRouter extends AbstractRoutingDataSource {
 
 // 獲取數據源名稱
 protected Object determineCurrentLookupKey() {
  return HandleDataSource.getDataSource();
 }

}

2. 線程內部數據源處理類

DataSourceRouter 類中通過HandleDataSource.getDataSource()獲取數據源的key值。此方法應該和線程綁定。

package com.ctitc.collect.manage.datasource;
/**
 * 線程相關的數據源處理類
 * @author zongbo
 *
 */
public class HandleDataSource {
 // 數據源名稱線程池
 private static final ThreadLocal<String> holder = new ThreadLocal<String>();

 /**
  * 設置數據源
  * @param datasource 數據源名稱
  */
 public static void setDataSource(String datasource) {
  holder.set(datasource);
 }
 /**
  * 獲取數據源
  * @return 數據源名稱
  */
 public static String getDataSource() {
  return holder.get();
 }
 /**
  * 清空數據源
  */
 public static void clearDataSource() {
  holder.remove();
 }
}

3. 自定義數據源注解類

對于spring來說,注解即簡單方便且可讀性也高。所以,我們也通過注解在service的方法前指定所用的數據源。我們先定義自己的注解類,其中value為數據源的key值。

package com.ctitc.collect.manage.datasource;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 數據源注解類
 * @author zongbo
 *
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value();
}

4. AOP 攔截service并切換數據源

指定注解以后,我們可以通過AOP攔截所有service方法,在方法執行之前獲取方法上的注解:即數據源的key值。

package com.ctitc.collect.manage.datasource;

import java.lang.reflect.Method;
import java.text.MessageFormat;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * 切換數據源(不同方法調用不同數據源)
 */
@Aspect
@Component
@Order(1) //請注意:這里order一定要小于tx:annotation-driven的order,即先執行DataSourceAspect切面,再執行事務切面,才能獲取到最終的數據源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DataSourceAspect {
    static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * 切入點 service包及子孫包下的所有類
     */
    @Pointcut("execution(* com.ctitc.collect.service..*.*(..))")
    public void aspect() {
    }

    /**
     * 配置前置通知,使用在方法aspect()上注冊的切入點
     */
    @Before("aspect()")
    public void before(JoinPoint point) {
        Class<?> target = point.getTarget().getClass();
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod() ;
        DataSource dataSource = null ;
        //從類初始化
        dataSource = this.getDataSource(target, method) ;
        //從接口初始化
        if(dataSource == null){
            for (Class<?> clazz : target.getInterfaces()) {
                dataSource = getDataSource(clazz, method);
                if(dataSource != null){
                    break ;//從某個接口中一旦發現注解,不再循環
                }
            }
        }
        
        if(dataSource != null && !StringUtils.isEmpty(dataSource.value()) ){
            HandleDataSource.setDataSource(dataSource.value());
        }
    }

    @After("aspect()")
    public void after(JoinPoint point) {
        //使用完記得清空
        HandleDataSource.setDataSource(null);
    }
    
    
    /**
     * 獲取方法或類的注解對象DataSource
     * @param target    類class
     * @param method    方法
     * @return DataSource
     */
    public DataSource getDataSource(Class<?> target, Method method){
        try {
            //1.優先方法注解
            Class<?>[] types = method.getParameterTypes();
            Method m = target.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                return m.getAnnotation(DataSource.class);
            }
            //2.其次類注解
            if (target.isAnnotationPresent(DataSource.class)) {
                return target.getAnnotation(DataSource.class);
            }
            
        } catch (Exception e) {
            e.printStackTrace();
            logger.error(MessageFormat.format("通過注解切換數據源時發生異常[class={0},method={1}]:"
                    , target.getName(), method.getName()),e)  ;
        }
        return null ;
    }
}

5. 數據源配置

假設我有兩個庫:業務庫和訂單庫。先要配置這兩個數據源

  • 業務數據源
<bean id="busiDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
  <description>業務數據源</description>
     <!-- 數據庫基本信息配置 -->
        <property name="driverClassName" value="${busi.driverClassName}" />
        <property name="url" value="${busi.url}" />
        <property name="username" value="${busi.username}" />
        <property name="password" value="${busi.password}" />        
        <!-- 初始化連接數量 -->
        <property name="initialSize" value="${druid.initialSize}" />
        <!-- 最大并發連接數 -->
        <property name="maxActive" value="${druid.maxActive}" />
        <!-- 最小空閑連接數 -->
        <property name="minIdle" value="${druid.minIdle}" />
        <!-- 配置獲取連接等待超時的時間 -->     
        <property name="maxWait" value="${druid.maxWait}" />
        <!-- 超過時間限制是否回收 -->
        <property name="removeAbandoned" value="${druid.removeAbandoned}" />
        <!-- 超過時間限制多長; -->
        <property name="removeAbandonedTimeout" value="${druid.removeAbandonedTimeout}" />
        <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />
        <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />
        <!-- 用來檢測連接是否有效的sql,要求是一個查詢語句-->
        <property name="validationQuery" value="${druid.validationQuery}" />
        <!-- 申請連接的時候檢測 -->
        <property name="testWhileIdle" value="${druid.testWhileIdle}" />
        <!-- 申請連接時執行validationQuery檢測連接是否有效,配置為true會降低性能 -->
        <property name="testOnBorrow" value="${druid.testOnBorrow}" />
        <!-- 歸還連接時執行validationQuery檢測連接是否有效,配置為true會降低性能  -->
        <property name="testOnReturn" value="${druid.testOnReturn}" />
        <!-- 打開PSCache,并且指定每個連接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="${druid.poolPreparedStatements}" />     
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}" />
        <!--屬性類型是字符串,通過別名的方式配置擴展插件,常用的插件有:                 
                監控統計用的filter:stat
                日志用的filter:log4j
               防御SQL注入的filter:wall -->
        <property name="filters" value="${druid.filters}" />  
 </bean>  
  • 訂單數據源
<bean id="orderDataSource" class="com.alibaba.druid.pool.DruidDataSource"  parent="busiDataSource">   
     <description>訂單數據源</description>
        <property name="driverClassName" value="${order.driverClassName}" />
        <property name="url" value="${order.url}" />
        <property name="username" value="${order.username}" />
        <property name="password" value="${order.password}" />        
        
 </bean>  
  • dataSource 則是剛剛實現的DataSourceRouter,且需要指定此類的 targetDataSources屬性和 defaultTargetDataSource屬性。

targetDataSources :數據源列表,key-value形式,即上面配置的兩個數據源
defaultTargetDataSource:默認數據源,如果未指定數據源 或者指定的數據源不存在的話 默認使用這個數據源

<bean id="dataSource" class="com.ctitc.collect.manage.datasource.DataSourceRouter" lazy-init="true">
  <description>多數據源路由</description>
  <property name="targetDataSources">
   <map key-type="java.lang.String" value-type="javax.sql.DataSource">
    <!-- write -->
    <entry key="busi" value-ref="busiDataSource" />
    <entry key="order" value-ref="orderDataSource" />
   </map>
  </property>
  <!-- 默認數據源,如果未指定數據源 或者指定的數據源不存在的話 默認使用這個數據源 -->
  <property name="defaultTargetDataSource" ref="busiDataSource" />
  
 </bean>

6. AOP的順序問題

由于我使用的注解式事務,和我們的AOP數據源切面有一個順序的關系。數據源切換必須先執行,數據庫事務才能獲取到正確的數據源。所以要明確指定 注解式事務和 我們AOP數據源切面的先后順序。

  • 我們數據源切換的AOP是通過注解來實現的,只需要在AOP類上加上一個order(1)注解即可,其中1代表順序號。
  • 注解式事務的是通過xml配置啟動
<tx:annotation-driven transaction-manager="transactionManager"
  proxy-target-class="true" order="2" />

7. 示例Demo

在每個service方法前使用@DataSource("數據源key")注解即可。

@Override
 @DataSource("busi")
 @Transactional(readOnly = true, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
 public List<BasVersion> test1() {
  // TODO Auto-generated method stub
  return coreMapper.getVersion();
 }
 
 @Override
 @DataSource("order")
 @Transactional(readOnly = true, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
 public List<BasVersion> test2() {
  // TODO Auto-generated method stub
  return coreMapper.getVersion();
 }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容