相信我們一談到數(shù)據(jù)存入數(shù)據(jù)庫時猎塞,我們都會為數(shù)據(jù)庫的表設置一個表主鍵(PK)试读,作為表中每條記錄的唯一標識,這也是數(shù)據(jù)庫設計范式中的第一范式邢享。那么鹏往,自打我們使用數(shù)據(jù)庫來存儲數(shù)據(jù)時,數(shù)據(jù)庫的廠商都會為我們提供自動生成主鍵ID值的功能骇塘。例如伊履,我們熟悉的Mysql是通過主鍵自增的方式來生成,Oracle則是通過定義序列來為主鍵ID賦值款违,SqlServer與Mysql一樣也提供了主鍵自增的方式唐瀑。
那么,有沒有想過數(shù)據(jù)庫的這種主鍵生成策略有沒有問題呢插爹?問題肯定是有的哄辣,比如请梢,當我們的應用產(chǎn)生的數(shù)據(jù)越來越龐大時,業(yè)界都會采用水平拆分的方式力穗,也就是我們常說的分庫分表策略來對數(shù)據(jù)進行拆分存儲毅弧。一旦數(shù)據(jù)庫做了分庫分表,那么問題就來了当窗。舉個例子够坐,我們有一張用戶表(user_tb),然后我們現(xiàn)在把user_tb做了拆分崖面,分成了5張用戶表來存(user_tb1,user_tb2,user_tb3,user_tb4,user_5)元咙,假設原先user_tb表里有10條記錄,id值為1~10,那么分完表后巫员,數(shù)據(jù)按照id值取模的方式存放的話庶香,我們則得到下面5張表:
當有新用戶注冊的時候,我們需要保存用戶數(shù)據(jù)简识,那么這個時候赶掖,假設是寫到user_tb1,那么由于主鍵是自增的财异,這個時候就都在user_tb1中寫入一條ID值為7的用戶記錄倘零,顯然這條記錄與user_tb2中的記錄ID值重復了。那么戳寸,針對這個問題該如何解決呢?我們可以通過重新設置每張user_tb表的主鍵生成策略來解決拷泽,將5張user_tb表的ID初始值為11疫鹊,然后設置user_tb1的ID自增步長為1,user_tb2的ID自增步長為2司致,以此類推拆吆,user_tb5的步長為5,這樣當有新數(shù)據(jù)保存時脂矫,就可以保存了5張表各自生成的ID都不會與其他表重復了枣耀,這樣也就解決了ID重復的問題。但是庭再,當我們的數(shù)據(jù)進一步增長的時候捞奕,我們發(fā)現(xiàn)5張表已經(jīng)無法保存的時候,就需要繼續(xù)擴展拄轻,增加分表來分灘數(shù)據(jù)颅围,而這個時候,我們又需要去修改每張表的主鍵生成策略恨搓,顯然這種方式院促,數(shù)據(jù)遷移的成本是巨大的筏养,也需要DBA長期進行維護。那么常拓,我們也許就會想渐溶,我們不再通過數(shù)據(jù)庫自己生成ID,而是通過我們的應用程序來生成主鍵ID弄抬,是否可以掌猛?
于是,我們會發(fā)現(xiàn)眉睹,通過程序生成主鍵ID這種方法荔茬,我們就需要考慮在高并發(fā)的情況,程序需要保證生成的主鍵ID是全局唯一的竹海,且不可以與歷史生成的ID重復慕蔚。相信很多同學都使用過SnowFlake算法來生成全局主鍵ID,那么你是否對SnowFlake算法有所了解呢斋配?
SnowFlake算法是 Twitter 開源的分布式 id 生成算法孔飒。其核心思想就是:使用一個 64 bit 的 long 型的數(shù)字作為全局唯一 id。在分布式系統(tǒng)中的應用十分廣泛艰争,且ID 引入了時間戳坏瞄,基本上保持自增的。
從下圖我們可以看到甩卓,SnowFlake ID的組成部分鸠匀,它是由1位符號位,41位時間戳逾柿,10位機器ID缀棍,12位序列號組成。我解釋下各個部分的具體含義:
- 符號位:基本不用机错,該部分值固定為0爬范,可無需理會;
- 時間戳:用來記錄時間戳弱匪,毫秒級青瀑。41位可以表示2的41次方-1個數(shù)字,如果只用來表示正整數(shù)(計算機中正數(shù)包含0)萧诫,可以表示的數(shù)值范圍是:0 至 2的41次方-1斥难,減1是因為可表示的數(shù)值范圍是從0開始算的,而不是1财搁。也就是說41位可以表示2的41次方-1個毫秒的值蘸炸,轉化成單位年則是(2的41次方-1)/1000606024365=69年。
- 機器ID:10位用來記錄工作機器ID尖奔,可提供2的10次方搭儒,1024個數(shù)字穷当,包含5位數(shù)據(jù)中心ID+5位工作進程ID。
-
序列號:12位序列號可提供2的12次方-1淹禾,即4095個數(shù)字馁菜,序列號為同一時間戳下,一毫秒可生成4095個序列號铃岔。
image.png
通過上面對SnowFlake ID的分析汪疮,我們可以得到以下結論。SnowFlake算法毁习,在單體應用中智嚷,一毫秒可以為我們產(chǎn)生4095個ID,可保證單體應用持續(xù)69年生成全局唯一的ID纺且。說到這里盏道,我們可能會覺得,一個應用能跑69年基本上已經(jīng)很了不起了载碌,就算69年后ID會有重復的問題猜嘱,相信69年后一定會有更好的ID生成解決方案的出現(xiàn),所以不必擔心嫁艇。那么朗伶,由于我們業(yè)務的不斷壯大,我們的系統(tǒng)會從原來的單體架構變成分布式的系統(tǒng)架構步咪。這時论皆,SnowFlake ID還可以保證我們的ID生成是全局唯一的嗎?答案是不一定能保證歧斟。
- 進程ID沖突問題
舉個例子:在分布式架構中纯丸,同一個機房,我們會把應用服務部署在多臺服務器中静袖。那么這個時候,由于應用服務連接的是同一個數(shù)據(jù)中心俊扭,那么不同服務器的應用服務連接的數(shù)據(jù)中心ID是一樣的队橙,而不同服務器的應用服務進程ID是否有可能會出現(xiàn)一樣呢,是有可能的萨惑,那么當系統(tǒng)高并發(fā)時捐康,在同一毫秒下,不同服務器上的應用服務生成的ID是有一定概率發(fā)生重復的庸蔼〗庾埽可能,我們會覺得這種情況發(fā)生的機率很低姐仅,可以通過拋出異常花枫,重新讓用戶再提交刻盐。但是,問題的確還是會一直存在劳翰。 - 時鐘回播
舉個例子:在分布式架構中敦锌,我們一般會選擇Linux服務器來部署我們的應用,由于SnowFlake算法生成ID對時間戳的依賴佳簸,一旦發(fā)生時鐘回播乙墙,在SnowFlake算法中,那么就可能會導致歷史時刻生成的ID在未來時間由于時鐘回播原因生均,而發(fā)生ID生成重復听想。我們需要對所有Linux服務器進行時鐘統(tǒng)一,這本身就是很麻煩的事马胧,由于Linux系統(tǒng)的機制汉买,每次系統(tǒng)重啟都會導致時鐘往前回播一點點,也就又得重新同步一次所有服務器的時鐘漓雅。這個時候录别,就需要所有服務器都去連接NTP來獲取時間進行統(tǒng)一,而我們的服務器一般都是內網(wǎng)環(huán)境邻吞,是無法連接公網(wǎng)的NTP服務器组题,所以我們需要在自己的內網(wǎng)部署一臺NTP服務器,而NTP服務器要保證高可用抱冷,我們還得部署多一臺NTP服務器來作為備用節(jié)點使用崔列,這不是一件很麻煩的事嗎?維護成本也很高旺遮。
以上是通過對SnowFlake算法生成ID的機制赵讯,得到的ID有可能出現(xiàn)重復的原因,那么連SnowFlake算法都不能很好解決全局唯一ID生成的問題耿眉,該如何解決這個問題呢边翼?
別擔心,由于SnowFlake算法存在的這些缺陷鸣剪,目前業(yè)界已經(jīng)有了自己的解決方案组底,并且也都開源了,下面做一下分享筐骇。 - 滴滴TinyId: https://github.com/didi/tinyid
- 百度Uid-generator:https://github.com/baidu/uid-generator
- 美團Leaf:https://github.com/Meituan-Dianping/Leaf
關于以上大廠的全局ID生成解決方案分享债鸡,3種方案的區(qū)別簡單講解一下: - 滴滴TinyId:采用的是借助數(shù)據(jù)庫表的方式,預分配ID分段铛纬,來提供主鍵生成厌均,并沒有使用SnowFlake算法。
- 百度Uid-generator:采用SnowFlake算法并對其進行了改良了告唆,采用了未來時間的機制保證解決了時鐘回播的問題棺弊,但這種方式相對縮短了ID生成可使用的年限晶密,少于69年。
- 美團Leaf:3種解決方案中镊屎,個人覺得做得最好的惹挟,也是對SnowFlake算法的改良,通過Zookeeper來保證機器ID沖突的問題缝驳,并且不依賴數(shù)據(jù)庫连锯,采用中心化服務的方式,來提供全局ID的生成用狱,避免了不同服務器時鐘回播的問題运怖,并提供了集群高可用的解決方案。
關于詳細的實現(xiàn)機制夏伊,此處不再做進一步講解摇展,感興趣的同學,可以上github查閱溺忧,主頁上都提供了文檔資料咏连。