原理解密 → Spring AOP 實現動態數據源(讀寫分離),底層原理是什麼
- 2020 年 4 月 13 日
- 筆記
開心一刻
女孩睡醒玩手機,收到男孩發來一條信息:我要去跟我喜歡的人表白了!
女孩的心猛的一痛,回了條信息:去吧,祝你好運!
男孩回了句:但是我沒有勇氣說不來,怕被打!
女孩:沒事的,我相信你!此時女孩已經傷心的流淚了
男孩:我已經到她家門口了,不敢敲門!
女孩擦了擦眼淚:不用怕,你是個好人,會有好報的!
男孩:那你來開下門吧,我在你家門口!
女孩不敢相信,趕緊跑去開門,看到他的那一刻傷心的淚水變成了感動
男孩拿出手裡那束玫瑰花說:你姐姐在家嗎?
前情回歸
一般來講,讀寫分離無非兩種實現方式。第一種是依靠數據庫中間件(比如:MyCat),也就是說應用程序連接到中間件,中間件幫我們做讀寫分離;第二種是應用程序自己做讀寫分離,結合 Spring AOP 實現讀寫分離
數據庫中間件的方式不做過多的闡述(誰讓你是配角!),有興趣的可以去查看
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 事務衝突 ,若衝突了該如何解決 ?