下午好,我是 Guide哥!java
今天分享一道朋友去京東面試真實遇到的面試題:「爲何要分佈式ID?你項目中是怎麼作的?」。git
這篇文章我會說說本身的見解,詳細介紹一下分佈式ID相關的內容包括分佈式 ID 的基本要求以及分佈式 ID 常見的解決方案。程序員
這篇文章全程都是大白話的形式,但願可以爲你帶來幫助!github
原創不易,如有幫助,點贊/分享就是對我最大的鼓勵!面試
我的能力有限。若是文章有任何須要補充/完善/修改的地方,歡迎在評論區指出,共同進步!redis
平常開發中,咱們須要對系統中的各類數據使用 ID 惟一表示,好比用戶 ID 對應且僅對應一我的,商品 ID 對應且僅對應一件商品,訂單 ID 對應且僅對應一個訂單。算法
咱們現實生活中也有各類 ID,好比身份證 ID 對應且僅對應一我的、地址 ID 對應且僅對應sql
簡單來講,ID 就是數據的惟一標識。數據庫
分佈式 ID 是分佈式系統下的 ID。分佈式 ID 不存在與現實生活中,屬於計算機系統中的一個概念。
我簡單舉一個分庫分表的例子。
我司的一個項目,使用的是單機 MySQL 。可是,沒想到的是,項目上線一個月以後,隨着使用人數愈來愈多,整個系統的數據量將愈來愈大。
單機 MySQL 已經沒辦法支撐了,須要進行分庫分表(推薦 Sharding-JDBC)。
在分庫以後, 數據遍及在不一樣服務器上的數據庫,數據庫的自增主鍵已經沒辦法知足生成的主鍵惟一了。咱們如何爲不一樣的數據節點生成全局惟一主鍵呢?
這個時候就須要生成分佈式 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 生成方式。像滴滴開源的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
複製代碼
相比於數據庫主鍵自增的方式,數據庫的號段模式對於數據庫的訪問次數更少,數據庫壓力更小。
另外,爲了不單點問題,你能夠從使用主從模式來提升可用性。
數據庫號段模式的優缺點:
通常狀況下,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 方案的優缺點:
除了 Redis 以外,MongoDB ObjectId 常常也會被拿來當作分佈式 ID 的解決方案。
MongoDB ObjectId 一共須要 12 個字節存儲:
MongoDB 方案的優缺點:
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 的介紹):
下面是 Version 1 版本下生成的 UUID 的示例:
JDK 中經過 UUID
的 randomUUID()
方法生成的 UUID 的版本默認爲 4。
UUID uuid = UUID.randomUUID();
int version = uuid.version();// 4
複製代碼
另外,Variant(變體)也有 4 種不一樣的值,這種值分別對應不一樣的含義。這裏就不介紹了,貌似平時也不怎麼須要關注。
須要用到的時候,去看看維基百科對於 UUID 的 Variant(變體) 相關的介紹便可。
從上面的介紹中能夠看出,UUID 能夠保證惟一性,由於其生成規則包括 MAC 地址、時間戳、名字空間(Namespace)、隨機或僞隨機數、時序等元素,計算機基於這些規則生成的 UUID 是確定不會重複的。
雖然,UUID 能夠作到全局惟一性,可是,咱們通常不多會使用它。
好比使用 UUID 做爲 MySQL 數據庫主鍵的時候就很是不合適:
最後,咱們再簡單分析一下 UUID 的優缺點 (面試的時候可能會被問到的哦!) :
Snowflake 是 Twitter 開源的分佈式 ID 生成算法。Snowflake 由 64 bit 的二進制數字組成,這 64bit 的二進制被分紅了幾部分,每一部分存儲的數據都有特定的含義:
若是你想要使用 Snowflake 算法的話,通常不須要你本身再造輪子。有不少基於 Snowflake 算法的開源實現好比美團 的 Leaf、百度的 UidGenerator,而且這些開源實現對原有的 Snowflake 算法進行了優化。
另外,在實際項目中,咱們通常也會對 Snowflake 算法進行改造,最多見的就是在 Snowflake 算法生成的 ID 中加入業務類型信息。
咱們再來看看 Snowflake 算法的優缺點 :
UidGenerator 是百度開源的一款基於 Snowflake(雪花算法)的惟一 ID 生成器。
不過,UidGenerator 對 Snowflake(雪花算法)進行了改進,生成的惟一 ID 組成以下。
能夠看出,和原始 Snowflake(雪花算法)生成的惟一 ID 的組成不太同樣。而且,上面這些參數咱們均可以自定義。
UidGenerator 官方文檔中的介紹以下:
自 18 年後,UidGenerator 就基本沒有再維護了,我這裏也不過多介紹。想要進一步瞭解的朋友,能夠看看 UidGenerator 的官方介紹。
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 是滴滴開源的一款基於數據庫號段模式的惟一 ID 生成器。
數據庫號段模式的原理咱們在上面已經介紹過了。Tinyid 有哪些亮點呢?
爲了搞清楚這個問題,咱們先來看看基於數據庫號段模式的簡單架構方案。(圖片來自於 Tinyid 的官方 wiki:《Tinyid 原理介紹》)
在這種架構模式下,咱們經過 HTTP 請求向發號器服務申請惟一 ID。負載均衡 router 會把咱們的請求送往其中的一臺 tinyid-server。
這種方案有什麼問題呢?在我看來(Tinyid 官方 wiki 也有介紹到),主要由下面這 2 個問題:
除此以外,HTTP 調用也存在網絡開銷。
Tinyid 的原理比較簡單,其架構以下圖所示:
相比於基於數據庫號段模式的簡單架構方案,Tinyid 方案主要作了下面這些優化:
Tinyid 的優缺點這裏就不分析了,結合數據庫號段模式的優缺點和 Tinyid 的原理就能知道。
這篇文章中,我基本上已經把最多見的分佈式 ID 生成方案都總結了一波。
最後再推薦一個很是不錯的 Java 教程類開源項目:JavaGuide 。我在大三開始準備秋招面試的時候,建立了 JavaGuide 這個項目。目前這個項目已經有 100k+的 star,相關閱讀:《1049 天,100K!簡單覆盤!》 。
對於你學習 Java 以及準備 Java 方向的面試都頗有幫助!正如做者說的那樣,這是一份:涵蓋大部分 Java 程序員所須要掌握的核心知識的 Java 學習+面試指南!
相關推薦:
我是 Guide哥,擁抱開源,喜歡烹飪。開源項目 JavaGuide 做者,Github:Snailclimb - Overview 。將來幾年,但願持續完善 JavaGuide,爭取可以幫助更多學習 Java 的小夥伴!共勉!凎!點擊查看個人2020年工做彙報!
除了上面介紹的方式以外,像 ZooKeeper 這類中間件也能夠幫助咱們生成惟一 ID。沒有銀彈,必定要結合實際項目來選擇最適合本身的方案。