一、背景
分布式系統(tǒng)中我們會(huì)對(duì)一些數(shù)據(jù)量大的業(yè)務(wù)進(jìn)行分拆,如:用戶表,訂單表。因?yàn)閿?shù)據(jù)量巨大一張表無(wú)法承接,就會(huì)對(duì)其進(jìn)行分庫(kù)分表。
但一旦涉及到分庫(kù)分表,就會(huì)引申出分布式系統(tǒng)中唯一主鍵ID的生成問題。
1.1 唯一ID的特性
* 整個(gè)系統(tǒng)ID唯一;
* ID是數(shù)字類型,而且是趨勢(shì)遞增;
* ID簡(jiǎn)短,查詢效率快。
1.2 遞增與趨勢(shì)遞增
遞增 趨勢(shì)遞增
第一次生成的ID為12,下一次生成的ID是13,再下一次生成的ID是14。
什么是?如:在一段時(shí)間內(nèi),生成的ID是遞增的趨勢(shì)。如:再一段時(shí)間內(nèi)生成的ID在【0,1000】之間,過段時(shí)間生成的ID在【1000,2000】之間。但在【0-1000】區(qū)間內(nèi)的時(shí)候,ID生成有可能第一次是12,第二次是10,第三次是14。
二、方案
2.1 UUID
UUID全稱:Universally Unique Identifier。標(biāo)準(zhǔn)型式包含32個(gè)16進(jìn)制數(shù)字,以連字號(hào)分為五段,形式為8-4-4-4-12
的36個(gè)字符,示例:9628f6e9-70ca-45aa-9f7c-77afe0d26e05。
* 優(yōu)點(diǎn):
* 代碼實(shí)現(xiàn)簡(jiǎn)單;
* 本機(jī)生成,沒有性能問題;
* 因?yàn)槭侨蛭ㄒ坏腎D,所以遷移數(shù)據(jù)容易。
* 缺點(diǎn):
* 每次生成的ID是無(wú)序的,無(wú)法保證趨勢(shì)遞增;
* UUID的字符串存儲(chǔ),查詢效率慢;
* 存儲(chǔ)空間大;
* ID本身無(wú)業(yè)務(wù)含義,不可讀。
* 應(yīng)用場(chǎng)景:
* 類似生成token令牌的場(chǎng)景;
* 不適用一些要求有趨勢(shì)遞增的ID場(chǎng)景,不適合作為高性能需求的場(chǎng)景下的數(shù)據(jù)庫(kù)主鍵。
也有在線生成UUID的網(wǎng)站,如果你的項(xiàng)目上用到了UUID,可以用來生成臨時(shí)的測(cè)試數(shù)據(jù)。https://www.uuidgenerator.net/
<https://www.uuidgenerator.net/>
2.2 MySQL主鍵自增
利用了MySQL的主鍵自增auto_increment,默認(rèn)每次ID加1。
優(yōu)點(diǎn):
* 數(shù)字化,ID遞增;
* 查詢效率高;
* 具有一定的業(yè)務(wù)可讀。
* 缺點(diǎn):
* 存在單點(diǎn)問題,如果MySQL掛了,就沒法生成ID了;
* 數(shù)據(jù)庫(kù)壓力大,高并發(fā)抗不住。
2.3 MySQL多實(shí)例主鍵自增
這個(gè)方案就是解決MySQL的單點(diǎn)問題,在auto_increment基本上面,設(shè)置step步長(zhǎng)
如上,每臺(tái)的初始值分別為1,2,3...N,步長(zhǎng)為N(這個(gè)案例步長(zhǎng)為4)
* 優(yōu)點(diǎn):解決了單點(diǎn)問題;
* 缺點(diǎn):一旦把步長(zhǎng)定好后,就無(wú)法擴(kuò)容;而且單個(gè)數(shù)據(jù)庫(kù)的壓力大,數(shù)據(jù)庫(kù)自身性能無(wú)法滿足高并發(fā)。
* 應(yīng)用場(chǎng)景:數(shù)據(jù)不需要擴(kuò)容的場(chǎng)景。
2.4 基于Redis實(shí)現(xiàn)
*
單機(jī):Redis的incr函數(shù)在單機(jī)上是原子操作,可以保證唯一且遞增。
*
集群:?jiǎn)螜C(jī)Redis可能無(wú)法支撐高并發(fā)。集群情況下,可以使用步長(zhǎng)的方式。比如有5個(gè)Redis節(jié)點(diǎn)組成的集群,它們生成的ID分別為:
A: 1,6,11,16,21 B: 2,7,12,17,22 C: 3,8,13,18,23 D: 4,9,14,19,24 E:
5,10,15,20,25
* 優(yōu)點(diǎn):有序遞增,可讀性強(qiáng)。
* 缺點(diǎn):占用帶寬,每次要向Redis進(jìn)行請(qǐng)求。
三、優(yōu)化方案
3.1、改造數(shù)據(jù)庫(kù)主鍵自增
數(shù)據(jù)庫(kù)的自增主鍵的特性,可以實(shí)現(xiàn)分布式ID,適合做userId,正好符合如何永不遷移數(shù)據(jù)和避免熱點(diǎn)? 但這個(gè)方案有嚴(yán)重的問題:
* 一旦步長(zhǎng)定下來,不容易擴(kuò)容;
* 數(shù)據(jù)庫(kù)壓力山大。
* 為什么壓力大?
因?yàn)槲覀兠看潍@取ID的時(shí)候,都要去數(shù)據(jù)庫(kù)請(qǐng)求一次。那我們可以不可以不要每次去???
可以請(qǐng)求數(shù)據(jù)庫(kù)得到ID的時(shí)候,可設(shè)計(jì)成獲得的ID是一個(gè)ID區(qū)間段。
* 上圖ID規(guī)則表含義:
* id表示為主鍵,無(wú)業(yè)務(wù)含義;
* biz_tag為了表示業(yè)務(wù),因?yàn)檎w系統(tǒng)中會(huì)有很多業(yè)務(wù)需要生成ID,這樣可以共用一張表維護(hù);
* max_id表示現(xiàn)在整體系統(tǒng)中已經(jīng)分配的最大ID;
* desc描述;
* update_time表示每次取的ID時(shí)間;
* 整體流程:
* 【用戶服務(wù)】在注冊(cè)一個(gè)用戶時(shí),需要一個(gè)用戶ID;會(huì)請(qǐng)求【生成ID服務(wù)(是獨(dú)立的應(yīng)用)】的接口;
* 【生成ID服務(wù)】會(huì)去查詢數(shù)據(jù)庫(kù),找到user_tag的id,現(xiàn)在的max_id為0,step=1000;
* 【生成ID服務(wù)】把max_id和step返回給【用戶服務(wù)】;并且把max_id更新為max_id = max_id + step,即更新為1000;
* 【用戶服務(wù)】獲得max_id=0,step=1000;
* 這個(gè)用戶服務(wù)可以用ID=【max_id + 1,max_id+step】區(qū)間的ID,即為【1,1000】;
* 【用戶服務(wù)】會(huì)把這個(gè)區(qū)間保存到j(luò)vm中;
* 【用戶服務(wù)】需要用到ID的時(shí)候,在區(qū)間【1,1000】中依次獲取ID,可采用AtomicLong中的getAndIncrement方法;
*
如果把區(qū)間的值用完了,再去請(qǐng)求【生產(chǎn)ID服務(wù)】接口,獲取到max_id為1000,即可以用【max_id + 1,max_id+step】區(qū)間的ID,即為
【1001,2000】。
* 該方案就非常完美的解決了數(shù)據(jù)庫(kù)自增的問題,而且可以自行定義max_id的起點(diǎn),和step步長(zhǎng),非常方便擴(kuò)容;
*
也解決了數(shù)據(jù)庫(kù)壓力的問題,因?yàn)樵谝欢螀^(qū)間內(nèi),是在jvm內(nèi)存中獲取的,而不需要每次請(qǐng)求數(shù)據(jù)庫(kù)。即使數(shù)據(jù)庫(kù)宕機(jī)了,系統(tǒng)也不受影響,ID還能維持一段時(shí)間。
3.2 競(jìng)爭(zhēng)問題
以上方案中,如果是多個(gè)用戶服務(wù),同時(shí)獲取ID,同時(shí)去請(qǐng)求【ID服務(wù)】,在獲取max_id的時(shí)候會(huì)存在并發(fā)問題。如:
用戶服務(wù)A,取到的max_id=1000 ;用戶服務(wù)B取到的也是max_id=1000,那就出現(xiàn)了問題,ID重復(fù)了。
解決方案是:加分布式鎖,保證同一時(shí)刻只有一個(gè)用戶服務(wù)獲取max_id。
3.3 突發(fā)阻塞問題
因?yàn)楦?jìng)爭(zhēng)問題,所有只有一個(gè)用戶服務(wù)去操作數(shù)據(jù)庫(kù),其他二個(gè)會(huì)被阻塞。出現(xiàn)的現(xiàn)象就是一會(huì)兒突然系統(tǒng)耗時(shí)變長(zhǎng),怎么去解決?
* 雙buffer方案
流程如下:
* 當(dāng)前獲取ID在buffer1中,每次獲取ID在buffer1中獲取;
* 當(dāng)buffer1中的ID已經(jīng)使用到了100,也就是達(dá)到區(qū)間的10%;
* 達(dá)到了10%,先判斷buffer2中有沒有去獲取過,如果沒有就立即發(fā)起請(qǐng)求獲取ID線程,此線程把獲取到的ID,設(shè)置到buffer2中;
* 如果buffer1用完了,會(huì)自動(dòng)切換到buffer2;
* buffer2用到10%了,也會(huì)啟動(dòng)線程再次獲取,設(shè)置到buffer1中;
* 依次往返。
3.4 總結(jié)
* 雙buffer的方案就達(dá)到了業(yè)務(wù)場(chǎng)景用的ID,都是在jvm內(nèi)存中獲得的,從此不需要到數(shù)據(jù)庫(kù)中獲取了,數(shù)據(jù)庫(kù)宕機(jī)時(shí)長(zhǎng)長(zhǎng)點(diǎn)兒也沒太大影響了。
* 因?yàn)闀?huì)有一個(gè)線程,會(huì)觀察什么時(shí)候去自動(dòng)獲取。兩個(gè)buffer之間自行切換使用,就解決了突發(fā)阻塞的問題。
四、其他方式
還有一些其他的ID生成方案,比如:
* 滴滴:時(shí)間+起點(diǎn)編號(hào)+車牌號(hào);
* 淘寶訂單:時(shí)間戳+用戶ID
* 其他電商:時(shí)間戳+下單渠道+用戶ID,有的會(huì)加上訂單第一個(gè)商品的ID;
* MongoDB 的ID:通過時(shí)間+機(jī)器碼+pid+inc共12個(gè)字節(jié),4+3+2+3的方式最終標(biāo)識(shí)成一個(gè)24長(zhǎng)度的十六進(jìn)制字符。
熱門工具 換一換