前言
俗話說:面試造火箭,入職擰螺絲。盡管99.99%的業(yè)務(wù)都不需要用到分庫分表,但是分庫分表還是頻繁出現(xiàn)在大廠的面試中。
分庫分表涉及到的內(nèi)容非常多,有很多細節(jié),如果在面試中被問到了,既是挑戰(zhàn),也是機會,如果你能回答好的話,會給你的面試加很多分。
由于業(yè)務(wù)量的關(guān)系,絕大部分同學都很難有實際分庫分表的機會,因此很多同學在碰到這個問題時很容易懵逼。
因此今天跟大家分享一下分庫分表的相關(guān)知識,本文內(nèi)容源于實際高并發(fā)+海量數(shù)據(jù)業(yè)務(wù)下的實戰(zhàn)和個人的思考總結(jié)。
什么是分庫分表
分表
分表指的是在數(shù)據(jù)庫數(shù)量不變的情況下,對數(shù)據(jù)庫里面的表進行拆分。
例如我們將SPU表從一張拆成四張。
分庫
分庫指的是在表數(shù)量不變的情況下對數(shù)據(jù)庫進行拆分。
例如我們本來有一個庫里面放了兩張表,一張是SPU表,一張是SKU表。我們將這兩張表拆到兩個不同的庫里面去。
分庫分表
也就是數(shù)據(jù)庫的數(shù)量,還有表的數(shù)量都發(fā)生變更。
例如我們有一個數(shù)據(jù)庫里面本來有一張SPU表。我們將這個SPU表拆成四張表,并且放在兩個數(shù)據(jù)庫里面。
拆分方式
當前主要的拆分方式有兩種:水平拆分和垂直拆分。
水平拆分就是從左往右橫著切,垂直拆分就是從上往下豎著切。當然具體切幾刀,這個要看具體的業(yè)務(wù)需求。
水平拆分
水平拆分指的是在整個表數(shù)據(jù)結(jié)構(gòu)不發(fā)生變更的情況下,將一張表的數(shù)據(jù)拆分成多張表。因為當單張表的數(shù)據(jù)量越來越大時,這張表的查詢跟寫入性能也會相應(yīng)的變得越來越慢。
因此這個時候我們可以將單張表拆分成多張表,從而讓每張表的數(shù)據(jù)量都變小,從而可以提供更好的讀寫性能。
垂直拆分
垂直拆分指的是將本來放在一張表的字段拆分到多張表中。
例如在這個例子中,我們將pic這個字段單獨拆分出來,然后剩下的三個字段還保留在原表里面。
這種場景主要是因為在業(yè)務(wù)的初期,為了業(yè)務(wù)的快速發(fā)展,我們將商品的所有字段都放在一張表里面。但是隨著后面的業(yè)務(wù)的發(fā)展,我們發(fā)現(xiàn)這個pic字段可能變得越來越大,從而影響到我們商品的基本信息的查詢性能。因此這個時候我們可以將這個pic字段單獨拆分出去。
當然這個pic字段拆分出去之后,它應(yīng)該要存儲這個原來這個商品的這個id。
為什么需要分庫分表
因為單臺MySQL服務(wù)器的硬件資源是有限的,隨著業(yè)務(wù)的不斷發(fā)展,請求量和數(shù)據(jù)量會不斷增加,數(shù)據(jù)庫的壓力會越來越大,到了某一時刻,數(shù)據(jù)庫的讀寫性能可能會開始下降,這個時候數(shù)據(jù)庫就成為請求鏈路中的瓶頸。
此時可能就需要我們?nèi)?shù)據(jù)庫進行優(yōu)化,業(yè)務(wù)初期我們可能會使用增加索引、優(yōu)化索引、讀寫分離、增加從庫等手段來進行優(yōu)化,但是隨著數(shù)據(jù)量的不斷增大,這些優(yōu)化手段的效果會變得越來越小,此時可能就需要使用分庫分表來進行優(yōu)化,對數(shù)據(jù)進行切分,將單庫和單表的數(shù)據(jù)量控制在合理的范圍內(nèi),以保證數(shù)據(jù)庫可以提供高效的讀寫能力。
何時需要分庫分表
總體來說:當性能出現(xiàn)瓶頸,并且其他優(yōu)化手段無法很好的解決的時候。
我們這邊必須首先明確分庫分表一般是作為最終的解決手段,我們會優(yōu)先使用其他的方法來進行優(yōu)化。常見的優(yōu)化手段有增加索引、優(yōu)化索引、讀寫分離、增加數(shù)據(jù)庫的從庫等等。當我們使用這些手段都無法解決的時候,就需要來考慮分庫分表。
單表出現(xiàn)瓶頸:
- 單表數(shù)據(jù)量較大,導(dǎo)致讀寫性能較慢。
單庫出現(xiàn)瓶頸:
- CPU壓力過大(busy、load過高),導(dǎo)致讀寫性能較慢。
- 內(nèi)存不足(緩存池命中率較低、磁盤讀寫IOPS過高),導(dǎo)致讀寫性能較慢。
- 磁盤空間不足,導(dǎo)致無法正常寫入數(shù)據(jù)。
- 網(wǎng)絡(luò)帶寬不足,導(dǎo)致讀寫性能較慢。
單表超過千萬級,就需要進行分庫分表?
這種說法不完全準確。因為有的表它本身的結(jié)構(gòu)比較簡單,字段也比較少。這種表可能即使數(shù)據(jù)量已經(jīng)超過了億級,整體的讀寫性能也是比較高的。而有的表如果整體的結(jié)構(gòu)比較復(fù)雜,字段本身也比較大,可能只是百萬級,整體的性能已經(jīng)比較慢了。所以這個還是得結(jié)合自己的業(yè)務(wù)情況來進行分析。這個千萬級只能是作為一個參考。
如何選擇分庫分表
只分表:
- 單表數(shù)據(jù)量較大,單表讀寫性能出現(xiàn)瓶頸。
- 經(jīng)過評估單庫的容量和性能可以支撐未來幾年的增長。
只分庫:
- 數(shù)據(jù)庫(讀)寫壓力較大,數(shù)據(jù)庫出現(xiàn)存儲性能瓶頸。
分庫分表:
- 單表數(shù)據(jù)量較大,單表讀寫性能出現(xiàn)瓶頸。
- 數(shù)據(jù)庫(讀)寫壓力較大,數(shù)據(jù)庫出現(xiàn)存儲性能瓶頸。
注意點:
我們在進行選擇的時候,必須以未來三到五年的業(yè)務(wù)發(fā)展情況去進行評估。不能只是以當前的數(shù)據(jù)量和業(yè)務(wù)量來進行評估。否則可能就會出現(xiàn)頻繁的進行分庫分表的情況。因為分庫分表整體的代價是比較大的。所以我們最好是進行充分的評估,保證最少可以支撐未來三到五年的業(yè)務(wù)增長。
小結(jié)
當數(shù)據(jù)庫出現(xiàn)了讀寫性能瓶頸的時候,我們優(yōu)先使用一些比較常規(guī)的優(yōu)化手段來進行解決。例如比較常見的有:增加索引、優(yōu)化索引、讀寫分離、增加從庫等方式。
如果使用這些常規(guī)的手段也無法解決的時候啊,我們才會去考慮用分庫分表來進行解決。
在使用分庫分表的時候,必須充分考慮業(yè)務(wù)未來的整體發(fā)展。至少做到這次分庫分表之后,未來的三到五年內(nèi)不需要再進行分庫分表。
拆分完整流程概覽
1、評估是否需要拆分。主要就是評估是否有其他更輕量的優(yōu)化手段可以解決問題,從而可以避免進行分庫分表。
2、拆分詳細技術(shù)方案設(shè)計。最核心的內(nèi)容是拆分SOP,也是我們今天后續(xù)要詳細講的內(nèi)容。
3、技術(shù)方案評審優(yōu)化。分庫分表的整體改動比較大,需要讓大家一起評估下方案是否有問題,或者是否存在可以優(yōu)化的地方。
4、同步相關(guān)影響方。拆分可能需要一些下游配合改造,需要提前周知他們。
5、正式進入拆分。
接下來我們來看一下拆分的SOP。
拆分SOP(核心)
1、目標評估。
我們首先要評估本次拆分需要拆成幾個庫和幾個表,這個主要取決于我們的拆分目標,例如:讀寫能力要提升到現(xiàn)在的X倍、負載降低Y%、容量要支撐未來的Z年發(fā)展等等。
在大多數(shù)情況下,我們可以將單表的行數(shù)作為一個重要參考指標,例如將單表控制在千萬級以下。特殊情況下如果你要拆分的表單行數(shù)據(jù)很大,例如字段很多或者某字段很大,這種情況你需要結(jié)合實際的性能表現(xiàn)去評估一個合理的值。
一個例子:當前數(shù)據(jù)20億,5年后評估為100億。分幾個表?分幾個庫?
解答:一個合理的答案,1024個表,16個庫。按1024個表算,拆分完單表200萬,5年后為1000萬。
2、切分策略
當前主流的方案有3種:范圍切分、中間表映射、hash切分。
范圍切分
范圍切分是指按某個字段的區(qū)間來進行切分。例如每個表放1000萬數(shù)據(jù),id從0~1000萬的放在第一個表,1000萬~2000萬放在第2個表,依次類推。
優(yōu)點:后續(xù)擴容很方便,無需進行遷移數(shù)據(jù),甚至可以將后續(xù)的表擴容、數(shù)據(jù)庫擴庫全部做到自動化。
缺點:存在明顯的寫偏移,寫流量其實是全部集中在最新的表上。因此范圍切分并沒有起到將寫流量均勻分攤到各個庫各個表的效果,同時讀流量可能也會存在偏移,因為一般來說,最近增加的數(shù)據(jù)被查詢的概率通常會更大一點。
中間表映射
中間表映射是將分表鍵和數(shù)據(jù)庫的映射關(guān)系記錄在一個單獨的表中,每次路由前先查詢該表,得到具體路由的數(shù)據(jù)庫,然后進行操作。
優(yōu)點:很靈活,可以隨意設(shè)置路由規(guī)則。
缺點:引入了額外的單點,增加了復(fù)雜度,這個映射表可能也會很大,并且其查詢QPS會非常高,怎么保障高性能和高可用會是一個新的問題。
Hash切分
通過對分表鍵進行一定的運算(通常是取模),從而決定路由到哪個庫哪個表。
優(yōu)點:數(shù)據(jù)分片比較均勻,讀寫也會比較均勻的分攤到各個庫和各個表。
缺點:可能存在跨節(jié)點查詢和分頁等問題。
小結(jié)
目前大多數(shù)互聯(lián)網(wǎng)服務(wù)主要使用的是hash切分。
范圍切分存在寫流量集中在單表的問題,這個會有嚴重的寫性能問題,特別是隨著業(yè)務(wù)的發(fā)展,寫流量的QPS會越來越高,這個會成為一個嚴重的瓶頸,目前看這個方案可能更適合一些歸檔類的功能。
中間表映射的方案則是太復(fù)雜了,如果你的映射數(shù)據(jù)太多的話,甚至有可能這個映射表也需要進行分庫分表,那就進入惡性循環(huán)了。
不過,雖然中間表映射雖然有一些問題,但是我覺得可能在一些特殊的場景下可以使用,例如大商家問題。如果有少量商家的數(shù)據(jù)量特別大,導(dǎo)致出現(xiàn)偏移,一種思路是將這些商家的數(shù)據(jù)使用單獨的表存放,這部分大商家通過中間表映射路由,其他的商家還是走hash路由。當然,這只是一個簡單的思考,沒有經(jīng)過嚴格的驗證。
3、選擇分表字段
在單庫單表的時候,全部數(shù)據(jù)都放在一張表中,因此我們可以隨意的進行 join 操作和分頁操作,但是如果進行了分庫分表,數(shù)據(jù)會分到不同的數(shù)據(jù)庫和數(shù)據(jù)表上,可能導(dǎo)致原本進行分頁的數(shù)據(jù)分到了不同的數(shù)據(jù)庫中,從而導(dǎo)致跨庫查詢等問題。而分表字段就是決定數(shù)據(jù)如何劃分的關(guān)鍵因素,通過合理的選擇分表字段,我們可以將原本需要進行分頁的數(shù)據(jù)劃分到同一張表上,從而避免跨庫查詢的問題。
例子:以美團外賣的商品數(shù)據(jù)為例,我們可以思考下主要有哪些查詢商品的場景。
第一個是用戶視角,我們在點外賣時需要查詢商品,但是我們在點外賣時會首先進入到商家頁面,所以這個地方有商家id字段。
第二個是商家視角,商家在后臺管理自己的商品,這個地方也有商家id字段。
因此在美團外賣商品數(shù)據(jù)的這個例子中,商家id字段作為分表鍵就是一個比較合理的選擇,因為他覆蓋了最高頻的幾個使用場景。
一個例子:10個庫,1000張表:0~99、100~199、200~299、…
分表字段:shopId,值為1234
數(shù)據(jù)表編號:shopId % 1000 = 1234 % 1000 = 234
數(shù)據(jù)庫編號:shopId % 1000 / 10 = 1234 % 1000 / 10 = 2
4、資源準備和代碼改造
新集群的所需數(shù)據(jù)庫資源可以盡早跟DBA申請,特別是拆分集群比較多的情況,一方面是因為DBA搭建新集群需要花一定的時間,另一方面是避免出現(xiàn)資源不足導(dǎo)致延期的情況。
至于代碼的改造,主要會涉及到幾個部分:
- 將新集群的數(shù)據(jù)源引入到我們的服務(wù)中
- 支持靈活的灰度讀寫操作
- 第三是數(shù)據(jù)全量遷移、一致性校驗等任務(wù)
因為整個分庫分表過程是不停機,并且無損的拆分,因此拆分過程中新老數(shù)據(jù)源會同時存在一段時間,在這段灰度期間,我們會通過配置中心和相關(guān)規(guī)則去靈活的控制究竟是寫新庫、寫老庫,還是雙寫,讀操作也類似。
5、增量數(shù)據(jù)同步(雙寫)
雙寫是為了保證增量數(shù)據(jù)在新庫和老庫都存在。
寫新庫是因為我們后續(xù)準備切換到新庫,因此新庫必須要有全部的數(shù)據(jù)。
寫老庫是因為我們不確定拆分過程中是否存在問題,通過寫老保證了老庫有全部的數(shù)據(jù),這樣萬一新流程有問題的時候,我們可以即使切回老庫的流程。從而保障了服務(wù)的可用性和穩(wěn)定性。
常見方案:
- 同步雙寫,在所有寫數(shù)據(jù)庫的地方進行修改,修改成寫兩份數(shù)據(jù)。當然,這個地方一般不會去修改全部的寫邏輯,而是在底層使用AOP來實現(xiàn)。
- 異步雙寫:寫老庫,監(jiān)聽binlog異步同步到新庫
- 中間件同步工具:通過一定的規(guī)則將數(shù)據(jù)同步到目標庫表
異步雙寫和中間件工具同步兩者本質(zhì)上類似,都是通過binlog的方式將數(shù)據(jù)寫入到新庫。只不過一個是你自己做,一個是中間件團隊幫你做。
這幾種方式一般來說不會差別太大,同步雙寫的寫入延遲可能會稍微小一點。
6、全量數(shù)據(jù)遷移
光有增量數(shù)據(jù)同步還沒法保證新庫有全部的數(shù)據(jù),我們還需要將以前的老數(shù)據(jù)全部遷移到新庫中。通過增量同步+全量遷移,我們才能保證新庫有完整的數(shù)據(jù)。
常見方案:
- 自己開發(fā)一個任務(wù)將老庫數(shù)據(jù)遷移到新庫。
- 使用中間件同步工具,將老庫數(shù)據(jù)同步到新庫。如果中間件有現(xiàn)成工具支持的話,一般建議好接使用現(xiàn)成的工具,這樣自己就不用再花時間去額外開發(fā)了。
注意點:
- 控制好同步速率
- 增量同步和全量遷移會同時進行,因此可能會存在并發(fā)寫同一條數(shù)據(jù),從而可能導(dǎo)致一些數(shù)據(jù)不一致的問題。
7、數(shù)據(jù)校驗、優(yōu)化和補償
在全量數(shù)據(jù)遷移完畢,增量同步也正常運行后,并不能直接將流量切到新庫。因為可能存在很多情況,導(dǎo)致新庫和老庫的數(shù)據(jù)可能沒法完全一致。
例如:我們的改造存在遺漏的地方,或者說并發(fā)修改導(dǎo)致數(shù)據(jù)問題,等等。因此,我們需要進行新老庫的數(shù)據(jù)校驗和補償,直到新老庫的數(shù)據(jù)一致了,才能進行流量切換。
方案:
- 增量數(shù)據(jù)校驗
- 全量數(shù)據(jù)校驗
- 人工抽檢
核心流程:
- 讀取老庫數(shù)據(jù)
- 讀取新庫數(shù)據(jù)
- 比較新老庫數(shù)據(jù),一致則繼續(xù)比較下一條數(shù)據(jù)
- 不一致則進行補償:
- 新庫存在,老庫不存在:新庫刪除數(shù)據(jù)
- 新庫不存在,老庫存在:新庫插入數(shù)據(jù)
- 新庫存在、老庫存在:比較所有字段,不一致則將新庫更新為老庫數(shù)據(jù)
注意點:
數(shù)據(jù)校驗是整個流程中最重要,通常也是花時間最多的一步。一方面是在并發(fā)下會出現(xiàn)很多種不一致的場景,另外是因為這一步是切讀之前的最后一個保障,因此我們必須再三確認數(shù)據(jù)是正確的。否則,切讀后可能就會導(dǎo)致一些線上問題。
8、灰度切讀
在數(shù)據(jù)一致性校驗通過后,我們開始將部分讀流量切換到新數(shù)據(jù)庫。
這一步必須遵循以下幾個原則:
- 必須支持靈活的切換,有問題可以及時切回老庫。
- 支持靈活的灰度規(guī)則,灰度早期我們會先拿少量門店進行灰度,觀察一段時間,如果沒問題再繼續(xù)增加灰度門店。依此類推,然后到后面開始逐步使用比例來進行灰度,直到最終我們將全部流量都切到新的數(shù)據(jù)庫上。
- 灰度放量先慢后快,每次放量觀察一段時間
9、binlog 切新庫
在讀流量全部切換到新庫后,此時新流程已經(jīng)驗證通過,我們開始為停寫老庫做準備,首先就是將監(jiān)聽的 binlog 從老庫切換到新庫。
核心流程:
- 啟動新庫的 binlog,此時下游會同時收到新老庫的 binlog
- 觀察一段時間是否正常
- 如果不正在,則將新庫的 binlog 關(guān)閉,排查修復(fù)問題
- 如果一切正常,則將老庫的 binlog 關(guān)閉,此時監(jiān)聽的 binlog 切換到新庫
注意點:
監(jiān)聽 binlog 的流程我們一般會收斂在團隊內(nèi)部,如果外部團隊想監(jiān)聽 binlog,一般會使用我們封裝過的消息,這樣在改造時,對外部團隊就基本沒有影響,我們改造起來也比較方便。
10、下游切換數(shù)據(jù)源
目前來看,除了 binlog 之外,主要的下游是數(shù)倉。數(shù)倉會將商品數(shù)據(jù)定期同步到 hive 上,用于進行數(shù)據(jù)的相關(guān)工作,因此需要讓數(shù)倉同學將數(shù)據(jù)源切換到新數(shù)據(jù)源。
數(shù)倉一般是定期同步數(shù)據(jù),例如一天同步一次全量數(shù)據(jù),對實時性要求不高,因此只需在指定時間內(nèi)切換即可。
11、停寫老庫
在我們確認老庫數(shù)據(jù)源的所有依賴都切換和下線后,停寫老庫,此時讀寫流程全部切換到新數(shù)據(jù)源。至此,整個拆分流程基本結(jié)束。
完整SOP
最后我們通過一張流程圖來回顧下整個拆分流程,整個流程主要包含5個階段。
第一階段:拆分前的相關(guān)準備,包含了拆分的目標評估、切分策略和分表字段的選擇,還有數(shù)據(jù)庫相關(guān)資源的準備。
第二階段:代碼改造,主要是將新數(shù)據(jù)源引入到服務(wù)中,同時支持靈活的灰度讀寫。
第三階段:數(shù)據(jù)遷移,包含了全量和增量數(shù)據(jù)遷移,還有數(shù)據(jù)一致性的校驗和修復(fù)。
第四階段:流量遷移,主要是將數(shù)據(jù)庫的讀寫流量按灰度規(guī)則逐步切換到新庫。
第五階段:停寫老庫,當讀寫流量全部遷移到新庫,老庫的相關(guān)依賴都全部下線后,停寫老庫并釋放相關(guān)資源。
相關(guān)工具
1、binlog監(jiān)聽工具
- Databus
- Canal
關(guān)于binlog
binlog是一個二進制文件,用于記錄數(shù)據(jù)庫表結(jié)構(gòu)和表記錄的變更。簡單點說,就是通過 binlog 文件你可以知道數(shù)據(jù)庫中究竟哪些數(shù)據(jù)發(fā)生了變更,從什么變成了什么。
而binlog監(jiān)聽工具主要就是用于監(jiān)聽MySQL產(chǎn)生的binlog,然后進行解析,解析成我們比較容易懂的格式,最后通過一定的手段發(fā)送到下游,例如比較常見的方式是消息隊列。
在分庫分表中就可以通過binlog監(jiān)聽工具來將老庫的數(shù)據(jù)變更實時同步到新庫中,以保證新老庫的數(shù)據(jù)一致。
2、分庫分表工具
目前主要有兩種,一種是增強版JDBC驅(qū)動,另一種是數(shù)據(jù)庫代理。
1)增強版JDBC驅(qū)動
以客戶端 jar 包形式提供了對 JDBC 的封裝,客戶端直連數(shù)據(jù)庫
開源:Sharding-JDBC、TDDL、Zebra
2)數(shù)據(jù)庫代理
需要單獨部署,客戶端連接代理服務(wù),代理服務(wù)負責跟數(shù)據(jù)庫打交道。
開源:Sharding-Proxy、MyCat
兩種方案的核心思想都是類似的,就是他們負責將分庫分表的邏輯進行抽象封裝,做到讓分庫分表對使用方無感知,使用方只需按照制定的規(guī)則進行簡單的配置和開發(fā),就可以像沒有分庫分表一樣正常的使用分庫分表規(guī)則了。
兩者的主要區(qū)別在于使用增強版JDBC驅(qū)動只需要依賴一個jar包,此時應(yīng)用服務(wù)還是直連數(shù)據(jù)庫的。
而數(shù)據(jù)庫代理則需要額外部署一個單獨的代理服務(wù),應(yīng)用服務(wù)從之前的直連數(shù)據(jù)庫,變成調(diào)用代理服務(wù),由代理服務(wù)來負責跟數(shù)據(jù)庫打交道。
目前使用的比較廣泛的是增強版JDBC驅(qū)動,一方面是增強版JDBC驅(qū)動比較輕量,另外是性能也會比較好。
分庫分表問題
在我們使用分庫分表之后,系統(tǒng)的性能和容量都會有很大的提升,但是也會隨之帶來一些問題。我們一起來看一下有哪些問題,當前的主流方案是如何解決的。
1、分布式唯一ID
在單庫單表情況下,我們使用表的自增ID就可以保證ID的唯一性,但是分庫分表后,一張表被拆成了多張表,此時自增ID就沒辦法保證唯一性了。因此,需要引入一種方案來保證ID的唯一性。
目前主流的方案有3種:UUID、雪花算法、號段模式。
UUID
UUID相信大家都不陌生,UUID是JDK中自帶的一個工具類。什么都不需要引入就可以直接使用了,同時因為是本地生成的,性能也非常好。
但是UUID并不適合拿來做MySQl數(shù)據(jù)庫的主鍵,MySQL的主鍵一般推薦使用單調(diào)遞增的數(shù)字,這個因為MySQL主鍵使用的是聚簇索引,會把相鄰主鍵的數(shù)據(jù)放在相鄰的物理存儲位置上。
當MySQL的主鍵是單調(diào)遞增時,每次只需要簡單的將數(shù)據(jù)追加到索引的最后面即可,類似于順序?qū)懘疟P。而如果MySQL的主鍵是無序的,則可能需要將數(shù)據(jù)插入到之前已有的數(shù)據(jù)中間。如果這個插入位置所在的數(shù)據(jù)頁不在內(nèi)存中,則需要先從磁盤讀取到內(nèi)存中,這會導(dǎo)致產(chǎn)生磁盤的隨機IO。同時,如果該數(shù)據(jù)頁的空間不足,則可能會產(chǎn)生頁分裂,導(dǎo)致需要移動大量數(shù)據(jù)。
最后就是,MySQL的普通索引需要存儲主鍵索引值,如果主鍵值更占用空間了,會導(dǎo)致普通索引的B+樹層高變高,磁盤IO次數(shù)變多,最終導(dǎo)致性能變慢。
雪花算法
雪花算法的核心思想是通過一定的規(guī)則生成一個64位的long類型數(shù)字。除了最高位的1位不用之外,其他63位由三部分組成。分別是41位用于存儲時間戳,10位用于存儲機器ID,12位用于存儲序列號。
簡單來說就是支持部署1024臺服務(wù)器,同時每臺服務(wù)器1毫秒最多可以生成4096個ID,也就是每秒可以生成四百零九萬個,并且可以使用69年。
這個量級應(yīng)該基本可以滿足任何業(yè)務(wù)了,當然在實際使用過程中,這三部分的位數(shù)可以結(jié)合自己的場景去進行修改。
號段模式
在講號段模式之前,我們先介紹下數(shù)據(jù)庫生成的方式。
數(shù)據(jù)庫生成指的是使用一個額外表的自增ID來作為分布式ID,因為ID都是由同一張表自增生成,所以可以保證全局唯一性。但是這種方案有個嚴重的問題,每次使用分布式唯一ID都需要來讀寫這張表。一旦并發(fā)量比較大,數(shù)據(jù)庫會有嚴重的性能問題。
號段模式就是在此基礎(chǔ)上進行了優(yōu)化,之前是每次獲取分布式ID都需要讀寫數(shù)據(jù)庫,號段模式優(yōu)化成批量的方式,每次讀寫數(shù)據(jù)庫時獲取一批ID,例如每次獲取1000個,將這1000個ID放在本地緩存中,1000個用完之后再來申請下一批,從而大大降低數(shù)據(jù)庫的讀寫壓力。
小結(jié)
這三種方案中,目前應(yīng)用的比較廣泛的是雪花算法和號段模式,美團開源的分布式ID生成組件 Leaf 就是提供了這兩種方案,如果大家對底層細節(jié)感興趣的話,可以去自己下載源碼來看。
最后需要說一下的是,對于訂單ID這種比較特殊的字段來說,一般可能不會直接使用上述的方案,而是會按照一定的規(guī)則去生成。同時可能會攜帶一些業(yè)務(wù)字段,例如用戶ID和商家ID。
2、分布式事務(wù)
在分庫分表之前,全部的表都在同一個庫里,我們可以使用本地事務(wù)來保障數(shù)據(jù)的正確性。引入了分庫分表之后,數(shù)據(jù)庫表被分到不同的數(shù)據(jù)庫中,此時就沒辦法使用本地事務(wù)了,因此就需要引入分布式事務(wù)來保障數(shù)據(jù)的正確性,我們來看一下當前有哪些常見的分布式事務(wù)。
2PC
兩階段提交,核心思想是將事務(wù)操作分為兩個階段。
第一階段:協(xié)調(diào)者首先詢問所有的事務(wù)參與者是否可以執(zhí)行事務(wù)提交操作。
第二階段:協(xié)調(diào)者根據(jù)所有參與者的返回結(jié)果決定是否提交事務(wù),如果全部的參與者都返回成功,則協(xié)調(diào)者向所有參與者發(fā)送事務(wù)提交請求。否則,協(xié)調(diào)者向所有參與者發(fā)送事務(wù)中斷回滾請求。
兩階段提交是目前比較出名也是用的相對比較多的分布式事務(wù),優(yōu)點是整體流程比較簡單,缺點是存在同步阻塞、協(xié)調(diào)者單點等問題。
TCC
核心思想是針對每個操作都有一個對應(yīng)的確認和取消操作。
TCC中有主服務(wù)和從服務(wù)兩個角色,例如在下單的流程中,首先會走到交易服務(wù),然后交易服務(wù)分別請求定訂單服務(wù)和庫存服務(wù)進行訂單創(chuàng)建和庫存扣減,此時交易服務(wù)就是主服務(wù),而訂單服務(wù)和庫存服務(wù)為從服務(wù)。
TCC的核心流程如下:
首先,主服務(wù)調(diào)用所有從服務(wù)的try接口,進行業(yè)務(wù)檢查和資源預(yù)留。
接著,主服務(wù)根據(jù)所有從服務(wù)的返回結(jié)果決定是否提交事務(wù),如果所有從服務(wù)都返回成功,則調(diào)用所有從服務(wù)的confirm接口執(zhí)行事務(wù)確認提交操作。否則,調(diào)用所有從服務(wù)的cancel接口執(zhí)行事務(wù)取消,并釋放預(yù)留資源。
估計大家應(yīng)該發(fā)現(xiàn)了,TCC其實跟兩階段提交非常像。其實很多分布式事務(wù)的思想都是很類似的,核心都是先詢問,然后提交。這兩者的主要區(qū)別在于TCC是應(yīng)用層的處理,而兩階段提交是數(shù)據(jù)庫層面的處理。
這兩種分布式事務(wù)應(yīng)該是目前分布式事務(wù)中比較出名的了,其他的分布式事務(wù)還有三階段提交、本地消息表、事務(wù)消息等等,這邊不做過多的介紹,有興趣的可以自己查閱資料。
高并發(fā)業(yè)務(wù)實際使用
首先說一下結(jié)論:在實際的高并發(fā)業(yè)務(wù)中一般都不會使用強一致性的分布式事務(wù),金融場景是個特例,因為涉及到太多錢了,所以可能會用強一致性的分布式事務(wù)。
更多的是通過各種各樣的手段來保證最終的一致性,常見的手段有:回滾、重試、監(jiān)控、告警、冪等、對賬等等,終極手段就是人工補償。
我之前在某篇文章中說過:每個看著光鮮亮麗的系統(tǒng)背后可能都有一堆苦逼的程序員在默默的修數(shù)據(jù),這個不是開玩笑的。
例子:
以外賣下單為例,整個用戶下單流程會涉及到很多步驟,最核心的包括:創(chuàng)建訂單、扣減商品庫存、核銷優(yōu)惠券、核銷會員紅包等等,如果其中有一步失敗,則會導(dǎo)致整個下單流程失敗,需要將其他的流程都進行回滾,以保證不會產(chǎn)生資損,否則有可能出現(xiàn)用戶下單失敗,但是會員紅包卻被扣掉等情況。
為了避免網(wǎng)絡(luò)抖動等情況導(dǎo)致回滾失敗,一般都會有回滾重試流程,但是重試一般會有次數(shù)上限,因為如果重試多次還是失敗,則可能是其他問題,例如代碼BUG,這種情況再怎么重試也沒用。因此在重試達到上限后,如果還是回滾失敗,則需要發(fā)送告警,人為介入排查,然后人工修復(fù)這些數(shù)據(jù)。
而對于這些訂單的下游服務(wù)來說,例如庫存、優(yōu)惠券等等,就需要做好接口的冪等,如果沒做好冪等,可能會導(dǎo)致數(shù)據(jù)出現(xiàn)重復(fù)回滾,造成數(shù)據(jù)錯誤和資損。
當然,從廣義上來說,保證最終一致性,也是屬于分布式事務(wù)的一種。
為什么不直接使用強一致性事務(wù)?
個人覺得主要有以下幾個原因:
- 會帶來嚴重的性能損耗,導(dǎo)致下單流程的耗時增加,最終導(dǎo)致服務(wù)吞吐量下降、用戶下單體驗變差。
- 會引入額外的復(fù)雜度,開發(fā)和維護成本較高。
- 實際業(yè)務(wù)中,由于部分成功導(dǎo)致數(shù)據(jù)不一致的場景,發(fā)生的概率比較低。
總結(jié)來說就是一個取舍的問題,目前大部分業(yè)務(wù)場景,使用強一致性分布式事務(wù)的ROI不夠高,因此一般不會選擇強一致性事務(wù),而是選擇柔性事務(wù),保障事務(wù)的最終一致性。
3、跨庫JOIN/分頁查詢問題
在單庫單表的時候,全部數(shù)據(jù)都放在一張表中,因此我們可以隨意的進行 join 和分頁操作,但是如果進行了分庫分表,數(shù)據(jù)會分到不同的數(shù)據(jù)庫和數(shù)據(jù)表上,可能導(dǎo)致原本進行分頁的數(shù)據(jù)分到了不同的數(shù)據(jù)庫中,從而導(dǎo)致跨庫查詢問題。
目前業(yè)界主流解決方案有以下幾種。
1)選擇合適的分表字段
這個在上文已經(jīng)詳細解釋過了??偨Y(jié)來說就是,分表字段的選擇,要能保證絕大部分高頻查詢場景,不會出現(xiàn)跨庫的問題。在實際業(yè)務(wù)中,分表字段選擇合理的話,基本可以避免95%,甚至99%以上的跨庫查詢問題,從而將問題的難度大大降低了。
2)使用搜索引擎支持,例如ES
我們可以將全量數(shù)據(jù)冗余一份到ES中,當出現(xiàn)分表字段支持不了的跨庫查詢時,可以使用ES來支持。除此之外,ES也會用于支持一些復(fù)雜搜索查詢請求。
使用ES需要注意的是:
- ES只存儲需要進行搜索的字段,查詢完ES后再根據(jù)關(guān)鍵字段去數(shù)據(jù)庫查詢完整的數(shù)據(jù),這樣是為了控制ES的大小,否則ES會容易過大,導(dǎo)致性能和存儲問題。
- ES只用于支持數(shù)據(jù)庫難以支持的查詢,就如上面說的跨庫查詢、復(fù)雜搜索查詢,這種復(fù)雜的查詢一般不會太多,因此可以保障ES的整體壓力不會太大。
3)分開查詢,內(nèi)存中聚合
這個方案跟使用join其實大同小異。區(qū)別在于,join是數(shù)據(jù)庫來做這個聚合操作,分開查詢是應(yīng)用層面來做聚合操作。
即使不分庫分表,當表的數(shù)據(jù)量比較大時,通常也是建議不要在數(shù)據(jù)庫中使用join操作,而是分開查詢,然后在應(yīng)用層內(nèi)存中聚合。
這是因為數(shù)據(jù)庫資源相對應(yīng)用服務(wù)器來說會更寶貴,通常也更容易成為鏈路中的瓶頸,因此盡量不要讓其做復(fù)雜的查詢,避免占用過多的數(shù)據(jù)庫資源。
注意點:
- 查詢出來的數(shù)據(jù)量
- 占用內(nèi)存情況
4)冗余字段
如果每次join操作只是為了獲取少量的字段,那么可以考慮直接將這些字段冗余到表上。
小結(jié)
這幾種方案在實際工作中都挺常使用的,一般看具體的業(yè)務(wù)場景選擇合適的方案即可。
原文出自公眾號:程序員囧輝
原文鏈接:https://mp.weixin.qq.com/s/X7ciEPZWLzgg_fnsCsr6wg