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