本文主要介紹在一個分佈式系統中, 如何去生成全局唯一的 ID。
前言
單純的生成全局ID並不是什麼難題,生成全局的 unique ID 要滿足以下需求:
- 保證生成的 ID 全局唯一
- 今後數據在多個 Shards 之間遷移不會受到 ID 生成方式的限制
- 生成的 ID 中最好能帶上時間信息, 例如 ID 的前 k 位是 Timestamp, 這樣能夠直接通過對 ID 的前 k位的排序來對數據按時間排序
- 生成的 ID 最好不大於 64 bits
- 生成 ID 的速度有要求. 例如, 在一個高吞吐量的場景中, 需要每秒生成幾萬個 ID (Twitter 最新的峯值到達了 143,199
Tweets/s, 也就是 10萬+/秒) - 整個服務最好沒有單點
問題描述
當用戶量激增 系統架構演進到一定的階段,常常會設計到分庫分表,
例如根據id對用戶表(t_user)進行分表,[0,999999]保存在t_user_0表,[1000000,1999999]保存在t_user_1表中,依次類推,怎麼給這些用戶生成全局的 unique ID?
全局ID產生的幾種方式
1、數據庫自增id
當服務使用的數據庫只有單庫單表時,可以利用數據庫的auto_increment來生成全局唯一遞增ID.
優勢:
- 簡單,無需程序任何附加操作
- 保持定長的增量
- 在單表中能保持唯一性
劣勢:
- 高併發下性能不佳,主鍵產生的性能上限是數據庫服務器單機的上限。
- 水平擴展困難,在分佈式數據庫環境下,無法保證唯一性。
2、UUID
一般的編程語言中會自帶UUID的實現,比如Java中UUID方式UUID.randomUUID().toString(),可以通過服務程序本地產生,ID的生成不依賴數據庫的實現。
優勢:
- 本地生成ID,不需要進行遠程調用。
- 全局唯一不重複。
- 水平擴展能力非常好。
劣勢:
- ID有128 bits,佔用的空間較大,需要存成字符串類型,索引效率極低。
- 生成的ID中沒有帶Timestamp,無法保證趨勢遞增
3、Flickr 的全局主鍵生成方案
flickr巧妙地使用了MySQL的自增ID,及replace into語法,十分簡潔地實現了分片ID生成功能。詳見 :http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/
比如創建64位的自增id:
首先,創建一個表:
CREATE TABLE `uid_sequence` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`stub` char(1) NOT NULL default '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=MyISAM;
SELECT * from uid_sequence 輸出:
+——————-+——+
| id | stub |
+——————-+——+
| 72157623227190423 | a |
如果我需要一個全局的唯一的64位uid,則執行:
REPLACE INTO uid_sequence (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
說明:
- 用 REPLACE INTO 代替 INSERT INTO 的好處是避免錶行數太大,還要另外定期清理。
- stub 字段要設爲唯一索引,這個 sequence 表只有一條紀錄,但也可以同時爲多張表生成全局主鍵,例如user_order_id。除非你需要表的主鍵是連續的,那麼就另建一個 user_order_id_sequence 表。
- 經過實際對比測試,使用 MyISAM 比 Innodb 有更高的性能。
這裏flickr使用兩臺數據庫(也可以更多)作爲自增序列生成,通過這兩臺機器做主備和負載均衡。
TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1
TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2
優點:
- 簡單可靠。
缺點:
- id只是一個ID,沒有帶入時間,shardingId等信息。
4、Twitter Snowflake
twitter利用zookeeper實現了一個全局ID生成的服務Snowflake:https://github.com/twitter/snowflake
Snowflake 生成的 unique ID 的組成 (由高位到低位):
- 41 bits: Timestamp (毫秒級)
- 10 bits: 節點 ID (datacenter ID 5 bits + worker ID 5 bits)
- 12 bits: sequence number
一共 63 bits (最高位是 0)
unique ID 生成過程:
- 10 bits 的機器號, 在 ID 分配 Worker 啓動的時候,從一個 Zookeeper 集羣獲取 (保證所有的 Worker 不會有重複的機器號);
- 41 bits 的 Timestamp: 每次要生成一個新 ID 的時候,都會獲取一下當前的 Timestamp, 然後分兩種情況生成 sequence number;
- 如果當前的 Timestamp 和前一個已生成 ID 的 Timestamp 相同 (在同一毫秒中),就用前一個 ID 的 sequence number + 1 作爲新的 sequence number (12 bits); 如果本毫秒內的所有 ID 用完,等到下一毫秒繼續 (這個等待過程中, 不能分配出新的 ID);
- 如果當前的 Timestamp 比前一個 ID 的 Timestamp 大, 隨機生成一個初始 sequence number (12bits) 作爲本毫秒內的第一個 sequence number;
整個過程中只是在 Worker 啓動的時候會對外部有依賴 (需要從 Zookeeper 獲取 Worker 號) 之後就可以獨立工作了,做到了去中心化。
5、Instagram的做法
instagram參考了flickr的方案,再結合twitter的經驗,利用Postgre數據庫的特性,實現了一個更簡單可靠的ID生成服務。鏈接:http://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram
instagram unique ID 的組成:
- 41 bits: Timestamp (毫秒)
- 13 bits: 每個 logic Shard 的代號 (最大支持 8 x 1024 個 logic Shards)
- 10 bits: sequence number; 每個 Shard 每毫秒最多可以生成 1024 個 ID
以instagram舉的例子爲說明:
假定時間是September 9th, 2011, at 5:00pm,則毫秒數是1387263000(直接使用系統得到的從1970年開始的毫秒數)。那麼先把時間數據放到ID裏:
id = 1387263000 << (64-41)
再把分片ID放到時間裏,假定用戶ID是31341,有2000個邏輯分片,則分片ID是31341 % 2000 -> 1341:
id |= 1341 << (64-41-13)
最後,把自增序列放ID裏,假定前一個序列是5000,則新的序列是5001:
id |= (5001 % 1024)
這樣就得到了一個全局的分片ID。
我們可以通過INSERT語句的RETURNING 關鍵字,將ID返回給應用程序;
這裏是the PL/PGSQL的完整例子(例子的schema :insta5):
CREATE OR REPLACE FUNCTION insta5.next_id(OUT result bigint) AS $$
DECLARE
our_epoch bigint := 1314220021721;
seq_id bigint;
now_millis bigint;
shard_id int := 5;
BEGIN
SELECT nextval('insta5.table_id_seq') %% 1024 INTO seq_id;
SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000) INTO now_millis;
result := (now_millis - our_epoch) << 23;
result := result | (shard_id << 10);
result := result | (seq_id);
END;
$$ LANGUAGE PLPGSQL;
And when creating the table, we do:
CREATE TABLE insta5.our_table (
"id" bigint NOT NULL DEFAULT insta5.next_id(),
...rest of table schema...
)
6、其他方案
例如:MongoDB的ObjectId,採用12個字節的長度,並且將時間戳進行編碼。鏈接:https://docs.mongodb.com/manual/reference/method/ObjectId/