UUID(Universally Unique Identifier,通用唯一標(biāo)識(shí)碼)不依賴于任何第三方系統(tǒng),所以在性能和可用性上都比較好,我一般會(huì)使用它生成 Request ID 來標(biāo)記單次請(qǐng)求,但是如果用它來作為數(shù)據(jù)庫(kù)主鍵,它會(huì)存在以下幾點(diǎn)問題。
首先,生成的 ID 做好具有單調(diào)遞增性,也就是有序的,而 UUID 不具備這個(gè)特點(diǎn)。為什么 ID 要是有序的呢?因?yàn)樵谙到y(tǒng)設(shè)計(jì)時(shí),ID 有可能成為排序的字段。我給你舉個(gè)例子。比如,你要實(shí)現(xiàn)一套評(píng)論的系統(tǒng)時(shí),你一般會(huì)設(shè)計(jì)兩個(gè)表,一張?jiān)u論表,存儲(chǔ)評(píng)論的詳細(xì)信息,其中有 ID 字段,有評(píng)論的內(nèi)容,還有評(píng)論人 ID,被評(píng)論內(nèi)容的 ID 等等,以 ID 字段作為分區(qū)鍵;另一個(gè)是評(píng)論列表,存儲(chǔ)著內(nèi)容 ID 和評(píng)論 ID 的對(duì)應(yīng)關(guān)系,以內(nèi)容 ID 為分區(qū)鍵。我們?cè)讷@取內(nèi)容的評(píng)論列表時(shí),需要按照時(shí)間序倒序排列,因?yàn)?ID 是時(shí)間上有序的,所以我們就可以按照評(píng)論 ID 的倒序排列。而如果評(píng)論 ID 不是在時(shí)間上有序的話,我們就需要在評(píng)論列表中再存儲(chǔ)一個(gè)多余的創(chuàng)建時(shí)間的列用作排序,假設(shè)內(nèi)容 ID、評(píng)論 ID 和時(shí)間都是使用 8 字節(jié)存儲(chǔ),我們就要多出 50% 的存儲(chǔ)空間存儲(chǔ)時(shí)間字段,造成了存儲(chǔ)空間上的浪費(fèi)。因?yàn)榱斜頂?shù)據(jù)在存儲(chǔ)和緩存的時(shí)候只有兩列,增加一列,空間會(huì)增加50%
另一個(gè)原因在于 ID 有序也會(huì)提升數(shù)據(jù)的寫入性能。我們知道 MySQL InnoDB 存儲(chǔ)引擎使用 B+ 樹存儲(chǔ)索引數(shù)據(jù),而主鍵也是一種索引。索引數(shù)據(jù)在 B+ 樹中是有序排列的
UUID 不能作為 ID 的另一個(gè)原因是它不具備業(yè)務(wù)含義,其實(shí)現(xiàn)實(shí)世界中使用的 ID 中都包含有一些有意義的數(shù)據(jù),這些數(shù)據(jù)會(huì)出現(xiàn)在 ID 的固定的位置上。比如說我們使用的身份證的前六位是地區(qū)編號(hào);7~14 位是身份證持有人的生日;不同城市電話號(hào)碼的區(qū)號(hào)是不同的;你從手機(jī)號(hào)碼的的前三位就可以看出這個(gè)手機(jī)號(hào)隸屬于哪一個(gè)運(yùn)營(yíng)商。而如果生成的 ID 可以被反解,那么從反解出來的信息中我們可以對(duì) ID 來做驗(yàn)證,我們可以從中知道這個(gè) ID 的生成時(shí)間,從哪個(gè)機(jī)房的發(fā)號(hào)器中生成的,為哪個(gè)業(yè)務(wù)服務(wù)的,對(duì)于問題的排查有一定的幫助。
最后,UUID 是由 32 個(gè) 16 進(jìn)制數(shù)字組成的字符串,如果作為數(shù)據(jù)庫(kù)主鍵使用比較耗費(fèi)空間。
Snowflake 的核心思想是將 64bit 的二進(jìn)制數(shù)字分成若干部分,每一部分都存儲(chǔ)有特定含義的數(shù)據(jù),比如說時(shí)間戳、機(jī)器 ID、序列號(hào)等等,最終生成全局唯一的有序 ID。
也就是從“時(shí)間” + “空間” + “并發(fā)冗余”這幾個(gè)要素區(qū)分開

如果你的系統(tǒng)部署在多個(gè)機(jī)房,那么 10 位的機(jī)器 ID 可以繼續(xù)劃分為 2~3 位的 IDC 標(biāo)示(可以支撐 4 個(gè)或者 8 個(gè) IDC 機(jī)房)和 7~8 位的機(jī)器 ID(支持 128-256 臺(tái)機(jī)器);12 位的序列號(hào)代表著每個(gè)節(jié)點(diǎn)每毫秒最多可以生成 4096 的 ID。
不同公司也會(huì)依據(jù)自身業(yè)務(wù)的特點(diǎn)對(duì) Snowflake 算法做一些改造,比如說減少序列號(hào)的位數(shù)增加機(jī)器 ID 的位數(shù)以支持單 IDC 更多的機(jī)器,也可以在其中加入業(yè)務(wù) ID 字段來區(qū)分不同的業(yè)務(wù)。比方說我現(xiàn)在使用的發(fā)號(hào)器的組成規(guī)則就是:1 位兼容位恒為 0 + 41 位時(shí)間信息 + 6 位 IDC 信息(支持 64 個(gè) IDC)+ 6 位業(yè)務(wù)信息(支持 64 個(gè)業(yè)務(wù))+ 10 位自增信息(每毫秒支持 1024 個(gè)號(hào))
我選擇這個(gè)組成規(guī)則,主要是因?yàn)槲以趩螜C(jī)房只部署一個(gè)發(fā)號(hào)器的節(jié)點(diǎn),并且使用 KeepAlive 保證可用性。業(yè)務(wù)信息指的是項(xiàng)目中哪個(gè)業(yè)務(wù)模塊使用,比如用戶模塊生成的 ID,內(nèi)容模塊生成的 ID,把它加入進(jìn)來,一是希望不同業(yè)務(wù)發(fā)出來的 ID 可以不同,二是因?yàn)樵诔霈F(xiàn)問題時(shí)可以反解 ID,知道是哪一個(gè)業(yè)務(wù)發(fā)出來的 ID。
一般來說我們會(huì)有兩種算法的實(shí)現(xiàn)方式:一種是嵌入到業(yè)務(wù)代碼里,也就是分布在業(yè)務(wù)服務(wù)器中。這種方案的好處是業(yè)務(wù)代碼在使用的時(shí)候不需要跨網(wǎng)絡(luò)調(diào)用,性能上會(huì)好一些,但是就需要更多的機(jī)器 ID 位數(shù)來支持更多的業(yè)務(wù)服務(wù)器。另外,由于業(yè)務(wù)服務(wù)器的數(shù)量很多,我們很難保證機(jī)器 ID 的唯一性,所以就需要引入 ZooKeeper 等分布式一致性組件來保證每次機(jī)器重啟時(shí)都能獲得唯一的機(jī)器 ID。
另外一個(gè)部署方式是作為獨(dú)立的服務(wù)部署,這也就是我們常說的發(fā)號(hào)器服務(wù)。業(yè)務(wù)在使用發(fā)號(hào)器的時(shí)候就需要多一次的網(wǎng)絡(luò)調(diào)用,但是內(nèi)網(wǎng)的調(diào)用對(duì)于性能的損耗有限,卻可以減少機(jī)器 ID 的位數(shù),如果發(fā)號(hào)器以主備方式部署,同時(shí)運(yùn)行的只有一個(gè)發(fā)號(hào)器,那么機(jī)器 ID 可以省略,這樣可以留更多的位數(shù)給最后的自增信息位。即使需要機(jī)器 ID,因?yàn)榘l(fā)號(hào)器部署實(shí)例數(shù)有限,那么就可以把機(jī)器 ID 寫在發(fā)號(hào)器的配置文件里,這樣即可以保證機(jī)器 ID 唯一性,也無需引入第三方組件了。微博和美圖都是使用獨(dú)立服務(wù)的方式來部署發(fā)號(hào)器的,性能上單實(shí)例單 CPU 可以達(dá)到兩萬每秒。
Snowflake 算法設(shè)計(jì)的非常簡(jiǎn)單且巧妙,性能上也足夠高效,同時(shí)也能夠生成具有全局唯一性、單調(diào)遞增性和有業(yè)務(wù)含義的 ID,但是它也有一些缺點(diǎn),其中最大的缺點(diǎn)就是它依賴于系統(tǒng)的時(shí)間戳,一旦系統(tǒng)時(shí)間不準(zhǔn),就有可能生成重復(fù)的 ID。所以如果我們發(fā)現(xiàn)系統(tǒng)時(shí)鐘不準(zhǔn),就可以讓發(fā)號(hào)器暫時(shí)拒絕發(fā)號(hào),直到時(shí)鐘準(zhǔn)確為止。通過記錄上一次發(fā)號(hào)的時(shí)間戳,如果這次的時(shí)間戳比上次的小,就認(rèn)為是回?fù)?,拒絕發(fā)號(hào)。另外,如果請(qǐng)求發(fā)號(hào)器的 QPS 不高,比如說發(fā)號(hào)器每毫秒只發(fā)一個(gè) ID,就會(huì)造成生成 ID 的末位永遠(yuǎn)是 1,那么在分庫(kù)分表時(shí)如果使用 ID 作為分區(qū)鍵就會(huì)造成庫(kù)表分配的不均勻。這一點(diǎn),也是我在實(shí)際項(xiàng)目中踩過的坑,
解決辦法主要有兩個(gè):
1. 時(shí)間戳不記錄毫秒而是記錄秒,這樣在一個(gè)時(shí)間區(qū)間里可以多發(fā)出幾個(gè)號(hào),避免出現(xiàn)分庫(kù)分表時(shí)數(shù)據(jù)分配不均。
2. 生成的序列號(hào)的起始號(hào)可以做一下隨機(jī),這一秒是 21,下一秒是 30,這樣就會(huì)盡量的均衡了。
Snowflake 的算法并不復(fù)雜,你在使用的時(shí)候可以不考慮獨(dú)立部署的問題,先想清楚按照自身的業(yè)務(wù)場(chǎng)景,需要如何設(shè)計(jì) Snowflake 算法中的每一部分占的二進(jìn)制位數(shù)。比如你的業(yè)務(wù)會(huì)部署幾個(gè) IDC,應(yīng)用服務(wù)器要部署多少臺(tái)機(jī)器,每秒鐘發(fā)號(hào)個(gè)數(shù)的要求是多少等等,然后在業(yè)務(wù)代碼中實(shí)現(xiàn)一個(gè)簡(jiǎn)單的版本先使用,等到應(yīng)用服務(wù)器數(shù)量達(dá)到一定規(guī)模,再考慮獨(dú)立部署的問題就可以了。這樣可以避免多維護(hù)一套發(fā)號(hào)器服務(wù),減少了運(yùn)維上的復(fù)雜度。
每一個(gè)毫秒將下41時(shí)間戳加1,10位的機(jī)器不變,12的序列號(hào)先隨機(jī)生成一個(gè)數(shù)字,然后再在這個(gè)基礎(chǔ)上生成這一毫秒所需要的全局id的數(shù)量。
1.數(shù)據(jù)庫(kù)自增的全局唯一鍵。可以在設(shè)計(jì)出按一定步進(jìn)生成id。比如分庫(kù)為3臺(tái),每臺(tái)的主鍵id初始值分別為0、1、2自增步進(jìn)為3。這樣也可以唯一。不過數(shù)據(jù)庫(kù)作為整個(gè)系統(tǒng)的吊車尾。還是別拿它搞事了。
2.如果業(yè)務(wù)沒有id帶有實(shí)時(shí)字段的要求,那么可以用預(yù)生成備用的方式??蛻舳朔?wù)每次按一定步進(jìn)來拉取id集合,并緩存到客戶端本地內(nèi)存。如此也能有效率的提升。(哪怕有實(shí)時(shí)業(yè)務(wù)段,也可以將非業(yè)務(wù)的其他部分生成好,到客戶端用時(shí)再拼接)
還有微信序列號(hào)生成器架構(gòu)可以參考
https://www.infoq.cn/article/wechat-serial-number-generator-architecture