原理解密 → Spring AOP 實現動態數據源(讀寫分離),底層原理是什麼

  • 2020 年 4 月 13 日
  • 筆記

開心一刻

  女孩睡醒玩手機,收到男孩發來一條信息:我要去跟我喜歡的人表白了!
  女孩的心猛的一痛,回了條信息:去吧,祝你好運!
  男孩回了句:但是我沒有勇氣說不來,怕被打!
  女孩:沒事的,我相信你!此時女孩已經傷心的流淚了
  男孩:我已經到她家門口了,不敢敲門!
  女孩擦了擦眼淚:不用怕,你是個好人,會有好報的!
  男孩:那你來開下門吧,我在你家門口!
  女孩不敢相信,趕緊跑去開門,看到他的那一刻傷心的淚水變成了感動
  男孩拿出手裡那束玫瑰花說:你姐姐在家嗎?

前情回歸

  一般來講,讀寫分離無非兩種實現方式。第一種是依靠數據庫中間件(比如:MyCat),也就是說應用程序連接到中間件,中間件幫我們做讀寫分離;第二種是應用程序自己做讀寫分離,結合 Spring AOP 實現讀寫分離

  數據庫中間件的方式不做過多的闡述(誰讓你是配角!),有興趣的可以去查看

    Mycat – 實現數據庫的讀寫分離與高可用

    Mycat – 高可用與負載均衡實現,滿滿的乾貨!

   spring集成mybatis實現mysql讀寫分離,簡單介紹了通過 Spring AOP 從應用程序層面實現讀寫分離;讀寫分離效果是達到了,可我們知道為什麼那麼做就能實現讀寫分離嗎 ?知道的請快點走開

  接下來請好好欣賞我的表演

原理解密

  我們逐個講解其中涉及的點,然後串起來理解讀寫分離的底層原理

  Spring AOP

    AOP:Aspect Oriented Program

    關於 Spring AOP,相信大家耳熟能詳,它是對 OOP 的一種補充,OOP 是縱向的,AOP 則是橫向的

    

    如上圖所示,OOP 屬於一種縱向拓展,AOP 則是一種橫向拓展。AOP 依託於 OOP,將公共功能代碼抽象出來作為一個切面,減少重複代碼量,降低耦合

    AOP 的底層實現是動態代理,具體的表現形式粗略如下

    

    對 Spring AOP 有個大致了解了,我們就可以接着往下看了 

  Spring 數據源

    無論是 Spring JDBC,還是 Hibernate,亦或是 MyBatis,其實都是對 JDBC 的封裝;對於JDBC,我們不要太熟,大體流程如下

    

    然而,在實際應用中,我們往往不會直接使用 JDBC,而是使用 ORM,ORM 會封裝上述的流程,也就說我們不再需要關注了;MyBatis 使用步驟大致如下

    

    我們以 SpringBoot + pagehelper + Druid(ssm) 為例,來看看具體是怎麼獲取 Connection 對象的

    

    可以看到,如果事務管理器中存在 Connection 對象,則直接返回,否則從數據源中獲取返回(同時也賦值給了事務管理器);當取到 Connection 對象後,後續的流程大家就非常清楚了

    然而我們不需要關注 Connection 對象,只需要關注數據源,為什麼呢 ? 因為我們的配置文件中配置的是數據源而不是 Connection,是不是很有道理 ?

  ThreadLocal

    如果我們需要在各層之間進行參數的傳遞,實現方式有哪些 ?

    最常見的方式可能就是方法參數,但還有一種容易忽略的方式:ThreadLocal,可以在當前線程內傳遞參數,減少方法的參數個數

    關於 ThreadLocal,有興趣的可以查看:結合ThreadLocal來看spring事務源碼,感受下清泉般的洗滌!

  當我們熟悉上面的三點後,後面的就好理解了,接着往下看

  動態數據源

    一個數據源只能對應一個數據庫,如果我們有多個數據庫(一主多從),那麼就需要配置多個數據源,類似如下

    <!-- master數據源 -->
    <bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!-- 基本屬性 url、user、password -->  
        <property name="driverClassName" value="${jdbc.driverClassName}" />  
        <property name="url" value="${jdbc.url}" />  
        <property name="username" value="${jdbc.username}" />  
        <property name="password" value="${jdbc.password}" />  
        <property name="initialSize" value="${jdbc.initialSize}" />  
        <property name="minIdle" value="${jdbc.minIdle}" />   
        <property name="maxActive" value="${jdbc.maxActive}" />  
        <property name="maxWait" value="${jdbc.maxWait}" />
        <!-- 超過時間限制是否回收 -->
        <property name="removeAbandoned" value="${jdbc.removeAbandoned}" />
        <!-- 超過時間限制多長; -->
        <property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}" />
        <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
        <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
        <!-- 用來檢測連接是否有效的sql,要求是一個查詢語句-->
        <property name="validationQuery" value="${jdbc.validationQuery}" />
        <!-- 申請連接的時候檢測 -->
        <property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
        <!-- 申請連接時執行validationQuery檢測連接是否有效,配置為true會降低性能 -->
        <property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
        <!-- 歸還連接時執行validationQuery檢測連接是否有效,配置為true會降低性能  -->
        <property name="testOnReturn" value="${jdbc.testOnReturn}" />
    </bean>

    <!-- slave數據源 -->
    <bean id="slaveDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${slave.jdbc.driverClassName}" />  
        <property name="url" value="${slave.jdbc.url}" />  
        <property name="username" value="${slave.jdbc.username}" />  
        <property name="password" value="${slave.jdbc.password}" />  
        <property name="initialSize" value="${slave.jdbc.initialSize}" />  
        <property name="minIdle" value="${slave.jdbc.minIdle}" />   
        <property name="maxActive" value="${slave.jdbc.maxActive}" />  
        <property name="maxWait" value="${slave.jdbc.maxWait}" />
        <property name="removeAbandoned" value="${slave.jdbc.removeAbandoned}" />
        <property name="removeAbandonedTimeout" value="${slave.jdbc.removeAbandonedTimeout}" />
        <property name="timeBetweenEvictionRunsMillis" value="${slave.jdbc.timeBetweenEvictionRunsMillis}" />
        <property name="minEvictableIdleTimeMillis" value="${slave.jdbc.minEvictableIdleTimeMillis}" />
        <property name="validationQuery" value="${slave.jdbc.validationQuery}" />
        <property name="testWhileIdle" value="${slave.jdbc.testWhileIdle}" />
        <property name="testOnBorrow" value="${slave.jdbc.testOnBorrow}" />
        <property name="testOnReturn" value="${slave.jdbc.testOnReturn}" />
    </bean>

View Code

    可是事務管理器中只有一個數據源的引用

    

    那怎麼對應我們配置文件中的多個數據源呢 ?其實,我們可以自定義一個類 DynamicDataSource 來實現 DataSource,DynamicDataSource 中存儲我們配置的多數據源,然後將 DynamicDataSource 的實例配置給事務管理器;當從事務管理器獲取 Connection 對象的時候,會從 DynamicDataSource 實例獲取,然後再由 DynamicDataSource 根據 routeKey 路由到某個具體的數據源,從中獲取 Connection;大體流程如下

    

    Spring 也考慮到了這一點,提供了一個抽象類:AbstractRoutingDataSource,DynamicDataSource 繼承它可以為我們省非常多的代碼

public class DynamicDataSource extends AbstractRoutingDataSource
{

    /**
     * 獲取與數據源相關的key 此key是Map<String,DataSource> resolvedDataSources 中與數據源綁定的key值
     * 在通過determineTargetDataSource獲取目標數據源時使用
     */
    @Override
    protected Object determineCurrentLookupKey()
    {
        return HandleDataSource.getDataSource();
    }

}

View Code

    配置文件中增加如下配置

<!-- 動態數據源,根據service接口上的註解來決定取哪個數據源 -->
    <bean id="dataSource" class="com.yzb.util.DynamicDataSource">  
        <property name="targetDataSources">      
          <map key-type="java.lang.String">      
              <!-- write or slave -->    
             <entry key="slave" value-ref="slaveDataSource"/>      
             <!-- read or master   -->  
             <entry key="master" value-ref="masterDataSource"/>      
          </map>               
        </property>   
        <property name="defaultTargetDataSource" ref="masterDataSource"/>      
      
    </bean>
    
    <!-- Mybatis文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:mybatis-config.xml" /> 
        <property name="dataSource" ref="dataSource" />
        <!-- 映射文件路徑 -->
        <property name="mapperLocations" value="classpath*:dbmappers/*.xml" />
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.yzb.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>

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

View Code

    但是問題又來了,這個 routeKey 怎麼處理,也就說 DynamicDataSource 怎麼知道用哪個數據源 ? AbstractRoutingDataSource 提供了一個方法: determineCurrentLookupKey 我們只需要實現它,DynamicDataSource 就知道是使用哪個 lookupKey (routeKey 在 Spring 中的命名)了;determineCurrentLookupKey 具體該如何實現了,我們可以結合 ThreadLocal 來實現;整個流程大致如下

    

    一旦我們在切面中指定了 lookupKey,那麼後續就會使用 lookupKey 對應的數據源來操作數據庫了

  自此,相信大家已經明白了動態數據源的底層原理

總結

  Spring AOP → 將我們指定的 lookupKey 放入 ThreadLocal

  ThreadLocal → 線程內共享 lookupKey

  DynamicDataSource → 對多數據源進行封裝,根據 ThreadLocal 中的 lookupKey 動態選擇具體的數據源

  如果我們對其中的某個環節不懂,可以試着刪掉它,然後看這個流程能否正常串起來,這樣就能明白各個環節的作用了

懸念

  Spring AOP 實現多數據源,是否與 Spring 事務衝突 ,若衝突了該如何解決 ?

參考

  什麼是面向切面編程AOP?

  Spring AOP就是這麼簡單啦