分佈式系統中生成全局唯一ID方案

本文主要介紹在一個分佈式系統中, 如何去生成全局唯一的 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/

參考資料

http://darktea.github.io/notes/2013/12/08/Unique-ID

發佈了486 篇原創文章 · 獲贊 405 · 訪問量 296萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章