爲何須要分佈式ID?大廠的分佈式 ID 生成方案是什麼樣的?| JavaGuide

今日推薦:Github 標星 100k!2021 最新Java 學習線路圖是怎樣的?html

下午好,我是 Guide哥!java

今天分享一道朋友去京東面試真實遇到的面試題:「爲何要分佈式ID?你項目中是怎麼作的?」。git

這篇文章我會說說本身的見解,詳細介紹一下分佈式ID相關的內容包括分佈式 ID 的基本要求以及分佈式 ID 常見的解決方案。程序員

這篇文章全程都是大白話的形式,但願可以爲你帶來幫助!github

原創不易,如有幫助,點贊/分享就是對我最大的鼓勵!面試

我的能力有限。若是文章有任何須要補充/完善/修改的地方,歡迎在評論區指出,共同進步!redis

分佈式 ID

何爲 ID?

平常開發中,咱們須要對系統中的各類數據使用 ID 惟一表示,好比用戶 ID 對應且僅對應一我的,商品 ID 對應且僅對應一件商品,訂單 ID 對應且僅對應一個訂單。算法

咱們現實生活中也有各類 ID,好比身份證 ID 對應且僅對應一我的、地址 ID 對應且僅對應sql

簡單來講,ID 就是數據的惟一標識數據庫

何爲分佈式 ID?

分佈式 ID 是分佈式系統下的 ID。分佈式 ID 不存在與現實生活中,屬於計算機系統中的一個概念。

我簡單舉一個分庫分表的例子。

我司的一個項目,使用的是單機 MySQL 。可是,沒想到的是,項目上線一個月以後,隨着使用人數愈來愈多,整個系統的數據量將愈來愈大。

單機 MySQL 已經沒辦法支撐了,須要進行分庫分表(推薦 Sharding-JDBC)。

在分庫以後, 數據遍及在不一樣服務器上的數據庫,數據庫的自增主鍵已經沒辦法知足生成的主鍵惟一了。咱們如何爲不一樣的數據節點生成全局惟一主鍵呢?

這個時候就須要生成分佈式 ID了。

分佈式 ID 須要知足哪些要求?

分佈式 ID 做爲分佈式系統中必不可少的一環,不少地方都要用到分佈式 ID。

一個最基本的分佈式 ID 須要知足下面這些要求:

  • 全局惟一 :ID 的全局惟一性確定是首先要知足的!
  • 高性能 : 分佈式 ID 的生成速度要快,對本地資源消耗要小。
  • 高可用 :生成分佈式 ID 的服務要保證可用性無限接近於 100%。
  • 方便易用 :拿來即用,使用方便,快速接入!

除了這些以外,一個比較好的分佈式 ID 還應保證:

  • 安全 :ID 中不包含敏感信息。
  • 有序遞增 :若是要把 ID 存放在數據庫的話,ID 的有序性能夠提高數據庫寫入速度。而且,不少時候 ,咱們還頗有可能會直接經過 ID 來進行排序。
  • 有具體的業務含義 :生成的 ID 若是能有具體的業務含義,可讓定位問題以及開發更透明化(經過 ID 就能肯定是哪一個業務)。
  • 獨立部署 :也就是分佈式系統單獨有一個發號器服務,專門用來生成分佈式 ID。這樣就生成 ID 的服務能夠和業務相關的服務解耦。不過,這樣一樣帶來了網絡調用消耗增長的問題。總的來講,若是須要用到分佈式 ID 的場景比較多的話,獨立部署的發號器服務仍是頗有必要的。

分佈式 ID 常看法決方案

數據庫

數據庫主鍵自增

這種方式就比較簡單直白了,就是經過關係型數據庫的自增主鍵產生來惟一的 ID。

以 MySQL 舉例,咱們經過下面的方式便可。

1.建立一個數據庫表。

CREATE TABLE `sequence_id` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `stub` char(10) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
複製代碼

stub 字段無心義,只是爲了佔位,便於咱們插入或者修改數據。而且,給 stub 字段建立了惟一索引,保證其惟一性。

2.經過 replace into 來插入數據。

BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT LAST_INSERT_ID();
COMMIT;
複製代碼

插入數據這裏,咱們沒有使用 insert into 而是使用 replace into 來插入數據,具體步驟是這樣的:

1)第一步: 嘗試把數據插入到表中。

2)第二步: 若是主鍵或惟一索引字段出現重複數據錯誤而插入失敗時,先從表中刪除含有重複關鍵字值的衝突行,而後再次嘗試把數據插入到表中。

這種方式的優缺點也比較明顯:

  • 優勢 :實現起來比較簡單、ID 有序遞增、存儲消耗空間小
  • 缺點 : 支持的併發量不大、存在數據庫單點問題(可使用數據庫集羣解決,不過增長了複雜度)、ID 沒有具體業務含義、安全問題(好比根據訂單 ID 的遞增規律就能推算出天天的訂單量,商業機密啊! )、每次獲取 ID 都要訪問一次數據庫(增長了對數據庫的壓力,獲取速度也慢)

數據庫號段模式

數據庫主鍵自增這種模式,每次獲取 ID 都要訪問一次數據庫,ID 需求比較大的時候,確定是不行的。

若是咱們能夠批量獲取,而後存在在內存裏面,須要用到的時候,直接從內存裏面拿就舒服了!這也就是咱們說的 基於數據庫的號段模式來生成分佈式 ID。

數據庫的號段模式也是目前比較主流的一種分佈式 ID 生成方式。像滴滴開源的Tinyid 就是基於這種方式來作的。不過,TinyId 使用了雙號段緩存、增長多 db 支持等方式來進一步優化。

以 MySQL 舉例,咱們經過下面的方式便可。

1.建立一個數據庫表。

CREATE TABLE `sequence_id_generator` (
  `id` int(10) NOT NULL,
  `current_max_id` bigint(20) NOT NULL COMMENT '當前最大id',
  `step` int(10) NOT NULL COMMENT '號段的長度',
  `version` int(20) NOT NULL COMMENT '版本號',
  `biz_type`    int(20) NOT NULL COMMENT '業務類型',
   PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
複製代碼

current_max_id 字段和step字段主要用於獲取批量 ID,獲取的批量 id 爲: current_max_id ~ current_max_id+step

version 字段主要用於解決併發問題(樂觀鎖),biz_type 主要用於表示業餘類型。

2.先插入一行數據。

INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`)
VALUES
	(1, 0, 100, 0, 101);
複製代碼

3.經過 SELECT 獲取指定業務下的批量惟一 ID

SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101
複製代碼

結果:

id	current_max_id	step	version	biz_type
1	0	100	1	101
複製代碼

4.不夠用的話,更新以後從新 SELECT 便可。

UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0  AND `biz_type` = 101
SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101
複製代碼

結果:

id	current_max_id	step	version	biz_type
1	100	100	1	101
複製代碼

相比於數據庫主鍵自增的方式,數據庫的號段模式對於數據庫的訪問次數更少,數據庫壓力更小。

另外,爲了不單點問題,你能夠從使用主從模式來提升可用性。

數據庫號段模式的優缺點:

  • 優勢 :ID 有序遞增、存儲消耗空間小
  • 缺點 :存在數據庫單點問題(可使用數據庫集羣解決,不過增長了複雜度)、ID 沒有具體業務含義、安全問題(好比根據訂單 ID 的遞增規律就能推算出天天的訂單量,商業機密啊! )

NoSQL

通常狀況下,NoSQL 方案使用 Redis 多一些。咱們經過 Redis 的 incr 命令便可實現對 id 原子順序遞增。

127.0.0.1:6379> set sequence_id_biz_type 1
OK
127.0.0.1:6379> incr sequence_id_biz_type
(integer) 2
127.0.0.1:6379> get sequence_id_biz_type
"2"
複製代碼

爲了提升可用性和併發,咱們可使用 Redis Cluser。Redis Cluser 是 Redis 官方提供的 Redis 集羣解決方案(3.0+版本)。

除了 Redis Cluser 以外,你也可使用開源的 Redis 集羣方案Codis (大規模集羣好比上百個節點的時候比較推薦)。

除了高可用和併發以外,咱們知道 Redis 基於內存,咱們須要持久化數據,避免重啓機器或者機器故障後數據丟失。Redis 支持兩種不一樣的持久化方式:快照(snapshotting,RDB)只追加文件(append-only file, AOF)。 而且,Redis 4.0 開始支持 RDB 和 AOF 的混合持久化(默認關閉,能夠經過配置項 aof-use-rdb-preamble 開啓)。

關於 Redis 持久化,我這裏就不過多介紹。不瞭解這部份內容的小夥伴,能夠看看 JavaGuide 對於 Redis 知識點的總結

Redis 方案的優缺點:

  • 優勢 : 性能不錯而且生成的 ID 是有序遞增的
  • 缺點 : 和數據庫主鍵自增方案的缺點相似

除了 Redis 以外,MongoDB ObjectId 常常也會被拿來當作分佈式 ID 的解決方案。

MongoDB ObjectId 一共須要 12 個字節存儲:

  • 0~3:時間戳
  • 3~6: 表明機器 ID
  • 7~8:機器進程 ID
  • 9~11 :自增值

MongoDB 方案的優缺點:

  • 優勢 : 性能不錯而且生成的 ID 是有序遞增的
  • 缺點 : 須要解決重複 ID 問題(當機器時間不對的狀況下,可能致使會產生重複 ID) 、有安全性問題(ID 生成有規律性)

算法

UUID

UUID 是 Universally Unique Identifier(通用惟一標識符) 的縮寫。UUID 包含 32 個 16 進制數字(8-4-4-4-12)。

JDK 就提供了現成的生成 UUID 的方法,一行代碼就好了。

//輸出示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa
UUID.randomUUID()
複製代碼

RFC 4122 中關於 UUID 的示例是這樣的:

咱們這裏重點關注一下這個 Version(版本),不一樣的版本對應的 UUID 的生成規則是不一樣的。

5 種不一樣的 Version(版本)值分別對應的含義(參考維基百科對於 UUID 的介紹):

  • 版本 1 : UUID 是根據時間和節點 ID(一般是 MAC 地址)生成;
  • 版本 2 : UUID 是根據標識符(一般是組或用戶 ID)、時間和節點 ID 生成;
  • 版本 三、版本 5 : 版本 5 - 肯定性 UUID 經過散列(hashing)名字空間(namespace)標識符和名稱生成;
  • 版本 4 : UUID 使用隨機性僞隨機性生成。

下面是 Version 1 版本下生成的 UUID 的示例:

JDK 中經過 UUIDrandomUUID() 方法生成的 UUID 的版本默認爲 4。

UUID uuid = UUID.randomUUID();
int version = uuid.version();// 4
複製代碼

另外,Variant(變體)也有 4 種不一樣的值,這種值分別對應不一樣的含義。這裏就不介紹了,貌似平時也不怎麼須要關注。

須要用到的時候,去看看維基百科對於 UUID 的 Variant(變體) 相關的介紹便可。

從上面的介紹中能夠看出,UUID 能夠保證惟一性,由於其生成規則包括 MAC 地址、時間戳、名字空間(Namespace)、隨機或僞隨機數、時序等元素,計算機基於這些規則生成的 UUID 是確定不會重複的。

雖然,UUID 能夠作到全局惟一性,可是,咱們通常不多會使用它。

好比使用 UUID 做爲 MySQL 數據庫主鍵的時候就很是不合適:

  • 數據庫主鍵要儘可能越短越好,而 UUID 的消耗的存儲空間比較大(32 個字符串,128 位)。
  • UUID 是無順序的,InnoDB 引擎下,數據庫主鍵的無序性會嚴重影響數據庫性能。

最後,咱們再簡單分析一下 UUID 的優缺點 (面試的時候可能會被問到的哦!) :

  • 優勢 :生成速度比較快、簡單易用
  • 缺點 : 存儲消耗空間大(32 個字符串,128 位) 、 不安全(基於 MAC 地址生成 UUID 的算法會形成 MAC 地址泄露)、無序(非自增)、沒有具體業務含義、須要解決重複 ID 問題(當機器時間不對的狀況下,可能致使會產生重複 ID)

Snowflake(雪花算法)

Snowflake 是 Twitter 開源的分佈式 ID 生成算法。Snowflake 由 64 bit 的二進制數字組成,這 64bit 的二進制被分紅了幾部分,每一部分存儲的數據都有特定的含義:

  • 第 0 位: 符號位(標識正負),始終爲 0,沒有用,不用管。
  • 第 1~41 位 :一共 41 位,用來表示時間戳,單位是毫秒,能夠支撐 2 ^41 毫秒(約 69 年)
  • 第 42~52 位 :一共 10 位,通常來講,前 5 位表示機房 ID,後 5 位表示機器 ID(實際項目中能夠根據實際狀況調整)。這樣就能夠區分不一樣集羣/機房的節點。
  • 第 53~64 位 :一共 12 位,用來表示序列號。 序列號爲自增值,表明單臺機器每毫秒可以產生的最大 ID 數(2^12 = 4096),也就是說單臺機器每毫秒最多能夠生成 4096 個 惟一 ID。

若是你想要使用 Snowflake 算法的話,通常不須要你本身再造輪子。有不少基於 Snowflake 算法的開源實現好比美團 的 Leaf、百度的 UidGenerator,而且這些開源實現對原有的 Snowflake 算法進行了優化。

另外,在實際項目中,咱們通常也會對 Snowflake 算法進行改造,最多見的就是在 Snowflake 算法生成的 ID 中加入業務類型信息。

咱們再來看看 Snowflake 算法的優缺點 :

  • 優勢 :生成速度比較快、生成的 ID 有序遞增、比較靈活(能夠對 Snowflake 算法進行簡單的改造好比加入業務 ID)
  • 缺點 : 須要解決重複 ID 問題(依賴時間,當機器時間不對的狀況下,可能致使會產生重複 ID)。

開源框架

UidGenerator(百度)

UidGenerator 是百度開源的一款基於 Snowflake(雪花算法)的惟一 ID 生成器。

不過,UidGenerator 對 Snowflake(雪花算法)進行了改進,生成的惟一 ID 組成以下。

能夠看出,和原始 Snowflake(雪花算法)生成的惟一 ID 的組成不太同樣。而且,上面這些參數咱們均可以自定義。

UidGenerator 官方文檔中的介紹以下:

自 18 年後,UidGenerator 就基本沒有再維護了,我這裏也不過多介紹。想要進一步瞭解的朋友,能夠看看 UidGenerator 的官方介紹

Leaf(美團)

Leaf 是美團開源的一個分佈式 ID 解決方案 。這個項目的名字 Leaf(樹葉) 起源於德國哲學家、數學家萊布尼茨的一句話: 「There are no two identical leaves in the world」(世界上沒有兩片相同的樹葉) 。這名字起得真心挺不錯的,有點文藝青年那味了!

Leaf 提供了 號段模式Snowflake(雪花算法) 這兩種模式來生成分佈式 ID。而且,它支持雙號段,還解決了雪花 ID 系統時鐘回撥問題。不過,時鐘問題的解決須要弱依賴於 Zookeeper 。

Leaf 的誕生主要是爲了解決美團各個業務線生成分佈式 ID 的方法多種多樣以及不可靠的問題。

Leaf 對原有的號段模式進行改進,好比它這裏增長了雙號段避免獲取 DB 在獲取號段的時候阻塞請求獲取 ID 的線程。簡單來講,就是我一個號段還沒用完以前,我本身就主動提早去獲取下一個號段(圖片來自於美團官方文章:《Leaf——美團點評分佈式 ID 生成系統》)。

根據項目 README 介紹,在 4C8G VM 基礎上,經過公司 RPC 方式調用,QPS 壓測結果近 5w/s,TP999 1ms。

Tinyid(滴滴)

Tinyid 是滴滴開源的一款基於數據庫號段模式的惟一 ID 生成器。

數據庫號段模式的原理咱們在上面已經介紹過了。Tinyid 有哪些亮點呢?

爲了搞清楚這個問題,咱們先來看看基於數據庫號段模式的簡單架構方案。(圖片來自於 Tinyid 的官方 wiki:《Tinyid 原理介紹》

在這種架構模式下,咱們經過 HTTP 請求向發號器服務申請惟一 ID。負載均衡 router 會把咱們的請求送往其中的一臺 tinyid-server。

這種方案有什麼問題呢?在我看來(Tinyid 官方 wiki 也有介紹到),主要由下面這 2 個問題:

  • 獲取新號段的狀況下,程序獲取惟一 ID 的速度比較慢。
  • 須要保證 DB 高可用,這個是比較麻煩且耗費資源的。

除此以外,HTTP 調用也存在網絡開銷。

Tinyid 的原理比較簡單,其架構以下圖所示:

相比於基於數據庫號段模式的簡單架構方案,Tinyid 方案主要作了下面這些優化:

  • 雙號段緩存 :爲了不在獲取新號段的狀況下,程序獲取惟一 ID 的速度比較慢。 Tinyid 中的號段在用到必定程度的時候,就會去異步加載下一個號段,保證內存中始終有可用號段。
  • 增長多 db 支持 :支持多個 DB,而且,每一個 DB 都能生成惟一 ID,提升了可用性。
  • 增長 tinyid-client :純本地操做,無 HTTP 請求消耗,性能和可用性都有很大提高。

Tinyid 的優缺點這裏就不分析了,結合數據庫號段模式的優缺點和 Tinyid 的原理就能知道。

分佈式 ID 生成方案總結

這篇文章中,我基本上已經把最多見的分佈式 ID 生成方案都總結了一波。

後記

最後再推薦一個很是不錯的 Java 教程類開源項目:JavaGuide 。我在大三開始準備秋招面試的時候,建立了 JavaGuide 這個項目。目前這個項目已經有 100k+的 star,相關閱讀:《1049 天,100K!簡單覆盤!》

對於你學習 Java 以及準備 Java 方向的面試都頗有幫助!正如做者說的那樣,這是一份:涵蓋大部分 Java 程序員所須要掌握的核心知識的 Java 學習+面試指南!

相關推薦:

我是 Guide哥,擁抱開源,喜歡烹飪。開源項目 JavaGuide 做者,Github:Snailclimb - Overview 。將來幾年,但願持續完善 JavaGuide,爭取可以幫助更多學習 Java 的小夥伴!共勉!凎!點擊查看個人2020年工做彙報!

除了上面介紹的方式以外,像 ZooKeeper 這類中間件也能夠幫助咱們生成惟一 ID。沒有銀彈,必定要結合實際項目來選擇最適合本身的方案。

相關文章
相關標籤/搜索