Spring+Mybatis動態切換數據源

来源:https://www.cnblogs.com/zackzhuzi/archive/2018/01/26/8359940.html
-Advertisement-
Play Games

運營平臺至少要連三個庫:運營庫,A庫,B庫,並且希望達到針對每個功能請求能夠自動切換到對應的數據源(我最終實現是針對Service的方法級別進行切換的,也可以實現針對每個DAO層的方法進行切換。我們系統的功能是相互之間比較獨立的)。 ...


功能需求是公司要做一個大的運營平臺:

1、運營平臺有自身的資料庫,維護用戶、角色、菜單、部分以及許可權等基本功能。

2、運營平臺還需要提供其他不同服務(服務A,服務B)的後臺運營,服務A、服務B的資料庫是獨立的。

所以,運營平臺至少要連三個庫:運營庫,A庫,B庫,並且希望達到針對每個功能請求能夠自動切換到對應的數據源(我最終實現是針對Service的方法級別進行切換的,也可以實現針對每個DAO層的方法進行切換。我們系統的功能是相互之間比較獨立的)。

第一步:配置多數據源

1、定義數據源:

我採用的數據源是阿裡的DruidDataSource(用DBCP也行,這個隨便)。配置如下:

<!-- op dataSource -->
    <bean id="opDataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <property name="url" value="${db.master.url}" />
        <property name="username" value="${db.master.user}" />
        <property name="password" value="${db.master.password}" />
        <property name="driverClassName" value="${db.master.driver}" />
        <property name="initialSize" value="5" />
        <property name="maxActive" value="100" />
        <property name="minIdle" value="10" />
        <property name="maxWait" value="60000" />
        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="timeBetweenEvictionRunsMillis" value="600000" />
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="1800" />
        <property name="logAbandoned" value="true" />
        <!-- 配置監控統計攔截的filters -->
        <property name="filters" value="config,mergeStat,wall,log4j2" />
        <property name="connectionProperties" value="config.decrypt=true" />
    </bean>

    <!-- serverA dataSource -->
    <bean id="serverADataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <property name="url" value="${db.serverA.master.url}" />
        <property name="username" value="${db.serverA.master.user}" />
        <property name="password" value="${db.serverA.master.password}" />
        <property name="driverClassName" value="${db.serverA.master.driver}" />
        <property name="initialSize" value="5" />
        <property name="maxActive" value="100" />
        <property name="minIdle" value="10" />
        <property name="maxWait" value="60000" />
        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="timeBetweenEvictionRunsMillis" value="600000" />
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="1800" />
        <property name="logAbandoned" value="true" />
        <!-- 配置監控統計攔截的filters -->
        <property name="filters" value="config,mergeStat,wall,log4j2" />
        <property name="connectionProperties" value="config.decrypt=true" />
    </bean>

    <!-- serverB dataSource -->
    <bean id="serverBDataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <property name="url" value="${db.serverB.master.url}" />
        <property name="username" value="${db.serverB.master.user}" />
        <property name="password" value="${db.serverB.master.password}" />
        <property name="driverClassName" value="${db.serverB.master.driver}" />
        <property name="initialSize" value="5" />
        <property name="maxActive" value="100" />
        <property name="minIdle" value="10" />
        <property name="maxWait" value="60000" />
        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="timeBetweenEvictionRunsMillis" value="600000" />
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="1800" />
        <property name="logAbandoned" value="true" />
        <!-- 配置監控統計攔截的filters -->
        <property name="filters" value="config,mergeStat,wall,log4j2" />
        <property name="connectionProperties" value="config.decrypt=true" />
    </bean>
View Code

 我配置了三個數據源:oPDataSource(運營平臺本身的數據源),serverADataSource,serverBDataSource。

2、配置multipleDataSource

multipleDataSource相當於以上三個數據源的一個代理,真正與Spring/Mybatis相結合的時multipleDataSource,和單獨配置的DataSource使用沒有分別:

    <!-- Spring整合Mybatis:配置multipleDatasource -->
    <bean id="sqlSessionFactory"
        class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="multipleDataSource" />
        <!-- 自動掃描Mapping.xml文件 -->
        <property name="mapperLocations">
            <list>
                <value>classpath*:/sqlMapperXml/*.xml</value>
                <value>classpath*:/sqlMapperXml/*/*.xml</value>
            </list>
        </property>
        <property name="configLocation" value="classpath:xml/mybatis-config.xml"></property>
        <property name="typeAliasesPackage" value="com.XXX.platform.model" />
        <property name="globalConfig" ref="globalConfig" />
        <property name="plugins">
            <array>
                <!-- 分頁插件配置 -->
                <bean id="paginationInterceptor"
                    class="com.baomidou.mybatisplus.plugins.PaginationInterceptor">
                    <property name="dialectType" value="mysql" />
                    <property name="optimizeType" value="aliDruid" />
                </bean>
            </array>
        </property>
    </bean>
    
    <!-- MyBatis 動態實現 -->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 對Dao 介面動態實現,需要知道介面在哪 -->
        <property name="basePackage" value="com.XXX.platform.mapper" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>    
    <!-- MP 全局配置 -->
    <bean id="globalConfig" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
        <property name="idType" value="0" />
        <property name="dbColumnUnderline" value="true" />
    </bean>


    <!-- 事務管理配置multipleDataSource -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="multipleDataSource"></property>
    </bean>
View Code

瞭解了multipleDataSource所處的位置之後,接下來重點看下multipleDataSource怎麼實現,配置文件如下:

<bean id="multipleDataSource" class="com.xxxx.platform.commons.db.MultipleDataSource">
        <property name="defaultTargetDataSource" ref="opDataSource" />
        <property name="targetDataSources">
            <map>
                <entry key="opDataSource" value-ref="opDataSource" />
                <entry key="serverADataSource" value-ref="serverADataSource" />
                <entry key="serverBDataSource" value-ref="serverBDataSource" />
            </map>
        </property>
    </bean>

實現的Java代碼如下,不需要過多的解釋,很一目瞭然:

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 
 * @ClassName: MultipleDataSource
 * @Description: 配置多個數據源<br>
 * @author: yuzhu.peng
 * @date: 2018年1月12日 下午4:37:25
 */
public class MultipleDataSource extends AbstractRoutingDataSource {
    
    private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<String>();
    
    public static void setDataSourceKey(String dataSource) {
        dataSourceKey.set(dataSource);
    }
    
    @Override
    protected Object determineCurrentLookupKey() {
        return dataSourceKey.get();
    }
    
    public static void removeDataSourceKey() {
        dataSourceKey.remove();
    }
}

繼承自spring的AbstractRoutingDataSource,實現抽象方法determineCurrentLookupKey,這個方法會在每次獲得資料庫連接Connection的時候之前,決定本次連接的數據源Datasource,可以看下Spring的代碼就很清晰了:

    /*獲取連接*/
    public Connection getConnection()
        throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        /*此處的determineCurrentLookupKey為抽象介面,獲取具體的數據源名稱*/
        Object lookupKey = determineCurrentLookupKey();
        DataSource 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;
    }
    /*抽象介面:也即我們的multipleDataSource實現的介面*/
    protected abstract Object determineCurrentLookupKey();
View Code

 

第二步:每次請求(Service方法級別)動態切換數據源

 實現思路是利用Spring的AOP思想,攔截每次的Service方法調用,然後根據方法的整體路徑名,動態切換multipleDataSource中的數據的key。我們的項目,針對不同服務也即不同資料庫的操作,是彼此之間互相獨立的,不太建議在同一個service方法中調用不同的數據源,這樣的話需要將動態判斷是否需要切換的頻次(AOP攔截的頻次)放在DAO級別,也就是SQL級別。另外,還不方便進行事務管理。

我們來看動態切換數據源的AOP實現:

import java.lang.reflect.Proxy;

import org.apache.commons.lang.ClassUtils;
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.springframework.core.annotation.Order;

/**
 * 數據源切換AOP
 * 
 * @author yuzhu.peng
 * @since 2018-01-15
 */
@Aspect
@Order(1)
public class MultipleDataSourceInterceptor {
    /**
     * 攔截器對所有的業務實現類請求之前進行數據源切換 特別註意,由於用到了多數據源,Mapper的調用最好只在*ServiceImpl,不然調用到非預設數據源的表時,會報表不存在的異常
     * 
     * @param joinPoint
     * @throws Throwable
     */
    @Before("execution(* com.xxxx.platform.service..*.*ServiceImpl.*(..))")
    public void setDataSoruce(JoinPoint joinPoint)
        throws Throwable {
        Class<?> clazz = joinPoint.getTarget().getClass();
        String className = clazz.getName();
        if (ClassUtils.isAssignable(clazz, Proxy.class)) {
            className = joinPoint.getSignature().getDeclaringTypeName();
        }
        // 對類名含有serverA的設置為serverA數據源,否則預設為後臺的數據源
        if (className.contains(".serverA.")) {
            MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_serverA);
        }
        else if (className.contains(".serverB.")) {
            MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_serverB);
        }
        else {
            MultipleDataSource.setDataSourceKey(DBConstant.DATA_SOURCE_OP);
        }
    }
    
    /**
     * 當操作完成時,釋放當前的數據源 如果不釋放,頻繁點擊時會發生數據源衝突,本是另一個數據源的表,結果跑到另外一個數據源去,報表不存在
     * 
     * @param joinPoint
     * @throws Throwable
     */
    @After("execution(* com.xxxx.service..*.*ServiceImpl.*(..))")
    public void removeDataSoruce(JoinPoint joinPoint)
        throws Throwable {
        MultipleDataSource.removeDataSourceKey();
    }
}

攔截所有的ServiceImpl方法,根據方法的全限定名去判斷屬於那個數據源的功能,然後選擇相應的數據源,發放執行完後,釋放當前的數據源。註意我用到了Spring的 @Order,註解,接下來會講到,當定義多個AOP的時候,order是很有用的。

 

其他:

一開始項目中並沒有引入事務,所以一切都OK,每次都能訪問到正確的數據源,當加入SPring的事務管理後,不能動態切換數據源了(也好像是事務沒有生效,反正是二者沒有同時有效),後來發現原因是AOP的執行順序問題,所以用到了上邊提到的SPring的Order:

 

 

 

 order越小,先被執行。至此,既可以動態切換數據源,又可以成功用事務(在同一個數據源)。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、基礎設置: 二、配置nginx.conf文件,在原來的vhost中增加如下代碼: 三、重啟nginx: ...
  • 1、通知DBA停庫; 串列登陸伺服器 2、備份系統信息 mkdir p /bakinfo df h /bakinfo/df.txt_ ps ef /bakinfo/ps.txt_ ip a /bakinfo/ip.txt_ netstat rn /bakinfo/netstat.txt_ free ...
  • 1、打開cmd ,輸入 F: // 切換到Apache安裝路徑,我的Apache安裝目錄在 F盤 2、cd F:\Apache\bin 3、set "openssl_conf = F:\Apache\conf\openssl.cnf" 臨時設置openssl_conf路徑,也可在環境變數中建新項目, ...
  • 說到 ___hash table___ 有兩個東西是我們經常會碰到的,首先就是 ___hash 碰撞___ 問題,__redis dict__ 是採用鏈地址法來解決,___dictEntry->next___ 就是指向下個衝突 __key__ 的節點。 還有一個經常碰到的就是 __rehash... ...
  • 進入redis目錄下 redis-cli -h IP -p 埠 -a 密碼 flushall ...
  • 用SQL語句添加刪除修改欄位 1.增加欄位 alter table docdsp add dspcode char(200) 2.刪除欄位 ALTER TABLE table_NAME DROP COLUMN column_NAME 3.修改欄位類型 ALTER TABLE table_name A ...
  • 在安裝完Oracle10g和創建完oracle資料庫之後,想用資料庫自帶的用戶scott登錄,看看連接是否成功。 問題: 在cmd命令中,用“sqlplus scott/ tiger”登錄時,老是提示如下信息: ERROR:ORA-28000:賬戶已被鎖定。 解決辦法: 在cmd命令提示符中可直接登 ...
  • Oracle導出導入表(.sql、.dmp文件)兩種方法 方法一:.sql文件的導出與導入 導出步驟 使用PL/SQL Developer登錄你需要備份的資料庫; 選擇工具->導出用戶對象; 在對象列表中選擇需要備份的對象,再選擇一個sql類型的輸出文件,點擊【導出】,這隻是導出數據結構; 選擇工具 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...