1. 程式人生 > 其它 >java分散式套路之分庫分表漫談

java分散式套路之分庫分表漫談

java分散式套路之分庫分表漫談

既然是“漫談分庫分表”,那麼我們需要確定我們要談什麼,不談什麼。

  1. 首先,我們不討論具體的分庫分表框架的實現和原始碼,這不是我們討論的範圍。
  2. 我們討論的是思路,主要討論如何分庫分表的套路,有什麼坑,有什麼心得,不針對具體的細節進行展開式討論。當然我自己的能力有限,只是希望能夠拋磚引玉。
  3. 我們要明確,分庫分表,並不是一個銀彈,它只是我們針對MySQL單機效能不夠的情況下,想要節約成本的一種方式。對於boss來說,既想要想要節約成本,又想要支撐業務,提供穩定持久度效能。

程式設計師發揮出聰明才智,絞盡腦汁,日復一日的努力與實踐,最終產生出主要的兩種方式:

  1. agent嵌入式模式,用一個jar包,整合到我們的程式碼裡,在程式碼裡通過路由規則,分片鍵方式進行分庫分表,屬於嵌入式方式。
  2. cs模式(客戶端-服務端模式),提供一個三方元件, 如: mycat,sharding-sphere中的proxy方式,類似於mycat;存在中心化,需要保證三方元件的高可用。

如果有更好的技術選型,我們寧願不用分庫分表,因為它本身就是一個複雜的解決方案。只是一種折中,更合適的是NewSQL、商業化資料庫(比方說,Oracle,在大部分場景下,效能足夠用,但是費用高昂)。

如果真的有一天,出現了一個優秀的、經濟的newSQL, 比方說 oceanbase,tidb,那麼我們基本上可以告別分庫分表。

我們之所以選擇使用分庫分表策略,根本上還是因為,一方面是因為我們的使用成本不能太高;一方面,單機DB資料庫效能不夠了;一方面,newSQL當前還不成熟,太貴,不敢用。

分庫分表用的廠家挺多的,有豐富的開源框架,有社群,還有成熟的案例,所以我們採用,

直接原因在於,阿里站臺了,我們國內的風氣是,阿里用啥我用啥,阿里怎麼做我這麼 跟風嚴重。我的想法是,我們還是有自己的技術前瞻性一些看法,最好不要唯阿里,唯技術。

說了這麼多,我們迴歸正題,開始看問題。

1. 只做分表可以嗎?還是必須要分表又分庫,如果是分庫的話 庫是在多個伺服器上嗎?這個怎麼來考慮

我想說,還是要看業務規模,既看當前的業務規模,也看未來3-5年的業務發展趨勢。

涉及到技術選型,我們的宗旨是,永遠選擇最適合當下業務的、成本最低的,收益最高的,合適的就是最好的。

我們選擇的方案最好是技術團隊剛剛好能夠hold住的選型。如果選型已經不適合當前的業務發展,那麼大可以換套更適合的。這個本來就是事物發展的必然規律。

要麼,業務還沒發展到一個更高層次,就已經GG了,那麼剛剛好,不用浪費錢買更好的設施,剛好止損;

要麼,就是當前的方案確實不夠用了,我們換了一套更牛X的,雖然說這樣會花更多的錢,請更多的人,但是,這不正合我們的心意麼,我們的目的本身就是通過合適的技術架構,更加優秀的程式碼,支援業務發展。

一句話總結就是,既然不得不花錢,那該花就花吧。

分場景討論

一圖勝千言,我們分別看看這兩種場景。

對於離線資料分析場景

只分表是夠的,因為你主要用來分析資料,分析資料完成之後的資料就可以刪掉了。非同步任務刪掉若干月/天的資料。

對於實時業務系統

如果是一個分散式的業務系統 2C,需要承載巨量的流量的,建議分庫分表同時考慮。

分庫分庫的前提,預估業務量

分庫分表的前提,是預估業務量,我們提供一個經驗值,不代表最合適的,只是一個定性的分析:

  1. QPS 500-1000以下, 那麼採用主從讀寫分離,基本上足夠支撐業務了;
  2. QPS 1000-10000,考慮分佈分表是一個比較合適的事情
  3. 12000TPS 30000QPS 32庫 1024表
  4. 1000多萬 16000QPS 16庫512表

本質上來說:分庫分表是一錘子買賣,前期的設計很重要,決定了後期擴容以及資料遷移的難度。在前期設計的時候,大概率我們需要做好未來3-5年的規劃,短的話需要做1-2年的規劃,根據規劃來確定是不是要分庫分表,以及分多少庫、多少表。

回到問題本身,這個主要取決於當前的業務量,以及業務量的增速。

我們根據這幾個維度,給出一組公式:

  1. 某年資料增量M = (1 + 資料年增速K)^ n * 初始資料量 N
  2. 第一年增量 M1 = (1+k) * N
  3. 第二年增量 M2 = (1+K)^2 * N
  4. 第三年增量 M3 = (1+K)^3 * N
  5. 三年資料總量 M' = N + m1 + m2 + m3

我們就以單表承載1000萬資料來算,一共要有幾張表,當前不一定是1000w,2000w-5000w都可以,這個首先是一個經驗值,其次還需要定量分析。

定量的分析,就需要進行壓測。我們需要針對你的線上的配置,用一個庫的例項去壓測,壓出你的這個配置下,在不影響系統吞吐量的前提下,單表的最大容量,壓測是一個穩妥的環節,能夠在前期很好的指導我們進行設計。

我們接著討論,什麼時候需要分庫,必須要保證每個資料庫都是一個獨立的例項麼?

並不是,我們還是要具體問題具體分析。

如果是開發環境,也就是研發RD自己寫程式碼用的庫,那麼多套庫在一臺機器上也可以,畢竟開發環境沒有併發量,最多拿來開發,只要不用來壓測就沒啥問題。

如果是線上環境,除了要將庫部署到多臺機器,還得考慮讀寫分離,以及庫的高可用。線上線下的主要區別在於,線上有高可用的要求,而線下不需要

思考一下,兩者區別是什麼,區別就在於成本的控制

我們給出結論,具體什麼時候要把資料庫部署到一臺機器例項,還是要看場景,看成本,看自己需不需要。具體問題具體分析。

2. 路由鍵怎麼生成?用雪花演算法可以嗎?如果原來的資料庫主鍵是自增的,沒有業務唯一約束,如果遷移之後,原先的資料怎麼在分庫分表中進行路由

好問題。

首先說,路由鍵怎麼生成?

本質上,這是一個如何實現一個可靠的分散式發號器的問題。我們只說思路,因為展開說都能但單獨說好半天了。

思路:

對於某些框架而言,他們有自己的主鍵生成器,比如說shardingSphere/ShardingJDBC 類SnowFlake演算法;

  1. UUID:字串形式,確實是唯一,但是可讀性差,不好做數學計算,不直觀,比較長,佔用空間大
  2. SNOWFLAKE:可以用,也可以用改進leaf,leaf本身就是一套完善的分散式發號器,自己也有高可用保障。

當然還有別的方式:

因為既然已經做了分庫分表,大概率你的系統也是分散式的吧,那麼用程序內的發號不是一個理想的方式。

如果要簡單實現一種分散式發號服務,我們可以利用 redis increment 實現一套發號器,也可以藉助資料庫的自增唯一id來做,但是我們還是需要自己進行開發,實現一個發號系統。

簡單的上一個圖,表達一下思路,這塊兒內容之後會單獨寫文章來講。

總結一下就是,本質上,這是一個如何實現一個可靠的分散式發號器的問題。

所以得依賴某個具體的分散式發號機制 這個問題不用糾結,關注一下最終的選型就好,多進行權衡。

3. 如果本身是一個單庫,並且沒有路由鍵,完全拿主鍵當唯一標識了,我分庫分表怎麼玩?

很簡單,你原來的唯一標識是什麼,分庫分表之後還用這個就行了。

但是,因為本身沒有一個業務屬性的鍵,所以建議在進行資料遷移之後,加入一個業務屬性的自然主鍵,並且大概率你需要配置一下新的路由規則。

具體的過程為:

  1. 遷移資料
  2. 更改路由配置 指定一個新的查詢規則,分庫分表的路由規則
  3. 改程式碼,把程式碼中涉及到C R U D 的程式碼,比如說DAO、repository中包含的程式碼,程式碼都加上路由規則,簡單的說你還是可以用原來的id去執行查詢 、插入 、刪除的,但是主要的改動點就在於你需要有一個路由規則。

我們說,資料庫遷移到分不分表的核心:是保證資料的完整性,程式碼該重構就重構,很難有一個全面的不需要改程式碼的方案,我們只能折中權衡,降低複雜度。

原先的主鍵id,遷移到分庫分表新庫中,已經不是連續的了,但是還需要保證unique,新的資料庫表中的自增主鍵還需要有,但是沒有業務屬性了,之所以分庫分表之後還需要有自增主鍵,主要在於提升插入效率,查詢效率。通過主鍵索引樹,進行回表操作。

相當於你原先用了自增id是有業務屬性的,這裡說句題外話,請儘量不要使用自增主鍵代表業務含義

3. 分片鍵怎麼選擇

我們的答案依舊不能給出一個準確的說法,我只能說,要根據業務場景的要求去選擇。

這麼說太籠統了,我們通過幾個例子來表達一下。

  1. 對於使用者表,使用使用者唯一標識, 如:userId作為分片鍵;
  2. 對於賬戶表,使用賬戶唯一標識,如:accountId作為分片鍵;
  3. 對於訂單表,使用訂單唯一標識, 如:orderId作為分片鍵;
  4. 對於商家相關資訊表,使用商家唯一標識, 如:merchantId作為分片鍵;
  5. ......

如果我們要查一個使用者的訂單,那麼我們應該用userId去路由表,插入訂單到訂單表,保證一個使用者的所有訂單都能夠分佈在一個表分片上。這樣做能夠很好的避免引入分散式事務。

如果說,維度不是使用者,而是其他維度,比方說,我們想查詢某個商家的所有使用者的訂單

那麼我們就應該用商家的merchantId也去存一份資料,路由的時候用商家id去路由,只要是這個商家的使用者訂單,我們寫入到商家的訂單表裡,那麼對於商家所屬的訂單,我們就可以從某個分片上獲取到。

用一個圖表達,能夠很明確地體現上述的說明內容:

對於使用者而言,分片鍵作用方式如下圖:
usertable.png

對於商家而言,分片鍵作用方式如下圖:
merchanttable.png

所以我們的結論就是:要根據業務場景的要求去選擇,具體問題具體分析,儘量保證不引入分散式事務,提升查詢效率。

補充一句,對於主流的做法,如果需要有複雜查詢,要麼依據不同維度去進行雙寫,要麼直接通過引入異構的方式去查詢,比方說使用elastic search,或者使用hive等方式。

4.批量插入資料的時候,會往各個分庫去插,在實際業務中是否要做分散式事務

第三個問題或多或少也提到了這個問題的答案。

我們在落地分庫分表的過程中,要儘量避免引入分散式事務

因為從上面第三個問題,你會發現,如果我們有路由鍵,問題就簡單的多了,我們大部分情況下不需要引入分散式事務,但是如果沒有就很痛苦。

對於亂序插入且需要保證插入事務性的場景,就需要分散式事務。但是這樣做效率太低,也不合適。

首先亂序插入的場景並不多,其次如果引入分散式事務,那麼事務的力度也不小,而且對於插入的效能有著顯著的影響。不是最佳的方式。

我的建議就是,還是基於最終一致性去做,否則引入分散式事務,太影響效率了,而且也會增加系統的複雜度,我覺得我們設計系統的宗旨就是,能不用複雜的方案就不用,有時間喝喝茶,乾點別的何樂而不為呢。

所以這個問題的結論就是:儘量避免分散式事務,如果不得不引入,需要儘量縮小事務的範圍和力度。通過折中,多去考慮一下方案的可行性,
效能很重要,沒有分散式事務也能做,怎麼做,就是通過最終一致性。

但是,如果你說 “我就是避免不了分散式事務啊,那咋辦嘛”。那就用吧,若無必要,勿增實體。不得不用,就用,沒什麼好說的。

5.如果一個庫有很多張表,對一張表進行分庫分表了,此時不分庫不分表的表怎麼放置, 是否指定到分庫裡某一個庫裡面?

本質上:這是非分庫分表的資料與分庫分表資料的分佈的一個問題。

實際上,分庫分表中介軟體往往都有對應功能,這個功能往往叫做預設路由規則,怎麼理解呢?

就是說,對於沒有分庫分表的這些表,走預設路由規則就行了,這樣的話始終會路由到default DataSource上去。

相當於是一個白名單。找一下中介軟體的文件,看看預設路由規則怎麼配,基本上中介軟體都考慮這個問題了,對於ShardingSphere而言,一個配置樣例如下:

  1. CustomerNoShardingDBAlgorithm
  2. default-table-strategy: (預設表分割槽策略)
  3. complex:
  4. sharding-columns: db_sharding_id
  5. algorithm-class-name: com.xxx.XxxClass
  6. tables:
  7. ops_account_info: (必須要配置這個,才能使用預設分表策略)
  8. actual-data-nodes: db-001.ops_account_info

詳細的舉個例子,比方說:

一個服務在原有的資料庫進行分庫(比如user庫分為了user01,user02)的時候,是把不分表的表強制路由走某一個數據庫嗎(比如把不分表的表都路由到user01)?

這裡說到的本質就是: 預設路由規則,我們只需要配置某些表走預設路由規則就行了,比方說,我們現在有user 表 order表,config表,其中user表、 order分庫分表,而config沒有分庫分表。

那麼我們只需要把config表放在user庫的0庫,1庫,2庫,隨便某個位置,

放好之後,我們只需要在分庫分表中介軟體的配置檔案中配置預設路由規則,把config表特殊配置一下,只要查config表,就走到這個指定的庫上去。

其他的也類似 ,只要有這種需求,就增加對應的配置。

一定要顯式告訴中介軟體,哪些表不走路由規則,並且要告訴它,這些表具體放在哪兒,
最好是放在請求量不大的庫裡,或者說單獨搞一個庫也可以,這個庫放的都是不進行分庫分表的表,
並配置不走路由規則就完事兒了,其實還是預設路由規則。

為什麼這麼做呢?有什麼意圖呢?

我的理解就是:之所以我們分庫分表的原因,就是因為請求很大需要降低併發度;而對於請求頻率小的表,我們可以不分庫分表還是通過單表方式使用,那麼就可以配置為預設路由規則就好。

8.資料遷移流程以及如何保證資料一致性

簡單的概括,資料遷移依賴於資料的雙寫;資料一致性,依賴於資料完整性校驗。

對於遷移而言,我們有以下步驟:

  1. 先修改程式碼,加入雙寫分庫分表程式碼;進行上線
  2. 開始進行資料雙寫,同步增量資料 ;雙寫,主要目的是追上實時資料,給全量同步資料一個deadline,保證從這個時間之後的資料都是完整的(同時,通過非同步資料完整性校驗程式去校驗資料完整性,但是如果我們能夠保證雙寫可靠性,這個對比可做可不做。最好還是做一下)
  3. 全量歷史資料同步,並校驗資料完整性;一般全量資料同步,不用同步寫的方式,原因在於同步寫入一方面程式碼耦合度高,一方面是對系統有影響。所以我們往往通過非同步方式進行寫入,這個過程後文有圖進行說明;
  4. 去掉雙寫程式碼,將查詢分庫分表的邏輯全量;
    通過開關切換,在全量資料同步完成之後切換到全量讀寫分庫分表邏輯即可。此時老的邏輯已經沒有請求路由過去了,我們只需要找個發版視窗把老邏輯下線就可以,此時線上已經完全遷移到分庫分表的程式碼流程。

最後我想說,一定要回歸,一定要回歸,一定要回歸!!!

原文連結:http://wuwenliang.net/2021/01/09/分散式套路之分庫分表漫談/

如果覺得本文對你有幫助,可以關注一下我公眾號,回覆關鍵字【面試】即可得到一份Java核心知識點整理與一份面試大禮包!另有更多技術乾貨文章以及相關資料共享,大家一起學習進步!