隨着互聯網產業的蓬勃發展,在互聯網應用上產生的數據也是與日俱增。產生大量的交易記錄和行爲記錄,它們的存放和分析是咱們須要面對的問題。算法
圖片來自 Pexels數據庫
例如:單表中出現了,動輒百萬甚至千萬級別的數據。「分表分庫」就成爲解決上述問題的有效工具。今天和你們一塊兒看看,如何進行分表分庫以及期間遇到的問題吧。緩存
爲何會分表分庫服務器
數據庫數據會隨着業務的發展而不斷增多,所以數據操做,如增刪改查的開銷也會愈來愈大。架構
再加上物理服務器的資源有限(CPU、磁盤、內存、IO 等)。最終數據庫所能承載的數據量、數據處理能力都將遭遇瓶頸。併發
換句話說須要合理的數據庫架構來存放不斷增加的數據,這個就是分庫分表的設計初衷。目的就是爲了緩解數據庫的壓力,最大限度提升數據操做的效率。app
數據分表負載均衡
若是單表的數據量過大,例如千萬級甚至更多,那麼在操做表的時候就會加大系統的開銷。框架
每次查詢會消耗數據庫大量資源,若是須要多表的聯合查詢,這種劣勢就更加明顯了。異步
以 MySQL 爲例,在插入數據的時候,會對錶進行加鎖,分爲表鎖定和行鎖定。
不管是哪一種鎖定方式,都意味着前面一條數據在操做表或者行的時候,後面的請求都在排隊,當訪問量增長的時候,都會影響數據庫的效率。
那麼既然必定要分表,那麼每張表分配多大的數據量比較合適呢?這裏建議根據業務場景和實際狀況具體分析。
通常來講 MySQL 數據庫單表記錄最好控制在 500 萬條(這是個經驗數字)。既然須要將數據從一個表分別存放到多個表中,那麼來看看下面兩種分表方式吧。
垂直分表
根據業務把一個表中的字段(Field)分到不一樣的表中。這些被分出去的數據一般根據業務須要,例如分出去一些不是常用的字段,一些長度較長的字段。
通常被拆分的表的字段數比較多。主要是避免查詢的時候出現由於數據量大而形成的「跨頁」問題。
通常這種拆分在數據庫設計之初就會考慮,儘可能在系統上線以前考慮調整。已經上線的項目,作這種操做是要慎重考慮的。
水平分表
將一個表中的數據,按照關鍵字(例如:ID)(或取 Hash 以後)對一個具體的數字取模,獲得的餘數就是須要存放到的新表的位置。
![1605191415430765.png image.png](http://static.javashuo.com/static/loading.gif)
用 ID 取模的分表方式分配記錄
ID 分別爲 01-04 的四條記錄,若是分配到 3 個表中,那麼對 3 取模獲得的餘數分別是:
ID:01 對 3 取模餘數爲 1 ,存到「表 1」。
ID:02 對 3 取模餘數爲 2 ,存到「表 2」。
ID:03 對 3 取模餘數爲 3 ,存到「表 3」。
ID:04 對 3 取模餘數爲 1 ,存到「表 1」。
固然這裏只是一個例子,實際狀況須要對 ID 作 Hash 以後再計算。同時還能夠針對不一樣表所在的不一樣的數據庫的資源來設置存儲數據的多少。針對每一個表所在的庫的資源設置權值。
用這種方式存放數據之後,在訪問具體數據的時候須要經過一個 Mapping Table 獲取對應要響應的數據來自哪一個數據表。目前比較流行的數據庫中間件已經幫助咱們實現了這部分的功能。
也就是說不用你們本身去創建這個 Mapping Table,在作查詢的時候中間件幫助你實現了 Mapping Table 的功能。因此,咱們這裏只須要了解其實現原理就能夠了。
![1605191422380922.png image.png](http://static.javashuo.com/static/loading.gif)
Mapping Table 協助分表
水平拆分還有一種狀況是根據數據產生的先後順序來拆分存放。例如,主表只存放最近 2 個月的信息,其餘比較老舊的信息拆分到其餘的表中。經過時間來作數據區分。更有甚者是經過服務的地域來作數據區分的。
![1605191426419936.png image.png](http://static.javashuo.com/static/loading.gif)
按照時間作的數據分表
須要注意的是因爲分表形成一系列記錄級別的問題,例如 Join 和 ID 生成,事務處理,
同時存在這些表須要跨數據庫的可能性:
Join:須要作兩次查詢,把兩次查詢的結果在應用層作合併。這種作法是最簡單的,在應用層設計的時候須要考慮。
ID:可使用 UUID,或者用一張表來存放生成的 Sequence,不過效率都不算高。UUID 實現起來比較方便,可是佔用的空間比較大。
Sequence 表的方式節省了空間,可是全部的 ID 都依賴於單表。這裏介紹一個大廠用的 Snowflake 的方式。
Snowflake 是 Twitter 開源的分佈式 ID 生成算法,結果是一個 long 型的 ID。
其核心思想是:使用 41bit 做爲毫秒數,10bit 做爲機器的 ID(5 個 bit 是數據中心,5 個 bit 的機器 ID),12bit 做爲毫秒內的流水號(意味着每一個節點在每毫秒能夠產生 4096 個 ID),最後還有一個符號位,永遠是 0。
![1605191433188166.png image.png](http://static.javashuo.com/static/loading.gif)
Snowflake 示意圖
排序/分頁:
數據分配到水平的幾個表中的時候,作排序和分頁或者一些集合操做是不容易的。
這裏根據經驗介紹兩種方法。對分表的數據先進行排序/分頁/聚合,再進行合併。對分表的數據先進行合併再作排序/分頁/聚合。
事務:
存在分佈式事務的可能,須要考慮補償事務或者用 TCC(Try Confirm Cancel)協助完成,這部分的內容咱們下面會爲你們介紹。
數據分庫
說完了分表,再來談談分庫。每一個物理數據庫支持數據都是有限的,每一次的數據庫請求都會產生一次數據庫連接,當一個庫沒法支持更多訪問的時候,咱們會把原來的單個數據庫分紅多個,幫助分擔壓力。
這裏有幾類分庫的原則,能夠根據具體場景進行選擇:
![1605191441669623.png image.png](http://static.javashuo.com/static/loading.gif)
單個表會分到不一樣的數據庫中
一般數據分庫以後,每個數據庫包含多個數據表,多個數據庫會組成一個 Cluster/Group,提升了數據庫的可用性,而且能夠把讀寫作分離。
Master 庫主要負責寫操做,Slave 庫主要負責讀操做。在應用訪問數據庫的時候會經過一個負載均衡代理,經過判斷讀寫操做把請求路由到對應的數據庫。
若是是讀操做,也會根據數據庫設置的權重或者平均分配請求。
另外,還有數據庫健康監控機制,定時發送心跳檢測數據庫的健康情況。
若是 Slave 出現問題,會啓動熔斷機制中止對其的訪問;若是 Master 出現問題,經過選舉機制選擇新的 Master 代替。
![1605191445800159.png image.png](http://static.javashuo.com/static/loading.gif)
主從數據庫簡圖
數據庫擴容
分庫以後的數據庫會遇到數據擴容或者數據遷移的狀況。這裏推薦兩種數據庫擴容的方案。
主從數據庫擴容
咱們這裏假設有兩個數據庫集羣,每一個集羣分別有 M1 S1 和 M2 S2 互爲主備。
![1605191452109349.png image.png](http://static.javashuo.com/static/loading.gif)
兩個數據庫集羣示意圖
因爲 M1 和 S1 互爲主備因此數據是同樣的,M2 和 S2 一樣。把原有的 ID %2 模式切換成 ID %4 模式,也就是把兩個數據集羣擴充到 4 個數據庫集羣。
負載均衡器直接把數據路由到原來兩個 S1 和 S2 上面,同時 S1 和 S2 會中止與 M1 和 M2 的數據同步,單獨做爲主庫(寫操做)存在。
這些修改不須要重啓數據庫服務,只須要修改代理配置就能夠完成。因爲 M1 M2 S1 S2 中會存在一些冗餘的數據,能夠後臺起服務將這些冗餘數據刪除,不會影響數據使用。
![1605191457806411.png image.png](http://static.javashuo.com/static/loading.gif)
兩個集羣中的兩個主從,分別擴展成四個集羣中的四個主機
此時,再考慮數據庫可用性,將擴展後的 4 個主庫進行主備操做,針對每一個主庫都創建對應的從庫,前者負責寫操做,後者負責讀操做。下次若是須要擴容也能夠按照相似的操做進行。
![1605191461293540.png image.png](http://static.javashuo.com/static/loading.gif)
從兩個集羣擴展成四個集羣
雙寫數據庫擴容
在沒有數據庫主從配置的狀況下的擴容,假設有數據庫 M1 M2 以下圖:
![1605191468290997.png image.png](http://static.javashuo.com/static/loading.gif)
擴展前的兩個主庫
須要對目前的兩個數據庫作擴容,擴容以後是 4 個庫以下圖。新增的庫是 M3,M4 路由的方式分別是 ID%2=0 和 ID%2=1。
![1605191472732195.png image.png](http://static.javashuo.com/static/loading.gif)
新增兩個主庫
這個時候新的數據會同時進入 M1 M2 M3 M4 四個庫中,而老數據的使用依舊從 M1 M2 中獲取。
與此同時,後臺服務對 M1 M3,M2 M4 作數據同步,建議先作全量同步再作數據校驗。
![1605191478713661.png image.png](http://static.javashuo.com/static/loading.gif)
老庫給新庫作數據同步
當完成數據同步以後,四個庫的數據保持一致了,修改負載均衡代理的配置爲 ID%4 的模式。此時擴容就完成了,從原來的 2 個數據庫擴展成 4 個數據庫。
固然會存在部分的數據冗餘,須要像上面一個方案同樣經過後臺服務刪除這些冗餘數據,刪除的過程不會影響業務。
![1605191482820673.png image.png](http://static.javashuo.com/static/loading.gif)
數據同步之後作 Hash 切分
分佈式事務原理
架構設計的分表分庫帶來的結果是咱們不得不考慮分佈式事務,今天咱們來看看分佈式事務須要記住哪兩個原理。
CAP
互聯網應用大多會使用分表分庫的操做,這個時候業務代碼極可能會同時訪問兩個不一樣的數據庫,作不一樣的操做。同時這兩個操做有可能放在同一個事務中處理。
這裏引出分佈式系統的 CAP 理論,他包括如下三個屬性:
一致性(Consistency):
分佈式系統中的全部數據,同一時刻有一樣的值。
業務代碼往數據庫 01 這個節點寫入記錄 A,數據庫 01 把 A 記錄同步到數據庫 02,業務代碼再從數據庫 02 中讀出的記錄也是 A。那麼兩個數據庫存放的數據就是一致的。
![820d09e953c30ae654d923cfaaeec15a.jpeg](http://static.javashuo.com/static/loading.gif)
一致性簡圖
可用性(Availability):
分佈式系統中一部分節點出現故障,分佈式系統仍舊能夠響應用戶的請求。
假設數據庫 01 和 02 同時存放記錄 A,因爲數據庫 01 掛掉了,業務代碼不能從中獲取數據。
那麼業務代碼能夠從數據庫 02 中獲取記錄 A。也就是在節點出現問題的時候,還保證數據的可用性。
![1605191525571402.png image.png](http://static.javashuo.com/static/loading.gif)
可用性簡圖
分區容錯性(Partition tolerance):
假設兩個數據庫節點分別在兩個區,而兩個區的通信發生了問題。就不能達成數據一致,這就是分區的狀況,我就須要從 C 和 A 之間作出選擇。
是選擇可用性(A),獲取其中一個區的數據。仍是選擇一致性(C),等待兩個區的數據同步了再去獲取數據。
這種狀況的前提是兩個節點的通信失敗了,寫入數據庫 01 記錄的時候,須要鎖住數據庫 02 記錄不讓其餘的業務代碼修改,直到數據庫 01 記錄完成修改。所以 C 和 A 在此刻是矛盾的。二者不能兼得。
![1605191530173104.png image.png](http://static.javashuo.com/static/loading.gif)
分區容錯簡圖
BASE
Base 原理普遍應用在數據量大,高併發的互聯網場景。
一塊兒來看看都包含哪些:
基本可用(Basically Available):
不會由於某個節點出現問題就影響用戶的請求。
即便在流量激增的狀況下,也會考慮經過限流降級的辦法保證用戶的請求是可用的。
好比,電商系統在流量激增的時候,資源會向核心業務傾斜,其餘的業務降級處理。
軟狀態( Soft State):一條數據若是存在多個副本,容許副本之間同步的延遲,在較短期內可以容忍不一致。這個正在同步而且尚未完成同步的狀態稱爲軟狀態。
![1605191536291593.png image.png](http://static.javashuo.com/static/loading.gif)
最終一致性( Eventual Consistency):
最終一致性是相對於強一致性來講的,強一致性是要保證全部的數據都是一致的,是實時同步。
而最終一致性會容忍一小段時間數據的不一致,但過了這段時間之後數據會保證一致。
其包含如下幾種「一致性」:
①因果一致性(Causal Consistency)
若是有兩個進程 1 和 2 都對變量 X 進行操做,「進程 1」 寫入變量 X,「進程 2」須要讀取變量 X,而後用這個 X 來計算 X+2。
這裏「進程 1」和「進程 2」 的操做就存在因果關係。「進程 2」 的計算依賴於進程 1 寫入的 X,若是沒有 X 的值,「進程 2」沒法計算。
![1605191540492869.png image.png](http://static.javashuo.com/static/loading.gif)
兩個進程對同一變量進行操做
②讀己之所寫(Read Your Writes)
「進程 1」寫入變量 X 以後,該進程能夠獲取本身寫入的這個值。
![1605191545969229.png image.png](http://static.javashuo.com/static/loading.gif)
進程寫入的值的同時獲取值
③會話一致性(Session Consistency)
若是一個會話中實現來讀己之所寫。一旦數據更新,客戶端只要在同一個會話中就能夠看到這個更新的值。
![1605191551252811.png image.png](http://static.javashuo.com/static/loading.gif)
多進程在同一會話須要看到相同的值
④單調寫一致性(Monotonic Write Consistency)
「進程 1」若是有三個操做分別是 1,2,3。「進程 2」有兩個操做分別是 1,2。當進程請求系統時,系統會保證按照進程中操做的前後順序來執行。
![1605191557291283.png image.png](http://static.javashuo.com/static/loading.gif)
多進程多操做經過隊列方式執行
分佈式事務方案
說完了分佈式的原理,再來提一下分佈式的方案。因爲所處場景不同,因此方案也各有不一樣,這裏介紹兩種比較流行的方案,兩段式和 TCC(Try,Confirm,Cancel)。
兩階段提交
顧名思義,事務會進行兩次提交。這裏須要介紹兩個概念,一個是事務協調者,也叫事物管理器。
它是用來協調事務的,全部事務何時準備好了,何時能夠提交了,都由它來協調和管理。
另外一個是參與者,也叫資源管理器。它主要是負責處理具體事務的,管理者須要處理的資源。例如:訂票業務,扣款業務。
第一階段(準備階段):
事務協調者(事務管理器)給每一個參與者(資源管理器)發送 Prepare 消息,發這個消息的目的是問「你們是否是都準備好了,咱們立刻就要執行事務了」。
參與者會根據自身業務和資源狀況進行檢查,而後給出反饋。
這個檢查過程根據業務內容不一樣而不一樣。
例如:訂票業務,就要檢查是否有剩餘票。扣款業務就要檢查,餘額是否足夠。一旦檢查經過了才能返回就緒(Ready)信息。
不然,事務將終止,而且等待下次詢問。因爲這些檢查須要作一些操做,這些操做可能再以後回滾時用到,因此須要寫 redo 和 undo 日誌,當事務失敗重試,或者事務失敗回滾的時候使用。
第二階段(提交階段):
若是協調者收到了參與者失敗或者超時的消息,會給參與者發送回滾(rollback)消息;不然,發送提交(commit)消息。
兩種狀況處理以下:
狀況 1,當全部參與者均反饋 yes,提交事務:
協調者向全部參與者發出正式提交事務的請求(即 commit 請求)。
參與者執行 commit 請求,並釋放整個事務期間佔用的資源。
各參與者向協調者反饋 ack(應答)完成的消息。
協調者收到全部參與者反饋的 ack 消息後,即完成事務提交。
狀況 2,當有一個參與者反饋 no,回滾事務:
協調者向全部參與者發出回滾請求(即 rollback 請求)。
參與者使用第一階段中的 undo 信息執行回滾操做,並釋放整個事務期間佔用的資源。
各參與者向協調者反饋 ack 完成的消息。
協調者收到全部參與者反饋的 ack 消息後,即完成事務。
![1605191566787926.png image.png](http://static.javashuo.com/static/loading.gif)
兩個階段提交事務示意圖
TCC(Try,Confirm,Cancel)
對於一些要求高一致性的分佈式事務,例如:支付系統,交易系統,咱們會採用 TCC。
它包括,Try 嘗試,Confirm 確認,Cancel 取消。看下面一個例子可否幫助你們理解。
假設咱們有一個轉帳服務,須要把「A 銀行」「A 帳戶」中的錢分別轉到「B銀行」「B 帳戶」和「C 銀行」「C 帳戶」中去。
假設這三個銀行都有各自的轉帳服務,那麼此次轉帳事務就造成了一次分佈式事務。
咱們來看看用 TCC 的方式如何解決:
![1605191571598677.png image.png](http://static.javashuo.com/static/loading.gif)
轉帳業務示意圖
首先是 Try 階段,主要檢測資源是否可用,例如檢查帳戶餘額是否足夠,緩存,數據庫,隊列是否可用等等。
並不執行具體的邏輯。如上圖,這裏從「A 帳戶」轉出以前要檢查,帳戶的總金額是否大於 100,而且記錄轉出金額和剩餘金額。
對於「B 帳戶」和「C 帳戶」來講須要知道帳戶原有總金額和轉入的金額,從而能夠計算轉入後的金額。
這裏的交易數據庫設計除了有金額字段,還要有轉出金額或者轉入金額的字段,在 Cancel 回滾的時候使用。
![1605191580763032.png image.png](http://static.javashuo.com/static/loading.gif)
Try 階段示意圖
若是 Try 階段成功,那麼就進入 Confirm 階段,也就是執行具體的業務邏輯。
這裏從「A 帳戶」轉出 100 元成功,剩餘總金額=220-100=120,把這個剩餘金額寫入到總金額中保存,而且把交易的狀態設置爲「轉帳成功」。
「B 帳戶」和「C 帳戶」分別設置總金額爲 80=50+30 和 130=60+70,也把交易狀態設置爲「轉帳成功」。則整個事務完成。
![1605191585818845.png image.png](http://static.javashuo.com/static/loading.gif)
Confirm 階段示意圖
若是 Try 階段沒有成功,那麼服務 A B C 都要作回滾的操做。對於「A帳戶」來講須要把扣除的 100 元加回,因此總金額 220=120+100。
那麼「B 服務」和「C 服務」須要把入帳的金額從總金額裏面減去,也就是 50=80-30 和 60=130-70。
![1605191589678581.png image.png](http://static.javashuo.com/static/loading.gif)
Cancel 階段示意圖
TCC 接口實現
這裏須要注意的是,須要針對每一個服務去實現 Try,Confirm,Cancel 三個階段的代碼。
例如上面所說的檢查資源,執行業務,回滾業務等操做。目前有不少開源的架構例如:ByteTCC、TCC-transaction 能夠借鑑。
![1605191594137306.png image.png](http://static.javashuo.com/static/loading.gif)
TCC 實現接口示意圖
TCC 可靠性
TCC 經過記錄事務處理日誌來保證可靠性。一旦 Try,Confirm,Cancel 操做的時候服務掛掉或者出現異常,TCC 會提供重試機制。另外若是服務存在異步的狀況能夠採用消息隊列的方式通訊保持事務一致。
![1605191599552358.png image.png](http://static.javashuo.com/static/loading.gif)
重試機制示意圖
分庫表中間件介紹
若是以爲分表分庫以後,須要考慮的問題不少,可使用市面上的現成的中間件幫咱們實現。
這裏介紹幾個比較經常使用的中間件:
基於代理方式的有 MySQL Proxy 和 Amoeba。
基於 Hibernate 框架的有 Hibernate Shards。
基於 JDBC 的有當當 Sharding-JDBC。
基於 MyBatis 的相似 Maven 插件式的蘑菇街 TSharding。
另外着重介紹 Sharding-JDBC 的架構,它的構成和「服務註冊中心」很像。
Sharding-JDBC 會提供一個 Sharding-Proxy 作代理,他會鏈接一個註冊中心(registry center),一旦數據庫的節點掛接到系統中,會在這個中心註冊,同時也會監控數據庫的健康情況作心跳檢測。
而 Sharding-Proxy 自己在業務代碼(Business Code)請求數據庫的時候能夠協助作負載均衡和路由。
同時 Sharding-Proxy 自己也能夠支持被 MySQL Cli 和 MySQL Workbench 查看。
實際上若是咱們理解了分表分庫的原理以後,實現並不難,不少大廠都提供了產品。
![1605191605626618.png image.png](http://static.javashuo.com/static/loading.gif)
Sharding-Proxy 實現原理圖
總結
由於數據量的上升,爲了提升性能會對系統進行分表分庫。從分表來講,有水平分表和垂直分表兩種方式。
能夠根據業務,冷熱數據等來進行分庫,分庫之後經過主從庫來實現讀寫分離。
若是對分庫以後數據庫作擴容,有兩種方式,主從數據庫擴容和雙寫數據庫擴容。
分表分庫會帶來分佈式事務,咱們須要掌握 CAP 和 BASE 原理,同時介紹了兩階段提交和 TCC 兩個分佈式事務方案。最後,介紹了流行的分表分庫中間件,以及其實現原理。