如何在分佈式場景下生成全局唯一 ID?


如何在分佈式場景下生成全局唯一 ID?

在分佈式系統中,有一些場景需要使用全局唯一 ID ,可以和業務場景有關,比如支付流水號,也可以和業務場景無關,比如分庫分表後需要有一個全局唯一 ID,或者用作事務版本號、分佈式鏈路追蹤等等,好的全局唯一 ID 需要具備這些特點:

  • 全局唯一:這是最基本的要求,不能重複;
  • 遞增:有些特殊場景是必須遞增的,比如事務版本號,後面生成的 ID 一定要大於前面的 ID ;有些場景遞增比不遞增要好,因為遞增有利於數據庫索引的性能;
  • 高可用:如果是生成唯一 ID 的系統或服務,那麼一定會有大量的調用,那麼保證其高可用就非常關鍵了;
  • 信息安全:如果 ID 是連續的,那麼很容易被惡意操作或洩密,比如訂單號是連續的,那麼很容易就被看出來一天的單量大概是多少;
  • 另外考慮到存儲壓力,ID 當然是越短越好。

那麼分佈式場景下有哪些生成唯一 ID 的方案呢?

利用數據庫生成

先說最容易理解的方案,利用數據庫的自增長序列生成:數據庫生成唯一主鍵,並通過服務提供給其他系統;如果是小型系統,數據總量和併發量都不是很大的情況下,這種方案足夠支撐。

如果每次生成一個 ID 可能會對數據庫有壓力,可以考慮一次性生成 N 個 ID 放入緩存中,如果緩存中的 ID 被取光,再通過數據庫生成下一批 ID 。

  • 優點: 理解起來最容易,實現起來也最簡單。
  • 缺點: 也非常明顯了,每種數據庫的實現不同,如果數據庫需要遷移的話比較麻煩;最大的問題是性能問題,併發量到一定級別的時候這個方法估計會很難滿足性能需求;另外通過數據庫自增生成的 ID 攜帶的信息太少,只能起到一個標識的作用,同時自增 ID 也是連續的。

利用其他組件/軟件/中間件生成

利用 Redis / MongoDB / zookeeper 生成:Redis 利用 incr 和 increby ;MongoDB 的 ObjectId;zk 通過 znode 數據版本;都可以生成全局的唯一標識碼。

我們用 MongoDB 的 ObjectId 來舉例:

<code>{"_id":ObjectId("5d47ca7528021724ac19f745")}複製代碼/<code>

MongoDB 的 ObjectId 共佔 12 個字節,其中:

  • 3.2 之前的版本(包括 3.2): 4 字節時間戳 + 3 字節機器標識符 + 2 字節進程 ID + 3字節隨機計數器
  • 3.2 之後版本: 4 字節時間戳 + 5 字節隨機值 + 3 字節遞增計數器

不管是老版本還是新版本,MongoDB 的 ObjectId 至少都可以保證集群內的唯一,我們可以搭建一個全局唯一 ID 生成的服務,利用 MongoDB 生成 ObjectId 並對外提供服務(MongoDB 的各語言驅動都實現了 ObjectId 的生成算法)。

  • 優點: 性能高於數據庫;可以使用集群部署;ID 內自帶一些含義,比如時間戳;
  • 缺點: 和數據庫一樣,需要引入對應的組件/軟件,增加了系統的複雜度;最關鍵的是,這兩種方案都意味著生成全局唯一 ID 的系統(服務),會成為一個單點,在軟件架構中,單獨就意味著風險;如果這個服務出現問題,那麼所有依賴於這個服務的系統都會崩潰掉。

UUID

這個是分佈式架構中,生成唯一標識碼最常用的算法。為了保證 UUID 的唯一性,生成因素包括了MAC地址、時間戳、名字空間(Namespace)、隨機或偽隨機數、時序等元素;UUID 有多個版本,每個版本的算法不同,應用範圍也不同:

  • Version 1: 基於時間的 UUID,是通過時間戳 + 隨機數 + MAC地址得到;如果應用直接局域網內使用,可以使用 IP 地址替代 MAC 地址;高度唯一(MAC 地址洩漏,也是一個安全問題)。
  • Version 2: DCE 安全的 UUID,把 Version 1 中的時間戳前 4 位置換為 POSIX 的 UID 或 GID ;高度唯一。
  • Version 3: 基於名字的 UUID(MD5),通過計算名字和名字空間的 MD5 散列值得到;一定範圍內唯一。
  • Version 4: 隨機 UUID,根據隨機數或偽隨機數生成 UUID;有一定概率重複。
  • Version 5: 基於名字的UUID(SHA1),和 Version 3 類似,只是散列值計算使用SHA1算法;一定範圍內唯一。

<code>publicclassCreateUUID{publicstaticvoidmain(String[]args){Stringuuid=UUID.randomUUID().toString();System.out.println("uuid:"+uuid);​uuid=UUID.randomUUID().toString().replaceAll("-","");System.out.println("uuid:"+uuid);}}複製代碼/<code>

  • 優點: 本地生成,沒有網絡消耗,不需要第三方組件(也就沒有單點的風險),生成比較簡單,性能好。
  • 缺點: 長度長,不利於存儲,並且沒有排序,相對來說還會影響性能(比如 MySQL 的 InnoDB 引擎,如果 UUID 作為數據庫主鍵,其無序性會導致數據位置頻繁變動)。

Snowflake

如果希望 ID 可以本地生成,但是又不要和 UUID 那樣無序,可以考慮使用 Snowflake 算法(Twitter開源)。

SnowFlake 算法生成 ID 是一個 64 bit 的整數,包括:

  • 1 bit : 不使用,固定是 0 ;
  • 41 bit : 時間戳(毫秒),數值範圍是:0 至 2的41次方 - 1 ;轉換成年的話,大約是 69 年;
  • 10 bit : 機器 ID ;5 位機房 ID + 5 位機器 ID ;(服務集群數量比較小的時候,可以手動配置,服務規模大的話,可以採用第三方組件進行自動配置,比如美團的 Leaf-snowflake,就是通過 Zookeeper 的持久順序節點做為機器 ID)
  • 12 bit : 序列號,用來記錄同一個毫秒內生成的不同 ID 。

在Java中,SnowFlake 算法生成的 ID 正好可以用 long 來進行存儲。

  • 優點: 本地生成,沒有網絡消耗,不需要第三方組件(也就沒有單點的風險),一定範圍內唯一(基本可以滿足大部分場景),性能好,按時間戳遞增(趨勢遞增);
  • 缺點: 依賴於機器時鐘,同一臺機器如果把時間回撥,生成的 ID 就會有重複的風險。
如何在分佈式場景下生成全局唯一 ID?

image


此外,還有很多優秀的互聯網公司也提供了唯一 ID 生成的方案或框架,比如美團開源的 Leaf ,百度開源的 UidGenerator 等等。


<code>@ResourceprivateUidGeneratoruidGenerator;​@TestpublicvoidtestSerialGenerate(){//GenerateUIDlonguid=uidGenerator.getUID();System.out.println(uidGenerator.parseUID(uid));/<code>


碼農三哥,一名普通程序員,會點java軟件開發,對AI人工智能有點興趣,後續會每日分享些關於互聯網技術方面的文章,感興趣的朋友可以關注我,一起交流學習。

想轉型或剛步入程序員Java開發的朋友,有問題可以留言或私信我。

再次感謝你的閱讀!


分享到:


相關文章: