上兩篇講到了我們的系統在面臨大并發讀取的時候,采用了讀寫分離主從復制(數據庫讀寫分離方案,實現高性能數據庫集群)的方案去應對,后來又面臨了大并發寫入的時候,系統數據庫采用了分庫分表的方案(數據庫分庫分表方案,優化大量并發寫入所帶來的性能問題),通過垂直拆分以及水平拆分的方式,將數據分到多個庫和多個表中去應對的,即現在是這樣的一套分布式存儲結構。
數據庫分庫分表那篇也講到了,使用了分庫分表勢必會帶來和我們之前使用不大相同的問題。今天,我將其中一個和我們開發息息相關的問題提出來進行講解,也就是我們開發中所使用的的主鍵的問題。我們知道,以前我們單庫的時候,主鍵唯一ID是自增的,現在好了,我們的數據被分到多個庫的多個表里面了,如果我們還是使用之前的主鍵自增策略,那么這樣就會出現兩個數據插入到了兩個不同的表會出現相同的ID值,這時我們該怎么去使用呢?
對于什么是主鍵,主鍵該怎么選,今天不做講解,我相信大家可能比我還精通,我們今天主要是講唯一主鍵ID在分布式存儲系統下怎么生成,保證ID的唯一性且符合我們業務需要,才是我們開發人員最關心的實戰。
UUID
這個時候,你可能會說,自增用不了,那我就是用UUID嘛,這個UUID生成出來的就是唯一的。的確,在我以前在一個公司中的確接觸到是使用UUID來生成唯一主鍵ID的,而且性能還可以。但是,我想提一點的就是,當這個ID和我們業務交集不相關的時候是可以使用UUID生成主鍵的。比如,一般我們業務是需要用來做查詢的,而且最好是單調遞增的,這樣我們的UUID就很不適合了。
主鍵ID單調遞增有什么好處呢?
1,就拿我們用戶關注航班這個模塊來說,我們查看某個航班關注用戶按照時間的先后進行排序。因為現在的ID是時間上有序的,所以現在我們就可以按照ID來進行排序了,同時這樣對于有些并不是要存儲時間的業務來說,會減少不少的存儲空間。
2,有序的ID可以提升數據寫入的性能
我們知道主鍵其實在數據庫中就是一種索引,而索引在MySQL數據庫的B+數據結構中是順序存儲的,所以每次插入的時候就是遞增排序的,直接追加到后面就行。如果是無序的話,則每次插入數據之前還得查找它應該所在的位置,這無疑就會增加數據的異動等相關的開銷,如下圖:
如上圖所示,如果我們生成的ID是有序的,那這個 50 就直接插在尾部就行了,如果是無序的話,突然生成了一個 26,我們還得先找到 26 需要存放的位置,然后還要對其后面數據進行挪位置。
3,UUID不具備業務相關性
我們現在開發的項目都是依據公司業務開展的,而我們的唯一ID一般都是和業務有關系的,比如,有些訂單ID中帶上了時間的維度、機房的維度以及業務類型等維度。也就是為了我方便進行定位是那種業務的訂單,才會這么設計的,是不是。
而UUID是由32位的16進制數字組成的字符串,不僅在存儲空間上造成浪費,更不具備我們業務相關性。那我們該怎么解決呢?其實twitter提出來的Snowflake 算法就能很好滿足我們現在的要求,滿足了主鍵ID的全局唯一性、單調遞增性,也可以滿足我們的業務相關。所以,我們現在使用的唯一ID生成方式就是使用Snowflake算法,這個算法其實很簡單。下面我們來對其進行講解,并對其相應改造使其能用到我們的開發業務中來。
Snowflake 算法原理
Snowflake 是由 64 比特bit二進制數字組成的,一共分為4大部分:
- 1位默認不使用
- 41位時間戳
- 10位機器ID
- 12位序列號
- 我們從上圖中可以看出snowflake算法的第二部分的41位時間戳,大概可以支撐2^41/1000/60/60/24/365 年,也就是大約有69年。我們設計一個系統用69年應該是足夠了吧。
- 10位的機器ID我們可以怎么使用呢?我們可以劃分成大概2到3位IDC,也就是可以支撐4到8個IDC機房;然后劃分7到 8 位的機器ID,即可以支撐128~256臺機器。
- 12位的序號,就代表每個節點每毫秒可以生成4096個ID序號。
如何改造
我們現在已經知道了Snowflake 算法的核心原理,并且知道了其有64位的二進制數據,那我們就可以根據自己業務進行改造以更好的來為我們業務服務。一般不同的公司對其進行改造的方式都不盡相同,但是道理都是一樣的。我們可以這么做:
- 我們是減少序列號的位數,增加機器ID的位數,是為了用來支撐我們單IDC的更多機器。
- 將我們業務ID加入進去用來區分我們不同的業務。比如,1位0 + 41位時間戳 + 6位IDC(64個IDC) + 6位業務信息(支撐64種業務) + 10位自增序列(每毫秒1024個ID)
如此,我們就可以在單機房部署這么一個統一ID發號器,然后用Keeplive 保證高可用(對于高可用不熟悉的回去看看哈「高可用」你們服務器掛了怎么辦,我們是這樣做的)可以將不同的業務模塊ID加入進去,這樣的好處是即使哪個業務出問題了,我只看ID號我就分析出來,比如,我看到現在ID號有我的訂單ID業務,我就去看訂單模塊。
開發如何使用
現在我們知道Snowflake 算法原理了,還知道了我們可以進行改造了。那我們開發人員該怎么去使用,來為我們業務生成統一的唯一ID呢?
1,直接嵌入到業務代碼
嵌入業務代碼的意思就是,這個snowflake算法就部署在和我們業務相同的服務器上,這樣我們代碼使用的時候,就不用了跨網絡調用,性能相對比較好。但是也是有缺點的,因為我們的業務機器肯定是很多的,這就意味著我們發號器算法需要更多的機器ID位數。同時,太多的業務服務器我們會很難保證業務機器id的唯一性,這里就需要引用zookeeper一致性組件來保證每次機器重啟都能能獲得唯一的機器ID。
2,獨立部署成發號器服務
也就是說,我們將其作為單獨的服務部署到單獨的機器上,已對外提供服務。這樣就是多了網絡的傳輸,不過影響不大,比如,我可以將其部署成一個主備的方式對外提供發號服務,機器ID可以用作序列號使用,這樣也就是會有更多的自增序號,有部分大廠就是以這樣單獨的服務提供出來的。
開發中避坑大法
1,雖然snowflake很優秀,但是它是基于系統時間的,萬一我們系統的時間不準怎么辦,就會造成我們的ID會重復。那我們的做法就是,要利用系統的對時功能,一旦發現時間不一致,就暫停發號器,等到時鐘準了在啟用。
2,還有一個坑比較關鍵,也是常發生的,就是當我們的QPS并發不高的時候,比如每毫秒只生成一個ID號,這樣就是直接結果是,每次生成的ID末尾都是1,這樣我們分庫分表就會出現問題呀對吧,因為我們用這個ID去分庫分表呀,會造成數據不均勻,是吧,忘記了去復習哈(數據庫分庫分表方案,優化大量并發寫入所帶來的性能問題)那我們怎么解決呢?
我們可以將時間戳記錄從毫秒記錄改為秒記錄,這樣我一秒可以發好多個號了
生成的序列號起始號隨機啟動,比如這一秒起始號是10,我下一秒隨機了變成了28,這樣就更加分散開了。
總結,今天我們針對分庫分表之后帶來的第一個直接影響我們開發的問題,就是主鍵ID唯一性的問題,然后說到了使用Snowflake算法去解決,并且對其原理和使用進行了詳細的講解,同時,還將其在使用中遇到的坑給講出來了,也對其進行了填坑分析,讓大家直接避免遇到同樣的問題。當然生成唯一ID有多種,我們根據業務選擇合適我們自己的就好,你們是基于什么方式生成的可以也可以告訴大家。