需求
爲什麼需要唯一ID
讓分佈式系統中的需要辨別的元素,都能有唯一的辨識標誌。 幾乎所有的業務系統,都有生成一個記錄標識的需求,例如:
- 消息標識:message-id
- 訂單標識:order-id
- 帖子標識:tiezi-id
爲什麼需要趨勢有序
記錄標識上的查詢,往往又有分頁或者排序的業務需求,例如:
- 拉取最新的一頁消息:select message-id order by time limit 100
- 拉取最新的一頁訂單:select order-id order by time limit 100
- 拉取最新的一頁帖子:select tiezi-id order by time limit 100
所以往往要有一個time字段,並且在time字段上建立普通索引(non-cluster index)。
普通索引存儲的是實際記錄的指針,其訪問效率一般會比聚集索引慢,如果記錄標識在生成時能夠基本按照時間有序,則可以省去這個time字段的索引查詢:select message-id (order by message-id) limit 100
但是,能這麼做的前提是,message-id的生成基本是趨勢時間遞增的。
怎麼實現唯一ID
UUID
UUID就是爲了要在分佈式環境中產生唯一標示符而發佈的一個標準。標準中規定UUID長度爲16Bytes(128Bits),一般將其表示爲550e8400-e29b-41d4-a716-446655440000
這種16進制格式,同時將其分爲5部分,每部分用-
分割,各部分長度分別爲8,4,4,12。現在使用的UUID算法有5個版本,分別使用5種不同的算法計算產生。
- UUID1: 依據當前計算機的MAC地址和時鐘來生成uuid。
- UUID2: 和版本1類似,不過使用域標示符和本地UID代替了版本1中的時鐘信息。
- UUID3: 根據url,域標示符等標示符做MD5 Hash產生的。
- UUID4: 根據產生的隨機數來生成。
- UUID5: 和版本3類似,只不過替換成了SHA-1算法。
優點:
- 本地生成,不需要控制中心管理,成本低
- 性能好
缺點:
- id共128Bits太長
- id間沒有次序關係,不能隱含信息
mogodb ObjectId
MongoDB中每一條記錄都有一個id
字段用來唯一標示本記錄。如果用戶插入數據時沒有顯示提供id
字段,那麼系統會自動生成一個。ObjectID一共12Bytes,設計的時候充分考慮了分佈式環境下使用的情況,所以能保證在一個分佈式MongoDB集羣中唯一。ObjectID格式如下:
0 4 7 9 12
+--------+------+----+------+
|time |pc |pid |inc |
+--------+------+----+------+
0~4 Byte是Unix Timestamp。 4~7 Byte是當前機器“hostname/mac地址/虛擬編號”其中之一的MD5結果的前3個字節。 7~9 Byte是當前進程的PID。 9~12Byte是累加計數器或是一個隨機數(只有當不支持累加計數器時才用隨機數)。 最後生成的仍然是一個用16進製表示的串,如47cc67093475061e3d95369d。這裏MongoDB的ObjectID相對UUID有個很大的優點就是ObjectID是時間上有序的。另外還有ObjectID本身也包含了很多其它有用的信息,通過直接解碼ObjectID即可直接獲得這些信息。
優點:
- 時間有序
- 隱含信息,可在業務中結合加以利用。
缺點:
- 當time段一樣,由於MD5只取前3Byte,有可能造成pc段一樣,這樣就有可能有重複的id。
- ID 間隙較大(當某一段時間不生成id,那麼這個time段浪費很多空間)
snowflack
Snowflake是twitter開源的一款獨立的適用於分佈式環境的ID生成服務器。生成的ID是64Bits,同時滿足高性能(>10K ids/s),低延遲(<2ms)和高可用。與MongoDB ObjectID類似這裏生成的ID也是時間上有序的。編碼方式也和ObjectID類似,如下:
0 41 51 64
+-----------+------+------+
|time |pc |inc |
+-----------+------+------+
前41bits是以微秒爲單位的timestamp。 接着10bits是事先配置好的機器ID。 最後12bits是累加計數器。
有缺點與MongoDB ObjectId類似。但是隻要機器ID不重複,應該不會出現重複的ID。
Instagram採用的方式
Instagram要將其中存儲的圖片分片到多個PostgreSQL中,其中生成ID的方案和MongoDB ObjectID類似。整個ID的長度爲64Bits,設定爲這個長度是爲了優化在redis中的存儲。ID的編碼格式如下:
41bits以微秒爲單位的timestamp,時間起點從2011-01-01開始。 13bits表示進行邏輯分片的Shard ID。 10bits表示一個累加計數器。 ID的生成邏輯用PL/PGSQL語言寫到PostgreSQL數據庫中,當每次插入數據時由數據庫自動計算生成。 與上面優缺點類似。
Leaf
主要參考:http://wiki.sankuai.com/pages/viewpage.action?pageId=465861190。 利用step設置每個服務能從數據庫拿到的號段大小,能充分的利用id的空間,能保證號段內各個id的時間順序,但是不能保證號段間時間上的順序。
主要優點是id佔用字節少(64bits),能充分利用空間,幾乎沒有間隙(按作者說,除非服務器宕機,這種可能會比較小)。
我的想法:
假設應用生命週期爲30年(一般極少有應用生命週期30年,linux系統到現在也不超過30年,就算30年到時候也該換方案和架構了),如果時間的精確度是微秒,30年需要通過12位整數保存,使用二進制保存所有12位整數需要大約40位二進制;如果是秒,需要9位整數保存,使用大約30位二進制。假設63位中(除最高位,最高位應該是符號位。)
- 使用微秒方案:前40位給時間,那麼還有23位可以給step區間(可表示8百萬個整數,相當於容量爲1微秒8百萬個id)。
- 使用秒方案:前30位給時間,那麼還有33位可以給step區間(每秒產生id數量與使用微妙方案一秒產生的id數量相同)。
- 使用X秒方案:以此類推
對比秒方案和微秒方案,(X)秒方案可能由於時間對系統能表述的id空間的浪費更少,而且整體能表述的id數量不變,但是遞增趨勢更弱(使用微妙,遞增趨勢更強)。
總結
一般在分佈式系統中,與生成唯一ID有關的因素可以來自:
- 時間(基於某一時刻到現在的相對時間,更節約空間)
- 機器邏輯區分ID(如:機器ID,存儲的分片)
- 機器的硬件信息(如:MAC地址等)
- 局部自增