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