本文目錄:
介紹什么是分庫分表;
為什么要分庫分表;
怎么做分庫分表,小米是如何實(shí)現(xiàn)的;如何進(jìn)行數(shù)據(jù)遷移。
分庫分表遇到的問題;
分庫分表的下一代解決方案;
介紹什么是分庫分表,為什么要分庫分表;
介紹分庫分表之前,要說下數(shù)據(jù)庫架構(gòu)的演進(jìn)過程。最早的數(shù)據(jù)庫是單體應(yīng)用,和我們的業(yè)務(wù)系統(tǒng)部署在同一個機(jī)器上。隨著業(yè)務(wù)發(fā)展,數(shù)據(jù)庫和業(yè)務(wù)系統(tǒng)分開部署,大量的讀請求會觸發(fā)高頻次的隨機(jī)IO,這在一定程度上影響了寫請求,且我們的業(yè)務(wù)幾乎都是讀多寫少,因此數(shù)據(jù)庫演變成了一主多從的部署方式,且實(shí)現(xiàn)了讀寫分離。寫只寫主庫,讀只讀從庫。架構(gòu)類似如下:
一主多從
小米之前基于開源KingShard中間件做了一層代理,客戶端發(fā)起請求后,代理會解析SQL,根據(jù)sql類型以及是否有顯示指定主庫來決定應(yīng)該把請求發(fā)給主庫還是從庫。
讀寫分離一定程度上分?jǐn)偭烁卟l(fā)請求和檢索性能,但如果單庫或者單表的數(shù)據(jù)量過大,其TPS/QPS以及查詢能力都會下降。下面是我們現(xiàn)在一個業(yè)務(wù)系統(tǒng)的幾張表,都幾十G了。
下面是數(shù)據(jù)庫健康狀況監(jiān)控的例子:
數(shù)據(jù)庫監(jiān)控
當(dāng)單庫或者單表的數(shù)據(jù)量過大時,無論如何進(jìn)行性能調(diào)優(yōu)(重建索引,優(yōu)化SQL等,增加單機(jī)配置),都無法改變當(dāng)前的讀請求的性能,此時單DB的性能已經(jīng)達(dá)到了瓶頸。如果讀寫請求頻次不高,可以忍一忍。但如果還是讀寫非常頻繁的業(yè)務(wù),特別是高并發(fā)場景下,基本上整個業(yè)務(wù)會受到DB的拖累,嚴(yán)重的會導(dǎo)致整個系統(tǒng)崩潰。此時,就要考慮進(jìn)行分庫分表了。
分庫分表的基本思想就是將之前揉在一個機(jī)器上的庫和表根據(jù)某種規(guī)則進(jìn)行拆分,將單表數(shù)據(jù)分散到多個字庫或者多張表上,不同的庫表會部署在不同的機(jī)器上,從而減輕單個節(jié)點(diǎn)的壓力。類似下圖,單張1000w條的數(shù)據(jù)表,根據(jù)某個規(guī)則拆分成10張表:
分表示例
怎么做分庫分表;
拆分方式
拆分方式包括兩種:垂直拆分和水平拆分
1)垂直拆分
垂直拆分主要是拆庫,根據(jù)業(yè)務(wù)功能區(qū)劃分,比如在電商系統(tǒng)中訂單,商品,庫存,履約,風(fēng)控等業(yè)務(wù)應(yīng)該使用獨(dú)立的數(shù)據(jù)庫。記得在2018年,那時我們后端所有業(yè)務(wù)的數(shù)據(jù)表都在一個庫中,有個負(fù)責(zé)拼團(tuán)的同學(xué)在自己代碼中引入了一個bug,大概邏輯是遍歷所有用戶并在每個循環(huán)內(nèi)都執(zhí)行一次慢查詢,用戶數(shù)量應(yīng)該有幾十萬。不出意外,在618的時候,系統(tǒng)崩掉了,導(dǎo)致30分鐘都下不了單。導(dǎo)致出現(xiàn)問題的根本原因就是其他業(yè)務(wù)的慢查詢拖垮了DB,從而影響了使用同一個DB的其他業(yè)務(wù),尤其是下單,這簡直是個災(zāi)難。因此,我們痛定思痛,開始進(jìn)行拆庫,各個業(yè)務(wù)線逐步將數(shù)據(jù)庫拆分出去。
垂直分庫
垂直拆分是和分布式系統(tǒng)相輔相成的,微服務(wù)的建立的前提應(yīng)該就是底層數(shù)據(jù)源的隔離,否則毫無意義。那么垂直拆分的原則在上面也說了,要根據(jù)業(yè)務(wù)去劃分,這個地方倒是不復(fù)雜。
2)水平分庫分表
水平拆分包括既分庫又分表,也可以不分庫只分表,兩種方式的目的都是將單張表數(shù)據(jù)分散到多個分表,每個分表的數(shù)量成倍減少,從而減輕了對單表的讀寫壓力。既分庫又分表的做法是不僅僅分表,還要根據(jù)一定規(guī)則分到不同的庫中,不同的庫可以部署在不同的機(jī)器上。相比于只分表不分庫的做法,分庫分表可以降低單臺機(jī)器的瓶頸,畢竟單臺機(jī)器的CPU,內(nèi)存,磁盤IO,帶寬等都是有限的。因此,通常情況下我們都會采用分庫分表的做法。
水平分庫分表
拆分規(guī)則
分庫分表該如何去拆分?該采用什么規(guī)則將數(shù)據(jù)平均分散到各個分表上?是不是符合我們實(shí)際的業(yè)務(wù)場景?
目前業(yè)界通用的分表規(guī)則主要有以下幾種:
1、按照范圍range分配
思想:按照某個字段值以及某種規(guī)則拆分,每個子表都劃分一定范圍的數(shù)據(jù)??梢园凑?span id="liebndo" class="wpcom_tag_link">時間或者其他的分片字段。
優(yōu)點(diǎn):基于范圍查詢或更新速度快,需要的數(shù)據(jù)可能落在同一張子表中,避免遍歷全部子表。比如我們按照年分表,多數(shù)請求查詢某一年的。
缺點(diǎn):有可能造成數(shù)據(jù)傾斜,數(shù)據(jù)不均衡。還是拿月份舉例吧。對于電商系統(tǒng),11月份大促期間的訂單量要遠(yuǎn)遠(yuǎn)大于其他月份。
適用場景:比較適合于有大量范圍查詢需求的場景。
2、Hash取模
思想:選定某個分片字段,對子庫(表)數(shù)量進(jìn)行hash取模算出下標(biāo),從而決定落到哪一個分片上。比如用戶 uid,分了16個庫(分庫按照序號排列 0~15)。 uid %16 計(jì)算出對應(yīng)分庫下標(biāo)。
hash取模
優(yōu)點(diǎn):數(shù)據(jù)相對比較均衡,基本上不會造成數(shù)據(jù)傾斜。
缺點(diǎn):最大的缺點(diǎn)就是不容易擴(kuò)縮容。因?yàn)樗莌ash取模的,基數(shù)是子庫數(shù)量,數(shù)量變化之后,需要重新計(jì)算下標(biāo)。這種變化對數(shù)據(jù)的遷移相對比較麻煩的。
適用場景:本分片規(guī)則是比較常用的規(guī)則,也是大家的首選。算法簡單,其缺點(diǎn)可以說是瑕不掩瑜。因?yàn)槲覀冊诜直頃r可以盡量將字庫數(shù)量設(shè)置多一些。或者可以評估未來幾年的數(shù)據(jù)量。
3、一致性hash
思想:是對按照實(shí)際子庫數(shù)量hash取模的改進(jìn)。首先對分片字段hash,隨后對2^32除余,從而確保值落到2^32 – 1區(qū)間,按照順時針找到第一個節(jié)點(diǎn)就是對應(yīng)服務(wù)器的位置,算法是hash(key)%2^32。下面是網(wǎng)上找到的一個比較容易理解的圖:
一致性hash
優(yōu)點(diǎn):它的優(yōu)點(diǎn)就是解決了第二個分片方案存在的問題,由于某臺服務(wù)器節(jié)點(diǎn)只會影響該節(jié)點(diǎn)逆時針方向的節(jié)點(diǎn),即影響數(shù)據(jù)范圍比較小。因此相對于第二種,它在進(jìn)行擴(kuò)容縮容時所產(chǎn)生的影響比較小。
缺點(diǎn):從該結(jié)構(gòu)上可以看出來,整個閉環(huán)很大,如果節(jié)點(diǎn)較少,非常容易造成數(shù)據(jù)分配不均衡。為了解決這個問題,引入了虛擬節(jié)點(diǎn)。虛擬節(jié)點(diǎn)本身不是真實(shí)存在,其只是真實(shí)節(jié)點(diǎn)的復(fù)制品,比如現(xiàn)在有個A,B,那么復(fù)制后就有A,A1,A2,A3;B,B1,B2,B3.他們像正常服務(wù)器節(jié)點(diǎn)那樣分布在整個Hash閉環(huán)中,然后Key還按照正常尋址,尋址哪個,就找到對應(yīng)得真實(shí)服務(wù)器節(jié)點(diǎn)。比如成熟的分庫方案Mycat,就在一致性hash算法中引入了虛擬節(jié)點(diǎn),虛擬節(jié)點(diǎn)的數(shù)量默認(rèn)是真實(shí)節(jié)點(diǎn)的160倍。
適用場景:如果未來可能會經(jīng)歷相對較頻繁的擴(kuò)容縮容可選擇一致性hash算法。
拆分多少分表合適?
主要要考慮以下幾個因素:
- 實(shí)例數(shù)考慮QPS的吞吐能力
- 分庫數(shù)考慮擴(kuò)容拆分
- 分表數(shù)考慮數(shù)據(jù)分布,性能提升
在做分庫分表時,要充分考慮當(dāng)下以及未來的數(shù)據(jù)量,避免或者減少擴(kuò)容。
小米是如何做的?
做為一個精品電商平臺,小米有品自16年成立起,業(yè)務(wù)在不斷地擴(kuò)展,在2020年年初,數(shù)據(jù)庫中的幾大ToC表已經(jīng)很龐大了。比如用戶優(yōu)惠券數(shù)據(jù)表單表就達(dá)到了10億多行,而且米粉節(jié)或者618,一次灌券就可以達(dá)到千萬或者上億級別,即存在較高的讀寫請求和并發(fā)處理。記得有個哥們在線進(jìn)行了一次DDL,它要加個字段,雖然是晚上執(zhí)行的,但還是造成了大量的死鎖情況(基于此,我之前還寫了一篇如何避免在線DDL出現(xiàn)死鎖問題的文章哈哈,感興趣的可以參考:千與千尋-Mysql在線DDL操作)。此時,在單表的情況下,性能已經(jīng)出現(xiàn)了瓶頸。因此,我們首先拿優(yōu)惠券開刀。
一、優(yōu)惠券分庫分表
剛開始我們也是對現(xiàn)有開源的分庫分表技術(shù)做了一下調(diào)研,主要包括應(yīng)用層面的和中間件層面兩種。
技術(shù)選型
下面是我畫的一個腦圖:
分庫分表技術(shù)調(diào)研
在做優(yōu)惠券時,采用了Sharding-JDBC這種代碼侵入式的方式。主要是考慮到其不需要任何的第三方依賴,只需要引入jar包即可,此外其支持所有的第三方數(shù)據(jù)庫鏈接,此外其還內(nèi)置了分布式主鍵id的生成以及支持分布式事務(wù)。嗯,當(dāng)當(dāng)雖然讓這對夫妻搞得烏煙瘴氣,但真的開發(fā)出了一個牛逼的東西,現(xiàn)在也屬于Apache孵化的項(xiàng)目??聪缕錁I(yè)務(wù)架構(gòu)圖:
Sharding-JDBC
在業(yè)務(wù)代碼和底層DB之間,加了一個Sharding-JDBC的代理。對于我們應(yīng)用來講,完全可以無感知使用。除了Sharding-JDBC,還有另外一個產(chǎn)品叫Sharding-Proxy,其主要是為了提供對異構(gòu)語言的支持,可以對任何實(shí)現(xiàn)Mysql協(xié)議的客戶端實(shí)現(xiàn)無差別支持,且是以中間件的形式部署。架構(gòu)如下:
Shardingsphere業(yè)務(wù)架構(gòu)
確定的分庫分表規(guī)則
優(yōu)惠券只是進(jìn)行了分庫,即將單表數(shù)據(jù)分到多個子庫上,每個子庫的表名完全相同,每個數(shù)據(jù)庫使用的用戶名密碼也都完全一致。這樣做的好處就是可以大大減少業(yè)務(wù)層面的代碼改動。
根據(jù)業(yè)務(wù)評估,選擇了64個子庫。分片規(guī)則采用用戶id % 64,分片數(shù)據(jù)庫名是在原dbname上加了序列號后綴。如db_00 ,db_01,,,,,,,db_63。結(jié)構(gòu)如下:
優(yōu)惠券分庫
下面是一個簡單的分片配置實(shí)例,是我自己的電商系統(tǒng)的配置,主要是要按照sharding-jdbc的要求去配置:
datasource: names: hbnnmall0,hbnnmall1 //我這是一個數(shù)據(jù)庫配置一次,好像應(yīng)該還有更簡單的配置方法。要不然如果分了64個庫,那每個都配置也不現(xiàn)實(shí)。 hbnnmall0: type: com.alibaba.druid.pool.DruidDataSource hbnnmall1: type: com.alibaba.druid.pool.DruidDataSource //這里就是分片的規(guī)則了。包括分庫和分表。 sharding: //這里指定了默認(rèn)的數(shù)據(jù)庫,如果不進(jìn)行分庫分表的,就需要使用默認(rèn)的數(shù)據(jù)庫了 default-data-source-name: hbnnmall0 //這配置的是數(shù)據(jù)庫的分片算法,mod 2. default-database-strategy: #行表達(dá)式 inline: //分片的列 sharding-column: gid algorithm-expression: hbnnmall$->{gid % 2} //涉及到的數(shù)據(jù)表 tables: shop_good: //實(shí)際的數(shù)據(jù)節(jié)點(diǎn),hbnnmall0,hbnnmall1就是上面配置的datasource actual-data-nodes: hbnnmall$->{0..1}.shop_good //這是key生成策略,可選擇snowflake. # key-generate-strategy: # column: id # key-generator-name: snowflake
二、訂單表分庫
優(yōu)惠券做完分庫之后半年,我們又開始了訂單相關(guān)的分庫分表。
和上次相同的是,我們依然采用的是分庫,但不同的是,這次沒有采用優(yōu)惠券的分庫方式,而是使用了小米自研的Gaea(目前已開源,GitHub – XiaoMi/Gaea: Gaea is a mysql proxy, it’s developed by xiaomi b2c-dev team.)。
之所以考慮使用中間件的代理方式,是因?yàn)橛唵蜗到y(tǒng)相對復(fù),還涉及到新老訂單系統(tǒng)的同時改動,如果使用sharding-jdbc這種應(yīng)用層代理,研發(fā)需要改動的地方過多,風(fēng)險較大。而反觀gaea,由我們DBA團(tuán)隊(duì)統(tǒng)一維護(hù)和管理,分庫之后,依然走小米自研的DB代理,對業(yè)務(wù)方來說是透明的。只需要提供給業(yè)務(wù)方用戶名密碼即可,業(yè)務(wù)方在代碼層面不用做任何改動。
Gaea
分庫分表如何做數(shù)據(jù)遷移?
數(shù)據(jù)遷移包括存量和增量數(shù)據(jù)。
主要方案:
1、分庫全量數(shù)據(jù)寫入
思想:數(shù)據(jù)向所有分庫全量寫入,隨后刪除。
優(yōu)點(diǎn):遷移簡單,在切換前完成數(shù)據(jù)同步后,停掉交易即可完成切換。
缺點(diǎn):這種方案需要后續(xù)刪除非對應(yīng)分片的數(shù)據(jù),風(fēng)險較大。
2、業(yè)務(wù)雙寫
思想:存量數(shù)據(jù)全量導(dǎo)入分庫,增量數(shù)據(jù)通過業(yè)務(wù)系統(tǒng)雙寫到原庫和分庫中從而達(dá)到一致;
優(yōu)點(diǎn):保證
缺點(diǎn):業(yè)務(wù)系統(tǒng)實(shí)現(xiàn)雙寫改動比較麻煩
3、存量導(dǎo)入,增量同步
我們采用的是第三種方式,具體操作流程如下:
1、將數(shù)據(jù)庫分成64個子庫;
2、找一臺Mysql Slave從庫,停下來;
3、根據(jù)我們的分片規(guī)則 uid % 64 dump出 64個源文件;
4、將文件分別source導(dǎo)入到對應(yīng)的子庫中,該步驟是非常耗時的,存量文件基本有幾十G,幾百G大小,而且中間不能停,這個要DBA操作執(zhí)行的;
5、重啟該臺slave,并將新增的數(shù)據(jù)實(shí)時同步到分庫中;
6、在切換前,停止線上交易,我們當(dāng)時晚上停了幾個小時;
7、上線分庫數(shù)據(jù)源代碼;
8、內(nèi)網(wǎng)驗(yàn)證;
9、開啟線上交易;
以上是進(jìn)行數(shù)據(jù)遷移以及上線前的一些工作。其中第5步要說一下,即增量數(shù)據(jù)的同步。我們采用了一套組合拳,使用的是canal-server+canal-adapter+sharding-proxy完成了增量同步。
這里簡單介紹一下cannal,是阿里開發(fā)的一個產(chǎn)品,用于完成數(shù)據(jù)庫的增量同步,并將結(jié)果發(fā)送到下游,如消息隊(duì)列,ES等。
Canal
下面是我們完成舊庫到分庫的增量同步流程圖:
數(shù)據(jù)增量同步架構(gòu)圖
基本思想就是利用canal通過binlog同步增量數(shù)據(jù),其只負(fù)責(zé)接收,canal-adapter負(fù)責(zé)將數(shù)據(jù)發(fā)送到下游,即Gaea或者Sharding-Proxy,隨后gaea或Sharding-proxy根據(jù)配置的分片鍵分庫規(guī)則將數(shù)據(jù)分配目標(biāo)子庫中。
分庫分表可能出現(xiàn)的問題;
1、join聯(lián)查問題
這是比較普遍的問題,join語句在分庫分表中本身就應(yīng)該避免,通常在ToC的業(yè)務(wù)都不使用join,高性能Mysql也說盡量使用多次的短語句查詢。比如可以使用uid查支付表以及訂單表。就算是join也盡量實(shí)現(xiàn)落到同一個子庫中的數(shù)據(jù)表進(jìn)行join,即同一用戶的相關(guān)數(shù)據(jù)表落到相同的子庫中。這也是目前多數(shù)成熟的中間件都支持的場景,比如Mycat,Gaea,他們稱之為ER分片。Sharding-JDBC雖然支持跨庫跨表查詢,但性能非常差,它會遍歷取所有相關(guān)的子庫表,然后聚合,最后算出笛卡爾積。所以盡量避免這種做法。
如果還有其他更多的所有數(shù)據(jù)的Join聯(lián)查操作,比如要統(tǒng)計(jì)分析,可以再做個合庫(我們就是這么干的,就是為了給數(shù)據(jù)組使用)。
2、跨節(jié)點(diǎn)的分頁、排序、聚合等
其實(shí)這個實(shí)現(xiàn)還是比較簡單的,如果where條件指定了分片字段,那就直接落到同一個分片上執(zhí)行即可;如果沒有分片字段,就在每個分片上執(zhí)行相應(yīng)的函數(shù),返回結(jié)果后,將所有結(jié)果進(jìn)行整合,計(jì)算,獲取最終的結(jié)果。
舉例,現(xiàn)在要進(jìn)行一個分頁查詢:
select oid,pay_time from shop_order order by pay_time limit 0,100;
在每個分片上查出100個,最終比較返回.
當(dāng)然,這種聚合、分片等查詢最好不要在分庫分表的情況下進(jìn)行。數(shù)據(jù)一多,分的庫和表越多,性能越差。
3、全局性ID
實(shí)際應(yīng)用中,對于分布式系統(tǒng),我們希望能有一個ID生成器用來生成全局的、唯一的ID,比如用戶id,訂單號,支付單號等等。
目前,存在的主要有以下幾種。
1、UUID
這個應(yīng)該都很熟悉,通用唯一識別碼,它是由32位的16進(jìn)制數(shù)組成的。
類似:00112233-4455-6677-8899-aabbccddeeff。
其生成規(guī)則有很多的版本,有根據(jù)網(wǎng)卡或時間生成的,有隨機(jī)生成的,有基于時間生成的等等。具體可見: UUID維基百科
但其并不適合做為我們實(shí)際系統(tǒng)中的ID生成器,因?yàn)槠涫请S機(jī)的,無序的,在實(shí)際中我們系統(tǒng)是有序的,此外無序的索引在Mysql中查詢數(shù)據(jù)也是有性能的影響,容易造成頻繁的頁分裂。此外,UUID標(biāo)識位較多,較占空間。
2、數(shù)據(jù)庫主鍵自增
使用Mysql的主鍵當(dāng)作ID生成器,不同的系統(tǒng)可以統(tǒng)一通過DB代理訪問數(shù)據(jù)庫,并獲取生成的主鍵ID。
這種方式是相對比較簡單的一種做法,不需要額外做什么操作,只需要部署好DB。但其本身也存在缺點(diǎn),因?yàn)樵摲绞绞菑?qiáng)依賴DB的,假如DB掛了,那服務(wù)完全不可用。且ID生產(chǎn)性能受限于Mysql性能。此外,如果后續(xù)Mysql分庫分表的話,很難保證ID不出現(xiàn)重復(fù)。
3、Redis,zk
其實(shí)說實(shí)話,基本沒有用的,雖然兩個都能實(shí)現(xiàn)。然而Redis非??赡軐?dǎo)致數(shù)據(jù)丟失,從而出現(xiàn)重復(fù)。zk的話本身集群的性能一般。
4、SnowFlake算法
該算法是Twitter開源的一個算法,生成的ID是由63位數(shù)字組成的long型整數(shù),41位的時間戳+5位的數(shù)據(jù)中心id+5位機(jī)器id+12位遞增序列號。
第1位是符號位,0是正數(shù),1是負(fù)數(shù)。
41位時間戳是毫秒時間戳,不過這個時間戳并不是絕對時間戳,而是一個差值,差值是當(dāng)前時間戳減去起始時間戳,而起始時間戳的值取決于自己的選擇。時間戳最多可以用69年。
中間的10位是機(jī)器ID,由5為數(shù)據(jù)中心ID和5為機(jī)器ID組成,也就是總共可部署1024個機(jī)器。
最后的12bit是循環(huán)位,用來同一毫秒內(nèi)生成不同的id,12位最多可以生成4095個,因此同一毫秒內(nèi),允許生成4095個ID。
該算法最大的缺點(diǎn)就是性能超好,沒有其他的依賴,全局唯一。如果部署1024個機(jī)器的話,理論上每秒可產(chǎn)生40多億個id。
但該算法有一個眾所周知的缺點(diǎn)就是時鐘回?fù)艿膯栴},即因?yàn)闄C(jī)器的原因,時鐘回?fù)芰?,可能?dǎo)致當(dāng)前時間戳還要小于之前的時間戳,這樣就出現(xiàn)ID重復(fù)。
針對這個問題,目前有很多的解決方案。
方案1:每臺都維護(hù)一個上個時間戳 lastTimestamp,每次都會將當(dāng)前時間戳和上一個時間戳進(jìn)行比較。如果差值較?。ㄓ卸嘈⊥耆Q于實(shí)際業(yè)務(wù)需求,可以5ms,可以10ms),可以等待,一直到時間超過lastTimestamp;如果差值較大,無法一直阻塞,可以直接拋出異常,然后將時鐘回滾。或者增加在后面再增加擴(kuò)展位(但我覺得這種方式不妥,因?yàn)閿U(kuò)展位的位數(shù)也是有限的,不是解決問題的優(yōu)先方案);
方案2:百度提了一個UIdGenerator,源碼: UIDGenerator .
其基本思想是不采用SnowFlake算法中傳統(tǒng)的取當(dāng)前時間戳,而是通過AtomicLong類型,采用逐步+1的方式生成時間戳,這里只需要維護(hù)一個初始時間戳即可。這樣不需要再依賴服務(wù)器的時間,不會出現(xiàn)回?fù)軉栴}。
當(dāng)然這樣做的弊端就是,如果你希望通過序列號來獲取時間的話,這樣做是不可取的,因?yàn)橥ㄟ^這個方法獲取的不一定就是那時實(shí)際的時間戳。
目前這個項(xiàng)目,最后一次維護(hù)還是3年前。
5、美團(tuán)Leaf
其主要有兩個方案:Leaf-segment方案和Leaf-snowflake方案。
Leaf-segment方案就是基于數(shù)據(jù)庫自增ID來實(shí)現(xiàn)的,它在數(shù)據(jù)庫的性能上的提升做了一定的改進(jìn),即它一利用代理去一次生成一段的ID(segment),然后存儲下來,等用完了再去請求數(shù)據(jù)庫。這樣做的一個明顯有點(diǎn)就是沒必要每次生成ID都需要請求一次數(shù)據(jù)庫,減少IO,提升了性能。此外,在此基礎(chǔ)上,美團(tuán)又提出了buffer的優(yōu)化,即當(dāng)已獲取的一段ID號用了一定數(shù)量后,且未完全耗盡時,會異步再去請求數(shù)據(jù)庫并存儲下來。這對客戶端來講是無感知的,因此性能又得到了極大的提升。
Leaf-snowflake方案是一種類似snowflake的一種實(shí)現(xiàn)方式。它主要是采用了ZK去管理配置機(jī)器節(jié)點(diǎn)。
但說實(shí)話,我不知道這個方案有什么實(shí)際的意義,我是覺得用了ZK反倒影響性能。
關(guān)于Leaf更詳細(xì)的介紹可見: Leaf——美團(tuán)點(diǎn)評分布式ID生成系統(tǒng)
下面是用JAVA實(shí)現(xiàn)的SnowFlake算法,改進(jìn)之處就是對于時鐘回?fù)艿奶幚?,參考了美團(tuán)。
public class OrderIdGeneratorServiceImpl implements OrderIdGeneratorService { private static final Logger logger = LoggerFactory.getLogger(OrderIdGeneratorServiceImpl.class); /** * work開始id */ public static final AtomicLong START_WORK_ID = new AtomicLong(0); public static final Long START_TIMESTAMP = 1580580122000L; /** * ID 首位 */ private static final Short ID_HEADER = 1; private static final Long WorkIDBits = 8L; private static final Long MAX_WORK_ID = ~(-1L << WorkIDBits); //最大的work id private static final Long CycleIdBits = 12L; private static final Long MAX_CYCLE_ID = ~(-1L << CycleIdBits); private static final Long WorkIdShifts = CycleIdBits; private static final Long TimeStampShifts = CycleIdBits + WorkIDBits; private Long workerId; private Long sequenceId = 0L; private Long lastTimeStamp = -1L; @Override public synchronized Long getOrderId() throws OrderIdGenerationException { Long timestamp = this.getCurTimeStamp(); if(timestamp < this.lastTimeStamp) { Long delta = this.lastTimeStamp – timestamp; Long MAX_DElTA = 5L; if(delta <= MAX_DElTA){ try{ //設(shè)定一個休眠時間 wait(delta << 1); timestamp = this.getCurTimeStamp(); if(timestamp < lastTimeStamp){ throw new OrderIdGenerationException("時鐘已經(jīng)回?fù)?,請檢查機(jī)器時鐘"); } }catch (Exception e){ throw new OrderIdGenerationException(e.getMessage()); } }else { throw new OrderIdGenerationException("時鐘已經(jīng)回?fù)?,請檢查機(jī)器時鐘"); } } if(timestamp.equals(this.lastTimeStamp)){ sequenceId = (sequenceId + 1 ) & MAX_CYCLE_ID; if(sequenceId == 0){ timestamp = tillNextMill(lastTimeStamp); } }else { //如果不是在同一毫秒,直接返回0 sequenceId = 0L; } lastTimeStamp = timestamp; workerId = this.getCurWorkId(); logger.info("time:{},work:{},sequence:{}",timestamp,workerId,sequenceId); return (timestamp – START_TIMESTAMP) << TimeStampShifts | workerId << WorkIdShifts | sequenceId; } private Long getCurTimeStamp(){ return System.currentTimeMillis(); } private Long tillNextMill(Long lastTimeStamp){ Long timestamp = this.getCurTimeStamp(); while(timestamp MAX_WORK_ID){ throw new OrderIdGenerationException(String.format("work ID 超過最大值:%d",MAX_WORK_ID)); } if (this.workerId < 0L ){ throw new OrderIdGenerationException("work ID 不可為負(fù)數(shù)"); } //TODO 通過zookeeper獲取workid return this.workerId; }}
上述的workId應(yīng)該采用ZK管理比較合適,我上面就是隨意瞎寫的,不規(guī)范,因?yàn)閣orkerid是和機(jī)器有關(guān)的,每臺機(jī)器是一樣的。
4、分布式事務(wù)問題
分庫分表之后會涉及到多個實(shí)際不同的節(jié)點(diǎn)的事務(wù)提交,因此就要考慮分布式事務(wù)了。分布式中間件比如Mycat,以及jar包Sharding-JDBC都是支持分布式事務(wù)的。而分布式事務(wù)的底層實(shí)現(xiàn)邏輯主要包括XA事務(wù),TCC,AT,消息表等幾種方式。
XA事務(wù)
XA事務(wù)的基礎(chǔ)是兩階段提交以及其中的角色:資源管理器和事務(wù)協(xié)調(diào)器。
流程:
1、第一階段。資源管理先執(zhí)行prepare操作,但不實(shí)際提交;
2、事務(wù)協(xié)調(diào)器發(fā)現(xiàn)所有資源管理器都ready了,就會告知資源管理器執(zhí)行commit操作,反之就是執(zhí)行rollback操作。
XA事務(wù)的最大優(yōu)點(diǎn)是實(shí)現(xiàn)起來比較簡單。但最大的問題就是性能差,事務(wù)管理器要確保所有資源的一致性,容易造成同步阻塞。在并發(fā)場景下使用XA事務(wù)的話,基本上這個系統(tǒng)也就沒法用了。因此XA也很少用于實(shí)際的業(yè)務(wù)中。
TCC事務(wù)
Try-Commit-Cancel,該機(jī)制是相當(dāng)于XA的一種補(bǔ)償性機(jī)制。即事務(wù)可以先執(zhí)行commit,如果后面的rollback了。對于前面的要執(zhí)行逆向操作。再修改回原有的狀態(tài)。
TCC的目的就是減少底層數(shù)據(jù)源的依賴,由業(yè)務(wù)自信決定事務(wù)的粒度。
Try,執(zhí)行prepare,預(yù)留資源;
Confirm,不做任何檢查,真正地進(jìn)行資源操作;
Cancel ,執(zhí)行回滾操作。
相比于XA,TCC的并發(fā)性更好,因?yàn)橛蓸I(yè)務(wù)自己決定粒度,但缺點(diǎn)就是可能業(yè)務(wù)自己要寫很多的代碼,實(shí)現(xiàn)起來相對復(fù)雜,就算是借助分布式框架,同樣需要自己寫prepare,confirm,cancel等流程。
說一個我們有品在做分布式事務(wù)的一個實(shí)現(xiàn)。即訂單在下單的時候扣除優(yōu)惠的活動庫存:
1、prepare階段。首先,當(dāng)開始執(zhí)行優(yōu)惠結(jié)算的時侯,訂單調(diào)用優(yōu)惠的服務(wù),優(yōu)惠服務(wù)首先會向資源表中寫入訂單的唯一資源并進(jìn)行鎖定,唯一鍵是資源id+資源類型。該階段會將資源鎖定(根據(jù)status字段實(shí)現(xiàn));
2、commit。接下來,如果訂單正常執(zhí)行下單或者結(jié)算操作,會調(diào)用優(yōu)惠的commit接口,優(yōu)惠服務(wù)會將屬于該訂單的資源表commit,其實(shí)也是修改status字段;
3、cancel階段。如果訂單在執(zhí)行過程中出現(xiàn)問題,那么必須調(diào)用優(yōu)惠的服務(wù)進(jìn)行回滾,優(yōu)惠要將資源表回滾,也是改狀態(tài),然后根據(jù)具體優(yōu)惠類型進(jìn)行具體回滾操作,比如回滾活動庫存等。
上面是正常的流程,還是要注意其他的問題,比如冪等,空回滾和空懸掛等問題。
冪等:即如果重復(fù)調(diào)用commit或者cancel都能保證結(jié)果的一致性;
空回滾:即沒有執(zhí)行try,直接執(zhí)行cancel,通常是發(fā)生在try因?yàn)樽枞L時間沒有執(zhí)行,就調(diào)用了cancel。此時,cacel應(yīng)該什么也不做,這就是空回滾。想實(shí)現(xiàn)這個還是比較容易的,執(zhí)行cancel時要判斷try是否執(zhí)行,主要根據(jù)相應(yīng)的id判斷即可,比如訂單庫存,可以使用訂單號,商品庫存可以使用sku,活動庫存可以使用活動id和sku等來判斷是否存在。
懸掛:如果此時已經(jīng)執(zhí)行了上步的空回滾,此時try又開始執(zhí)行了,那么try應(yīng)該判斷當(dāng)前的資源是否已經(jīng)回滾了,如果回滾了,就不應(yīng)該執(zhí)行try操作。
對于冪等性,空回滾,懸掛,實(shí)現(xiàn)的前提都是通過建立事務(wù)資源id,狀態(tài)等來實(shí)現(xiàn)。
TCC的實(shí)現(xiàn)可以使用成熟的分布式框架如Seata,也可以自實(shí)現(xiàn),我們部門優(yōu)惠服務(wù)就是自實(shí)現(xiàn)的,主要使用的是ebay提出的本地消息表的概念。將資源通過數(shù)據(jù)表以及Redis來實(shí)現(xiàn)資源的鎖定,提交以及回滾等。現(xiàn)在的消息隊(duì)列Rocketmq也支持事務(wù)消息,其保證的是最終一致性。
RocketMQ實(shí)現(xiàn)分布式事務(wù)
Saga
它的思想是對于每一個資源都配置一個補(bǔ)償節(jié)點(diǎn),在提交事務(wù)時,依次執(zhí)行本地事務(wù)(可以是異步的),如果其中有失敗的,就執(zhí)行已經(jīng)執(zhí)行過的事務(wù)的補(bǔ)償操作??梢钥吹剑鼪]有prepare階段,每個階段直接就提交事務(wù),但如果有失敗的,可以自動執(zhí)行反向的回滾操作。因此,這種模式的并發(fā)性更好一些。
我推薦使用Seata實(shí)現(xiàn)分布式事務(wù),對于Sharding-JDBC這種分庫分表代理來說,已經(jīng)內(nèi)置了Seata。
分庫分表的下一代解決方案;
分庫分表是目前用于提升DB性能的比較通用的解決方案,但分庫分表后,隨著數(shù)據(jù)量不斷增大,仍然有可能遇到瓶頸。如果在繼續(xù)擴(kuò)大分庫分表的數(shù)量,會比較麻煩,如果之前是按照hash取模進(jìn)行分片的,還要重新對數(shù)據(jù)進(jìn)行分片。所以,分庫分表是較好的提升性能的方案,但卻不是最終的解決方案。
下一代解決方案應(yīng)該是原生分布式數(shù)據(jù)庫,開發(fā)者和DBA都不需要自己分庫分表,所有的工作都是原生數(shù)據(jù)庫來完成,使用者仍然可以像單庫單表一樣使用。
其實(shí)Mysql本身也提供了分布式解決方案,比如Mysql NDB Cluster,但cluster是基于內(nèi)存的(最初的版本要所有數(shù)據(jù)都在內(nèi)存,現(xiàn)在只需索引在內(nèi)存中即可),比較耗費(fèi)機(jī)器,且部署比較復(fù)雜,每臺機(jī)器都需要啟動多個進(jìn)程,通常都不會選用。但在電信業(yè)務(wù)中用得還是比較多的。
目前國內(nèi)也有很多比較好的分布式數(shù)據(jù)庫產(chǎn)品,如TiDB,還有俄羅斯的ClickHouse,阿里的云分布式數(shù)據(jù)庫,他們共同點(diǎn)都是分布式是存儲數(shù)據(jù),可以動態(tài)擴(kuò)縮容,且采用了列式存儲。不過ClickHouse主要還是用于OLAP場景,即更適合于處理讀請求。而TiDB在處理OLTP,OLAP方面都提供了較好的方案,而且TiDB和Mysql可以實(shí)現(xiàn)無縫切換,其提供了從Mysql向TiDB遷移的方案。目前我們公司已經(jīng)逐步用起來了,現(xiàn)在的初級用法是使用TiDB做從庫,不過后續(xù)會考慮逐步完全替代Mysql。
下面是官網(wǎng)給的tiDB介紹:
TiDB 是 PingCAP 公司自主設(shè)計(jì)、研發(fā)的開源分布式關(guān)系型數(shù)據(jù)庫,是一款同時支持在線事務(wù)處理與在線分析處理 (Hybrid Transactional and Analytical Processing, HTAP) 的融合型分布式數(shù)據(jù)庫產(chǎn)品,具備水平擴(kuò)容或者縮容、金融級高可用、實(shí)時 HTAP、云原生的分布式數(shù)據(jù)庫、兼容 MySQL 5.7 協(xié)議和 MySQL 生態(tài)等重要特性。目標(biāo)是為用戶提供一站式 OLTP (Online Transactional Processing)、OLAP (Online Analytical Processing)、HTAP 解決方案。TiDB 適合高可用、強(qiáng)一致要求較高、數(shù)據(jù)規(guī)模較大等各種應(yīng)用場景。
我覺得最重要的就是一個純天然的分布式關(guān)系型數(shù)據(jù)庫,其擴(kuò)展性非常好,可以實(shí)現(xiàn)彈性的擴(kuò)縮容??吹竭@兒,是不是覺得分庫分表太死板了,也快過時了。未來Mysql會逐步被分布式數(shù)據(jù)庫取代。
關(guān)系型數(shù)據(jù)庫的最新發(fā)展路線:
單庫 -> 主從分離 -> 分庫分表 -> 分布式數(shù)據(jù)庫-> ……..
參考資料:
千與千尋-Mysql分庫分表之理論學(xué)習(xí)以及sharding-jdbc使用
MySQL 分庫分表方案,總結(jié)的非常好! – 掘金
數(shù)據(jù)庫分庫分表如何避免“過度設(shè)計(jì)”和“過早優(yōu)化” – SQL優(yōu)化 – dbaplus社群:圍繞Data、Blockchain、AiOps的企業(yè)級專業(yè)社群。技術(shù)大咖、原創(chuàng)干貨,每天精品原創(chuàng)文章推送,每周線上技術(shù)分享,每月線下技術(shù)沙龍。
水平分庫分表的關(guān)鍵步驟以及可能遇到的問題_語言 & 開發(fā)_丁浪_InfoQ精選文章
接入端 :: ShardingSphere
XA 事務(wù)水很深,小伙子我怕你把握不??! – SegmentFault 思否
Seata 是什么
如何理解TCC分布式事務(wù)? – 知乎
分布式事務(wù)TCC模式的空回滾和業(yè)務(wù)懸掛問題 – benym
分布式事務(wù) Seata 及其三種模式詳解