1. 程式人生 > >Spring實現動態資料來源,支援動態新增、刪除和設定權重及讀寫分離

Spring實現動態資料來源,支援動態新增、刪除和設定權重及讀寫分離

當專案慢慢變大,訪問量也慢慢變大的時候,就難免的要使用多個數據源和設定讀寫分離了。

在開題之前先說明下,因為專案多是使用Spring,因此以下說到某些操作可能會依賴於Spring。

在我經歷過的專案中,見過比較多的讀寫分離處理方式,主要分為兩步:

1、對於開發人員,要求serivce類的方法名必須遵守規範,讀操作以query、get等開頭,寫操作以update、delete開頭。

2、配置一個攔截器,依據方法名判斷是讀操作還是寫操作,設定相應的資料來源。

以上做法能實現最簡單的讀寫分離,但相應的也會有很多不方便的地方,印象最深的應該是以下幾點:

1、資料來源的管理不太方便,基本上只有2個數據源了,一個讀一個寫。這個可以在spring中宣告多個bean來解決該問題,但bean的id和資料來源的功能也就綁定了。

2、因為讀寫分離往往是在專案慢慢變大後加入的,不是一開始就有,上面說到的第二點方法名可能會各式各樣,find、insert、save、exe等等,這些都要一一修改,且要保證以後讀的方法名中不能有寫操作。也可以攔截的底層一點如JdbcTemplate,但這樣會導致交叉設定資料來源。

3、資料來源無法動態修改,只能在專案啟動時載入。

以上問題我想開發人員多多少少都會遇到,這也是本文要討論的問題。

動態資料來源結構

在我看來一個好的動態資料來源,應該跟單資料來源一樣讓使用者感覺不到他是動態的,至少dao層的開發者應該感覺不到。先來看張圖:

dynamicDataSource-diagram

看了上圖應該就明白我的思路了,對使用者來說只有一個數據源DynamicDataSource

,下面我們來談談如何實現它。

基於spring實現動態資料來源

其實spring早就想到了這一點,也已經為我們準備好了擴充套件類AbstractRoutingDataSource,我們只需要一個簡單的實現即可。網上關於這個類文章很多,但都比較粗淺沒有講到點子上,只是實現了多個數據源而已。

這裡我們同樣來實現AbstractRoutingDataSource,它只要求實現一個方法:

  1. /**
  2. * Determine the current lookup key. This will typically be
  3. * implemented to check a thread-bound transaction context.
  4. * <p>Allows for arbitrary keys. The returned key needs
  5. * to match the stored lookup key type, as resolved by the
  6. * {@link #resolveSpecifiedLookupKey} method.
  7. */
  8. protectedabstractObject determineCurrentLookupKey();

你可以簡單的理解它:spring把所有的資料來源都存放在了一個map中,這個方法返回一個key告訴spring用這個key從map中去取。

它還有個targetDataSourcesdefaultTargetDataSource屬性,網上的一堆做法是繼承這個類,然後在宣告bean的時候注入dataSource:

  1. <beanid="dynamicdatasource"class="......">
  2. <propertyname="targetDataSources">
  3. <map>
  4. <entrykey="dataSource1"value-ref="dataSource1"/>
  5. <entrykey="dataSource2"value-ref="dataSource2"/>
  6. <entrykey="dataSource3"value-ref="dataSource3"/>
  7. </map>
  8. </property>
  9. <propertyname="defaultTargetDataSource"ref="dataSource1"/>
  10. </bean>

這樣雖然簡單,但是弊端也是顯而易見的,除了使用了多個數據源之外沒有我們想要的任何操作。但是如果不配置targetDataSources,spring在啟動的時候就會丟擲異常而無法執行。

其實我們完全可以在spring啟動的時候,我們自己來解析資料來源,然後把解析出並例項化的dataSource設定到targetDataSources。下面是解析的核心程式碼,資料來源配置檔案的格式可以看這裡:資料來源配置樣例

  1. /**
  2. * 初始化資料來源
  3. *
  4. * @param dataSourceList
  5. */
  6. publicvoid initDataSources(List<Map<String,String>> dataSourceList){
  7. LOG.info("開始初始化動態資料來源");
  8. readDataSourceKeyList =newArrayList<String>();
  9. writeDataSourceKeyList =newArrayList<String>();
  10. Map<Object,Object> targetDataSource =newHashMap<Object,Object>();
  11. Object defaultTargetDataSource =null;
  12. for(Map<String,String> map : dataSourceList){
  13. String dataSourceId =DynamicDataSourceUtils.getAndRemoveValue(map, ATTR_ID,
  14. UUIDUtils.getUUID8());
  15. String dataSourceClass =DynamicDataSourceUtils
  16. .getAndRemoveValue(map, ATTR_CLASS,null);
  17. String isDefaultDataSource =DynamicDataSourceUtils.getAndRemoveValue(map,
  18. ATTR_DEFAULT,"false");
  19. String weight =DynamicDataSourceUtils.getAndRemoveValue(map, DS_WEIGHT,"1");
  20. String mode =DynamicDataSourceUtils.getAndRemoveValue(map, DS_MODE,"rw");
  21. DataSource dataSource =(DataSource)ClassUtils.newInstance(dataSourceClass);
  22. DynamicDataSourceUtils.setDsProperties(map, dataSource);
  23. targetDataSource.put(dataSourceId, dataSource);
  24. if(Boolean.valueOf(isDefaultDataSource)){
  25. defaultTargetDataSource = dataSource;
  26. }
  27. DynamicDataSourceUtils.addWeightDataSource(readDataSourceKeyList,
  28. writeDataSourceKeyList, dataSourceId,Integer.valueOf(weight), mode);
  29. LOG.info("dataSourceId={},dataSourceClass={},isDefaultDataSource={},weight={},mode={}",
  30. newObject[]{ dataSourceId, dataSourceClass, isDefaultDataSource, weight, mode });
  31. }
  32. this.setTargetDataSources(targetDataSource);
  33. if(defaultTargetDataSource ==null){
  34. defaultTargetDataSource =(CollectionUtils.isEmpty(writeDataSourceKeyList)? targetDataSource
  35. .get(readDataSourceKeyList.iterator().next()): targetDataSource
  36. .get(writeDataSourceKeyList.iterator().next()));
  37. }
  38. this.setDefaultTargetDataSource(defaultTargetDataSource);
  39. super.afterPropertiesSet();
  40. LOG.info("初始化動態資料來源完成");
  41. }

在解析出來之後,我們呼叫父類的this.setTargetDataSources(targetDataSource);this.setDefaultTargetDataSource(defaultTargetDataSource);方法將它們存入進去。而dataSource的key則根據讀寫和權重的不同,分別儲存到readDataSourceKeyListwriteDataSourceKeyList

那麼什麼時候來執行這個解析的方法呢?有些同學可能一下就想到了spring宣告bean時的init-method屬性,但是這裡不行。因為init-method是在bean初始化完成之後呼叫的,當spring在初始化DynamicDataSource時發現這兩個屬性是空的異常就丟擲來了,根本就沒有機會去執行init-method

所以我們要在bean的初始化過程中來解析並存入我們的資料來源。要實現這個操作,我們可以實現spring的InitializingBean介面。由於AbstractRoutingDataSource已經實現了該介面,我們只需要重寫該方法就行。也就是說DynamicDataSource要實現以下兩個方法:

  1. @Override
  2. protectedObject determineCurrentLookupKey(){
  3. ...
  4. }
  5. @Override
  6. publicvoid afterPropertiesSet(){
  7. this.initDataSources();
  8. }

afterPropertiesSet方法中實現我們解析資料來源的操作。但是這樣還不夠,因為spring容器並不知道你做了這些,所以最後的一行super.afterPropertiesSet();千萬別忘了,用來通知spring容器。

到這裡資料來源的解析已經完成了,我們又怎麼樣來取資料來源呢?

這個我們可以利用ThreadLocal來實現。編寫DynamicDataSourceHolder類,核心程式碼:

  1. privatestaticfinalThreadLocal<DataSourceContext> DATASOURCE_LOCAL =newThreadLocal<DataSourceContext>();
  2. /**
  3. * 設定資料來源讀寫模式
  4. *
  5. * @param isWrite
  6. */
  7. publicstaticvoid setIsWrite(boolean isWrite){
  8. DataSourceContext dsContext = DATASOURCE_LOCAL.get();
  9. //已經持有且可寫,直接返回
  10. if(dsContext !=null&& dsContext.getIsWrite()){
  11. return;
  12. }
  13. if(dsContext ==null|| isWrite){
  14. dsContext =newDataSourceContext();
  15. dsContext.setIsWrite(isWrite);
  16. DATASOURCE_LOCAL.set(dsContext);
  17. }
  18. }
  19. /**
  20. * 獲取dsKey
  21. *
  22. * @return
  23. */
  24. publicstaticDataSourceContext getDsContent(){
  25. return DATASOURCE_LOCAL.get();
  26. }

只有簡單的設定讀寫模式和獲取dataSource的key。

動態資料來源”讀已之所寫”問題

在設定讀寫模式時需要注意,如果當前執行緒已經擁有資料來源了且是可寫的,則直接返回使用當前的資料來源。這是一個簡單的操作卻會影響到整個專案。為什麼要這樣做呢?要是我方法中寫操作後有讀操作不是也用寫資料來源了?沒錯!

這涉及到一個多資料來源主從同步時的讀已之所寫問題,這裡簡單的來講解一下。

資料庫主從同步時,事務一般分兩種:

1、硬事務,當往資料庫儲存資料時,程式讀到所有資料庫的資料都是一致的,但相應的效能會變低,如果資料庫操作時間較長,有可能會引起執行緒阻塞。

2、軟事務,當往資料庫儲存資料時,程式讀到的資料不一定是一致的,但最終是一致的。舉個例子,當你往主庫(寫庫)存入資料時,資料可能無法實時同步到從庫(讀庫),這中間可能會有那麼幾秒鐘的誤差,如果這時候剛好讀到這批資料,資料就是不一致的。

當資料庫都要分主從和讀寫分離了,肯定是有效能壓力了,所以大多數都會選擇第二種(只是大部分不是絕對,銀行等機構可能會第一種)。

這時候資料就會有一個實時展示的問題了。以當前較流行的微信朋友圈為例,我自己發表了一條朋友圈動態,肯定是希望能夠馬上看到,如果隔個三五秒才能顯示我會懷疑是不是釋出失敗了?使用者體驗感也會直線下降。但對別人來說,就算時時關注著我也不會知道我這個時候釋出了動態,遲個三五秒顯示並無大礙,對整個系統也沒有影響。

說到這裡相信應該已經明白了吧,簡單說就是自己寫的資料要能夠馬上讀到,這就是原因了。

指定了讀寫模式,接下來就是獲取資料來源了。程式碼:

  1. @Override
  2. protectedObject determineCurrentLookupKey(){
  3. DataSourceContext dsContent =DynamicDataSourceHolder.getDsContent();
  4. //已設定過資料來源,直接返回
  5. if(StringUtils.isNotBlank(dsContent.getDsKey())){
  6. return dsContent.getDsKey();
  7. }
  8. if(dsContent.getIsWrite()){
  9. String dsKey = writeDataSourceKeyList.get(RandomUtils.nextInt(writeDataSourceKeyList
  10. .size()));
  11. dsContent.setDsKey(dsKey);
  12. }else{
  13. String dsKey = readDataSourceKeyList.get(RandomUtils.nextInt(readDataSourceKeyList
  14. .size()));
  15. dsContent.setDsKey(dsKey);
  16. }
  17. if(LOG.isDebugEnabled()){
  18. LOG.debug("當前操作使用資料來源:{}", dsContent.getDsKey());
  19. }
  20. return dsContent.getDsKey();
  21. }

這裡同樣注意如果已經設定過資料來源了,直接返回,這樣就能保證當前執行緒用的始終是同一個資料來源(讀改寫時會變化一次)。

如果未設定過資料來源則根據讀寫模式,隨機的從key列表中取一個使用。為什麼要隨機呢?這就牽扯到具體的權重實現了。

動態資料來源權重實現

這裡的權重實現十分簡單,也是當前很多元件的權重實現方式。假設一個讀dataSource的權重是5,則相應的往readDataSourceKeyList中存入5個key,寫dataSource也一樣,讀寫則兩邊都存。這樣根據權重的不同key列表中存入的數量也就不盡相同,取時生成一個小於列表大小的隨機數隨機取一個就行了。

使用攔截器設定讀寫模式

各個元件的功能都實現了,只差東風了,什麼時候來設定讀寫模式呢?

這個簡單,使用一個攔截器就能搞定。因為是基於Spring JdbcTemplate,所以只要攔截相應的方法即可。JdbcTemplate的方法命名還是十分規範的,開發人員改動的可能性也幾乎為零,這裡我們攔截介面:

  1. /**
  2. * 動態資料來源攔截器
  3. *
  4. * Created by liyd on 2015-11-2.
  5. */
  6. @Aspect
  7. @Component
  8. publicclassDynamicDsInterceptor{
  9. @Pointcut("execution(* org.springframework.jdbc.core.JdbcOperations.*(..))")
  10. publicvoid executeMethod(){
  11. }
  12. @Around("executeMethod()")
  13. publicObject methodAspect(ProceedingJoinPoint pjp)throwsThrowable{
  14. String methodName = pjp.getSignature

    相關推薦

    Spring實現動態資料來源支援動態新增刪除設定權重分離

    當專案慢慢變大,訪問量也慢慢變大的時候,就難免的要使用多個數據源和設定讀寫分離了。 在開題之前先說明下,因為專案多是使用Spring,因此以下說到某些操作可能會依賴於Spring。 在我經歷過的專案中,見過比較多的讀寫分離處理方式,主要分為兩步: 1、對於開發人員,要

    spring+mybatis配置多資料來源總結重點是動態載入資料來源支援動態切換

    最近在做一款遊戲的GM管理平臺,需要連線遊戲的資料庫去查詢資料;由於遊戲的每個服的資料是獨立的,所以就有了連線多個數據庫的問題;經過一番查詢,好在mybatis的學習資源還少,很快找到了配置多資料來源的方法;感謝以下大牛分享的學習資源: http://lvdong5830

    Keepalive+Amoeba+Mysql 實現高可用負載均衡分離

    一:實驗環境   寫:寫入的介面是keepalive配置的虛擬IP(192.168.1.60),而這個VIP指向雙主複製中的兩個節點。 讀:slave1(該slave1指向的是master1)。 二:實驗目的 Master1與master2實現高可用,master1

    mysql的復制集群分離

    replica 成功 http 一個 主機名 技術 端口號 變化 res 為什麽要設置mysql集群? 為了減輕,mysql服務器的IO壓力,設置多個其他mysql服務器幫他分擔讀寫操作 1.mysql復制集群的類型 主從架

    MariaDB實現主從配置分離(一)

    一、主從複製方案 1.  在兩臺CentOS7虛擬機器上分別部署MariaDB, 主資料庫伺服器IP為192.168.17.235, 從伺服器IP為192.168.17.238. 從伺服器通過調取主伺服器上binlog日誌, 在本地重建庫、表, 實現與主伺服器的AB複製. 二、步驟 1. 對兩

    二叉樹構建新增刪除遍歷總結

    敬請關注部落格,後期不斷更新優質博文,謝謝 原始碼: ------------------------------------------------------------------------------------ Node.java:

    Vue中的新增刪除搜尋

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>demo</title> <style>

    Redis學習筆記七——向叢集節點新增刪除分配slot

    上邊文章講了如何搭建redis叢集,這邊文章講叢集節點的新增、刪除和重新分配slot。 1、新增節點 新增節點:redis-trib.rb add-node 192.168.72.100:7006

    C#對資料庫的新增刪除修改

    string str = "server=NO1;database=SuperMarket;integrated security=true"; SqlConnection c

    C語言檔案讀取單鏈表的新增刪除排序等操作例項

    /* 1、從文字檔案中匯入班級學生資訊:學號、姓名、性別、籍貫 2、將學號重複的刪除 3、顯示匯入的學生資訊 4、按學號、姓名、性別、籍貫相等和不相等查詢 5、多次查詢 6、查詢結果寫入檔案 7、VC++6.0編譯通過 //以下程式碼存為main.cpp */ #inclu

    MHA+keepalived實現Mysql高可用分離

    1.準備工作 1.準備機器(5臺) keepalived+mha(2臺): 10.11.13.11 keepalived master及mha管理機公用 10.11.13.12 keepalived slave 10.11.13.241 keepalived vip 10.11.13.2

    原理解密 → Spring AOP 實現動態資料來源分離底層原理是什麼

    開心一刻   女孩睡醒玩手機,收到男孩發來一條資訊:我要去跟我喜歡的人表白了!  女孩的心猛的一痛,回了條資訊:去吧,祝你好運!  男孩回了句:但是我沒有勇氣說不來,怕被打!  女孩:沒事的,我相信你!此時女孩已經傷心的流淚了  男孩:我已經到她家門口了,不敢敲門!  女孩擦了擦眼淚:不用怕,你是個好人,會有

    解析配置檔案自動裝配 DataSource + AbstractRoutingDataSource + AOP 實現動態資料來源 下:配置動態資料來源AOP 進行使用

    上篇文章中已經藉助 DynamicDataSourceBuilder 類從配置檔案中解析得到了預設資料來源和動態資料來源,接下來需要配置動態資料來源的“本體”,並藉助 AOP 動態的切換資料來源。 配置動態資料來源 AbstractRoutingDataSource 實現了 In

    Spring 動態資料來源AOP實現資料庫分離

    網際網路架構演進中 資料庫優化是不可或缺的一環,資料庫層面優化分為兩個階段:讀寫分離、分庫分表。 今天要說的是 資料庫讀寫分離技術,其原理就是一個Master資料庫,多個Slave資料庫。Master庫負責資料更新和實時資料查詢,Slave庫當然負責非實時資料

    擴充套件:spring3整合quartz2實現動態新增刪除定時任務

        本文的用的是quartz-2.2.1與spring-3.2.2。之所以在這裡特別對版本作一下說明,是因為spring和quartz的整合對版本是有要求的。spring3.1以下的版本必須使用quartz1.x系列,3.1以上的版本才支援quartz 2.x,不然會出錯。原因主要是:spring對於qu

    Spring Boot MyBatis 動態資料來源切換資料來源分離

    轉載自:https://blog.csdn.net/u013360850/article/details/78861442本專案使用 Spring Boot 和 MyBatis 實現多資料來源,動態資料來源的切換;有多種不同的實現方式,在學習的過程中發現沒有文章將這些方式和常

    Spring動態數據源實現分離

    character count 方法 cdata mas ebe aso nds evict 一、創建基於ThreadLocal的動態數據源容器,保證數據源的線程安全性 package com.bounter.mybatis.extension; /** * 基

    獲取元素動態新增刪除元素以及getElementByTagNamegetElementByClassName)的用法

    1.獲取元素 (1)getElementById 根據元素的id屬性來獲取元素,獲取到的是一個元素 (2)getElementByTagName 根據標籤名來獲取元素,結果是一個元素集合 (3) getElementByClassName 根據class屬性來獲取元素,結果是一個元素集合 (

    SSM整合系列之 配置多資料來源 利用AOP動態切換資料來源 實現分離

    摘要:在開發的專案中大都都會使用讀寫分離的技術,本人目前開發的專案接觸到的都是主從複製(一主一從),就是一個Master資料庫,一個Slave資料庫。主庫負責資料插入、更新和實時資料查詢,從庫庫負責非實時資料查詢。在實際專案應用中,都是讀多寫少,而讀取資料通常比較複雜而且耗時,SQL語句

    分散式資料層中介軟體詳解:如何實現分庫分表+動態資料來源+分離

    優知學院 2018-10-13 12:10:20   分散式資料層中介軟體: 1.簡介: 分散式資料訪問層中介軟體,旨在為供一個通用資料訪問層服務,支援MySQL動態資料來源、讀寫分離、分散式唯一主鍵生成器、分庫分表、動態化配置等功能,並且支援從客戶端角度對