在實際的項目中,我們很少就使用一個數據庫,出于災難恢復或者負載均衡之類目的考慮,生產環境中通常都會存在多臺數據庫服務器,相應的,在應用程序對這些數據庫進行數據訪問的時候, 我們通常就會碰到一個比較常見的問題,即如何管理數據訪問過程中牽扯的多個數據源。
下面我們不妨從兩個角度來闡述一下在應用程序中如何對多個數據源進行管理…
所謂“ 主權獨立 ”是指系統中的每一個數據源都對外獨立承擔暴露數據庫資源的職能:
具體的應用場景可能是:
- 每一個數據庫所存儲的數據性質不同,比如數據庫A存儲重要的交易信息,數據庫B存儲次要的系統管理 信息等等, 如果要訪問交易信息,那么通常可以明確指定使用對應數據庫A的dataSourceA進行數據訪問;如果要訪問系統管理信息,則明確指定使用對應數據庫B 的dataSourceB,其他依次類推。
- 每一個數據庫分別承擔不同的數據訪問請求形式,比如數據庫A只允許更新操作不允許查詢,數據庫B只允許查詢不允許更新等, 這個時候,也是可以明確指定使用哪一個dataSource進行數據訪問;
當然,類似的場景并非只有這些,但總的意圖是相似的,那就是每一個dataSource的職能對于使用它們的客戶端來說足夠明確,完全是各自獨立使 用。 筆者經歷的FX項目中就使用了這樣的多數據源管理方式。在FX中,設置的MAIN數據庫主要存儲顧客或者銀行與FX Broker之間的交易信息,設置的INFO數據庫主要存儲匯率以及系統履歷之類的信息 [ 32 ] 。 通常情況下,針對交易以及匯率之間的信息存儲在邏輯上是可以分開進行的,所以現在,應用程序對應的spring配置內容基本如下:
<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="${main.jdbcUrl}"/> <property name="driverClassName" value="${main.driver}"/> <property name="username" value="${main.username}"/> <property name="password" value="${main.passwork}"/> <!-- other property settings --> </bean> <bean id="infoDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="${info.jdbcUrl}"/> <property name="driverClassName" value="${info.driver}"/> <property name="username" value="${info.username}"/> <property name="password" value="${info.password}"/> <!-- other property settings --> </bean> <bean id="mainJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="mainDataSource"/> </bean> <bean id="infoJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="infoDataSource"/> </bean> <bean id="dataAccessResourceSupport" abstract="true"> <property name="mainJdbcTemplate" ref=""/> <property name="infoJdbcTemplate" ref=""/> </bean> <bean id="someDaoWithMainDS" class="..."> <property name="mainJdbcTemplate" ref="mainJdbcTemplate"/> <!-- other property settings --> </bean> <bean id="someDaoWithInfoDS" class="..."> <property name="infoJdbcTemplate" ref="infoJdbcTemplate"/> <!-- other property settings --> </bean> <bean id="someDaoWithBothDS" class="..." parent="dataAccessResourceSupport"> <!-- other property settings --> </bean>
項目中這種情況下的多數據源管理是最簡單的,也是比較容易管理的方式,所以,在你考慮下面將要談到的這種更加動態,更加復雜的多數據源管理方式之 前, 請先對你的數據訪問場景做一個評估,看一下當前這種方式是否已經足夠滿足項目的數據訪問需要,實在不行的話,再考慮后繼方案,也就是在運行期間來決定到底 使用多個數據源中的哪一個。
社區中經常提到的“ 多數據源互換 ”即屬于這種場景,之所以用“ 合縱連橫 ”來形容這些數據源是因為,對于使用它們的數據訪問類來說, 這些數據源已經喪失了“ 獨立自主 ”的地位,所有與數據訪問類進行的交互需要通過“ 盟主 ”進行,該盟主本質上也是一個DataSource, 但它的職責更加傾向于對“ 聯盟 ”內的多個DataSource的職能進行協調和管理,最終數據訪問所需要的資源由“ 盟主 ”來決定要哪一個DataSource貢獻出來。
使用這種多數據源管理方式的具體場景可能有:
- 系統中設置多臺“ 地位相當 ”的數據庫以實現多機熱備,從而保證數據庫的高可用性(HA,High Availability),這個時候,如果某一臺數據庫掛掉的話, 可以迅速切換到另一臺數據庫,而對于數據訪問類來說,這樣的切換對其是透明的;
- 系統中存在的多臺服務器也是“ 地位相當 ” 的,不過,同一時間他們都處于活動(Active)狀態,處于負載均衡等因素考慮,數據訪問請求需要在這幾臺數據庫服務器之間進行合理分配, 這個時候,通過統一的一個DataSource來屏蔽這種請求分配的需求,從而屏蔽數據訪問類與具體DataSource的耦合;
- 系統中存在的多臺數據庫服務器現在地位可能相當也可能不相當,但數據訪問類在系統啟動時間無法明確到底應該使用哪一個數據源進行數據訪問,而必須在系統運行期間通過某種條件來判定到底應該使用哪一個數據源, 這個時候,我們也得使用這種“ 合縱連橫 ”的方式向數據訪問類暴露一個統一的DataSource,由該DataSource來解除數據訪問類與具體數據源之間的過緊耦合;
更多場景需要讀者根據具體的應用來判定,不過,并非所有的應用要做這樣的處理,如果能夠保持簡單,那盡量保持簡單,畢竟,我們提倡K.I.S.S.(Keep It Simple,Stupid)嘛! 要實現這種“ 合縱連和 ” 的多數據源管理方式,總的指導原則就是實現一個自定義的DataSource,讓該DataSource來管理系統中存在的多個與具體數據庫掛鉤的數據 源, 數據訪問類只跟這個自定義的DataSource打交道即可。在spring2.0.1發布之前,各個項目中可能存在多種針對這種情況下的多數據源管理方 式, 不過,spring2.0.1發布之后,引入了AbstractRoutingDataSource,使用該類可以實現普遍意義上的多數據源管理功能。
假設我們有三臺數據庫用來實現負載均衡,所有的數據訪問請求最終需要平均的分配到這三臺數據庫服務器之上,那么,我們可以通過繼承AbstractRoutingDataSource來快速實現一個滿足這樣場景的原型(Prototype):
public class PrototypeLoadBalanceDataSource extends AbstractRoutingDataSource { private Lock lock = new ReentrantLock(); private int counter = 0; private int dataSourceNumber = 3; @Override protected Object determineCurrentLookupKey() { lock.lock(); try { counter++; int lookupKey = counter % getDataSourceNumber(); return new Integer(lookupKey); } finally { lock.unlock(); } } // ... }
我們在介紹AbstractRoutingDataSource的時候說過,要繼承該類,通常只需要給出determineCurrentLookupKey()方法的邏輯即可。 下面是針對PrototypeLoadBalanceDataSource的配置:
<bean id="dataSourc1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value=".."/> <property name="driverClassName" value=".."/> <property name="username" value=".."/> <property name="password" value=".."/> <!-- other property settings --> </bean> <bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value=".."/> <property name="driverClassName" value=".."/> <property name="username" value=".."/> <property name="password" value=".."/> <!-- other property settings --> </bean> <bean id="dataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value=".."/> <property name="driverClassName" value=".."/> <property name="username" value=".."/> <property name="password" value=".."/> <!-- other property settings --> </bean> <util:map id="dataSources"> <entry key="0" value-ref="dataSource1"/> <entry key="1" value-ref="dataSource2"/> <entry key="2" value-ref="dataSource3"/> </util:map> <bean id="dataSourceLookup" class="org.springframework.jdbc.datasource.lookup.MapDataSourceLookup"> <constructor-arg> <ref bean="dataSources"/> </constructor-arg> </bean> <bean id="dataSource" class="..PrototypeLoadBalanceDataSource"> <property name="defaultTargetDataSource" ref="dataSourc1"/> <property name="targetDataSources" ref="dataSources"/> <property name="dataSourceLookup" ref=""/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="someDao" class="..."> <property name=""jdbcTemplate"" ref=""jdbcTemplate""/> <!-- other property settings --> </bean>
因為我們不想使用AbstractRoutingDataSource的默認鍵值(Key)查找行為(根據指定的鍵值(Key)通過JNDI進行查 找), 所以,我們為PrototypeLoadBalanceDataSource重新設置了DataSourceLookup,轉而使用 MapDataSourceLookup。
Tip
在PrototypeLoadBalanceDataSource中,我們直接將查找的鍵值(Key)寫死到了代碼中,實際上,更多時候,我們會將 鍵值 (Key)綁定到當前線程上,而determineCurrentLookupKey()方法內直接從當前線程取得綁定的Key返回即可。 而這種情況下對Key的更改也變得更加靈活多變,比如,我可以在數據訪問類內直接將我要訪問的數據源對應的查找Key綁定到當前線程,我也可以在系統的某 個位置設置攔截器(Interceptor),當攔截到相應事件的時候,根據邏輯設置綁定到當前線程的查找鍵值(Key)等等。 讀者可以先思考如果讓你來實現這么一個AbstractRoutingDataSource,你應該如何處理。不過,如果你已經迫不及待,那可以直接轉向 spring的事務管理一章的擴展篇,我將在那里為你展示詳細的實現過程。
有了AbstractRoutingDataSource之后,實現這種“ 合縱連橫 ”的多數據源管理,將不再像最初看起來那么復雜而神秘。
4.4.2.3. 結束語
因為以上兩種多數據源的管理方式在實際的使用過程中可能還有一些變數,所以,最后筆者還是覺得應該提及兩點:
-
不管是“
獨立主權
”的多數據源管理方式還是“
合縱連橫
”的多數據源管理方式, 單獨使用任何一種都是有其特定的應用場景的,不過,這并不意味著二者是相互競爭甚至割裂的,實際上,如果必要,我們完全可以組合兩種多數據源管理方式。
如果我們將“ 合縱連橫 ”的多數據源作為一個整體放入“ 獨立主權 ”的多數據源場景中的話,我們可以得到如下的一幅畫卷:?
Figure 4.11. “ 合縱連橫 ”爭取“ 主權獨立 ”
如果我們將“ 獨立主權 ”的多個數據源先分別注入上一層對象,然后將上一層對象和數據源作為一個整體再并入“ 合縱連橫 ”的多數據源場景的話,那么,我們又可以得出另一幅圖景:
Figure 4.12. 從“ 主權獨立 ”并入“ 合縱連橫 ”
當然,如果愿意,你還可以根據情況采取進一步的組合措施,不過,在進行之前,還是需要你首先全面評估一下整體情況,看是否真的需要這么做,畢竟,復雜度的過多引入有些時候并非必要的。
- 在“ 主權獨立 ”的多數據源場景中,我們是將獨立的數據源注入給了JdbcTemplate,但這只是為了演示的目的,實際上,對于IBatis以及Hibernate來說, 這樣的場景也是類似的;不過,在“ 合縱連橫 ”的多數據源場景中,將JdbcTemplate的使用類推IBatis,即SqlMapClientTemplate,是可以的,但以同樣的方式類推到Hibernate則有需要注意的地方。 我們可以將“ 合縱連橫 ” 的多個數據源注入給Hibernate的SessionFactory(實際上是通過Spring的LocalSessionFactoryBean), 然后HibernateTemplate直接引用這個SessionFactory即可, 但當你開啟了Hibernate的二級緩存的時候(與SessionFactory掛鉤),這樣的多個數據源直接注入SessionFactory并且可 以動態查找替換的方式可能造成問題。 如果二級緩存中有與當前使用的DataSource掛鉤的內容,而這個時候切換到了下一個DataSource,那么二級緩存里的內容需要你根據情況進行 合理的處理,或者清空,或者通過某種方式來同步, 否則在并發的情況下,難免出現問題,當然啦,如果你可以忽略這樣的數據沖突,那可能也有不處理的理由。如果你不需要開啟Hibernate的二級緩存,或 者可以和略二級緩存數據的不一致性,那么,采用“ 合縱連橫 ”的多數據源直接注入使用的SessionFactory的方式實現多個數據源的管理是可以的; 否則的話,可以將“ 獨立主權 ”的多數據源管理方式并入“ 合縱連橫 ” 的多數據源管理方式,以SessionFactory一級替代DataSource一級,這也就是第二種組合場景所描繪的那樣。 這個時候,你可以像Spring提供AbstractRoutingDataSource那樣,提供一個 AbstractRoutingSessionFactory,也可以自己實現一個SessionFactory來屏蔽多個具體的 SessionFactory, 總之方法和原則跟多個數據源的處理方式是類似的,至于你要采用設計模式還是AOP,那就看你的啦!
最后,祝各位在多數據源管理的道路上一帆風順,即使不順,那么應用以上的方式“ 排除萬難去爭取勝利 ”也不再是難事了吧?!
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
